Abstract

Better-Goth is a better-auth alternative, written purely in Go. It provides a complete OAuth 2.1 and OpenID Connect implementation as an embeddable library, giving you full control over your authentication flow without external dependencies or managed services.

Purely exists because golang cool and js ugly and we wanted to learn about OAuth.

Useful if you want a simple plug-and-play library for your web-app for both custom OAuth Providers and self-deployable OAuth server. Can be easily incorporated as it uses built-in net/http

Status of This Memo

This library is production ready.

1. Design Decisions

Better-Goth was made as a library so the user keeps complete control over what the application needs, and so wiring it in stays easy.

A few choices follow from that:

  • It embeds into your app instead of running next to it. You mount an http.Handler on your own mux, so there is no second service to deploy, patch, or keep alive and is also compatible with all backend frameworks.
  • dev_mode: true downgrades cookies for plain-HTTP localhost work.

2. How It Works

Three public files carry most of the weight:

File Job
auth.go Social/OIDC login. Builds the redirect with PKCE, checks state and nonce via short-lived cookies, verifies the upstream ID token, mints your session JWT
embed.go The embedded authorization server. Users, password reset over SMTP, access/device/recovery tokens, refresh rotation
verify.go Bearer verification and the RequireAuth middleware that drops a VerifiedUser into the request context

Under internal/ sit the oauth server handlers, key manager, storage, and mailer. The endpoints are:

GET  /authorize                            start the OAuth flow
POST /oauth/token                          code exchange & refresh
POST /oauth/token/revocation               RFC 7009 revocation
POST /oauth/token/introspection            RFC 7662 introspection
GET  /.well-known/openid-configuration     OIDC discovery
GET  /.well-known/jwks.json                signing keys
GET  /userinfo                             authenticated claims
GET  /admin/rotate                         key rotation

Social login runs on two routes:

GET /login/{provider}      redirect to provider with PKCE + state + nonce
GET /callback/{provider}   validate, exchange, verify, issue session

Flow cookies are HttpOnly, SameSite=Lax, and short lived. The code verifier gets five minutes, and each cookie clears after one use.

What you can override: SetUserHandler runs after every successful login (sync the user into your own DB there), SetAuthResultHandler decides what happens after auth (the built-in session handler sets a cookie and redirects), and storage.type in config swaps the backing store.

3. Quickstart

go get github.com/Protofarm/better-goth
package main

import (
    "log"
    "net/http"

    bettergoth "github.com/Protofarm/better-goth"
)

func main() {
    rt, err := bettergoth.Setup("config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    defer rt.Close()

    mux := http.NewServeMux()

    // oauth/oidc endpoints: authorize, token, jwks, userinfo...
    mux.Handle("/", rt.Handler())

    // social login: GET /login/{provider}, GET /callback/{provider}
    bettergoth.RegisterRoutes(mux, rt.Auth)

    // anything behind RequireAuth gets a VerifiedUser in context
    protected := rt.Auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, _ := bettergoth.UserFromContext(r.Context())
        bettergoth.WriteJSON(w, http.StatusOK, map[string]string{
            "hello": user.Subject,
        })
    }))
    mux.Handle("/api/me", protected)

    log.Fatal(http.ListenAndServe(rt.ListenAddr, mux))
}

And a config to setup both the oauthserver and google oauth.

app:
  port: "3000"
  dev_mode: true

providers:
  oauthserver:
    enabled: true
    issuer_url: "http://localhost:8080"
    client_id: "my-client"
    client_secret: "my-secret"
    redirect_uris:
      - "http://localhost:3000/callback/oauthserver"

  google:
    enabled: true
    auth_url: "https://accounts.google.com"
    client_id: "${GOOGLE_CLIENT_ID}"
    client_secret: "${GOOGLE_CLIENT_SECRET}"
    redirect_uri: "http://localhost:3000/callback/google"

jwt:
  secret: "replace-with-at-least-32-bytes-secret"

storage:
  type: "memory"

examples/ shows just this.

4. Standards & Compliance Notes

The RFC list:

  • RFC 6749: the core framework. Authorization code flow, token exchange, refresh grants.
  • OAuth 2.1 (draft): PKCE everywhere, nothing implicit. We target this over 2.0 so the secure path is the only path.
  • RFC 7636: PKCE with S256 challenges. The verifier never leaves a short-lived cookie.
  • RFC 7009: token revocation at /oauth/token/revocation.
  • RFC 7662: introspection at /oauth/token/introspection, useful for resource servers.
  • RFC 7517: JWKS at /.well-known/jwks.json so clients can verify signatures themselves.
  • RFC 7523: JWT bearer profile, shipped as private_key_jwt client authentication.
  • RFC 7591: dynamic client registration.
  • RFC 9700: the security BCP.

5. Roadmap & Limitations

  • Caching uses only default in-memory maps and redis implementation is pending (Check out Rsdis)

6. See Also