Go server quickstart

The Go server SDK verifies auth4.dev access tokens the same way as the TypeScript server SDK. Use it in a net/http API or any Go game backend.

Before you start

You need Go 1.22 or later and three values from the console:

OptionConsole valueExample
TenantIDProject IDten_abcdefghijklmnop
IssuerIssuerhttps://auth.auth4.dev/t/ten_abcdefghijklmnop
AudienceAudience (the client ID of the application calling your API)cli_…

Instead of Issuer you can set AuthOrigin, and the SDK uses{AuthOrigin}/t/{TenantID}:

client, err := auth4.New(auth4.Options{
	TenantID:   "ten_…",
	AuthOrigin: "https://auth.auth4.dev", // the SDK uses https://auth.auth4.dev/t/ten_…
	Audience:   "cli_…",
})

Signing keys always come from {issuer}/.well-known/jwks.json, never from a URL in the token.

1. Add the SDK

The SDK is the Go module in packages/sdk-go. It uses the standard library only. It isn't tagged yet, so add it to your go.mod with areplace directive pointing at a copy of the repository:

go.mod
require github.com/Plorax/auth4.dev/packages/sdk-go v0.0.0

// Until the module is tagged, point at a copy of the auth4.dev repository.
replace github.com/Plorax/auth4.dev/packages/sdk-go => ../path/to/auth4/packages/sdk-go

2. Protect your handlers

Create one client per audience with New, and reuse it so its key cache is shared by every request:

Excerpt from examples/go/gameapi/main.go
	// One client per audience. Reuse it, so its signing-key cache is shared by every request.
	client, err := auth4.New(auth4.Options{
		TenantID: os.Getenv("AUTH4_TENANT_ID"),
		Issuer:   os.Getenv("AUTH4_ISSUER"),
		Audience: os.Getenv("AUTH4_AUDIENCE"),
	})
	if err != nil {
		log.Fatal(err)
	}

Middleware checks the request's Authorization: Bearer token and stores the verified token in the request context. A missing or invalid token gets401 with WWW-Authenticate: Bearer.

Excerpt from examples/go/gameapi/main.go
	logOutages := auth4.MiddlewareOptions{
		// Log the code only: the token never reaches the logs.
		OnError: func(_ *http.Request, err *auth4.TokenError) {
			if err.IsOutage() {
				log.Printf("auth4.dev token check unavailable: %s", err.Code)
			}
		},
	}
	signedIn := client.Middleware(logOutages)

	mux := http.NewServeMux()
	// Any signed-in player, including guests.
	mux.Handle("GET /profile", signedIn(http.HandlerFunc(profile)))
	// Players with the moderator role only.
	mux.Handle("POST /moderation/kick", signedIn(auth4.RequireRole("moderator")(http.HandlerFunc(kick))))

In a handler, AccessTokenFromContext() returns the verified token:

Excerpt from examples/go/gameapi/main.go
func profile(w http.ResponseWriter, r *http.Request) {
	token, _ := auth4.AccessTokenFromContext(r.Context())
	// token.Subject is the player's user ID. Key your game data on it.
	writeJSON(w, map[string]any{
		"player": token.Subject,
		"guest":  token.IsGuest,
		"roles":  token.Roles,
		"level":  token.Metadata["level"],
	})
}
  • RequireRole lets through players with at least one of the roles, and answers403 otherwise.
  • RequireNonGuest lets only upgraded players through, and RequireScope requires a granted scope.
  • MiddlewareOptions can let requests without a token through (AllowAnonymous) and report each refused token's error code (OnError).

Run the example from the repository:

cd examples/go
AUTH4_TENANT_ID=ten_… \
AUTH4_ISSUER=https://auth.auth4.dev/t/ten_… \
AUTH4_AUDIENCE=cli_… \
go run ./gameapi

curl -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:5080/profile

To catch a wrong project ID or auth origin at startup, call Discover(). It checks that the issuer's discovery document names the same issuer and key URL:

Excerpt from examples/go/gameapi/main.go
	// Optional: fail fast on a wrong project ID or auth origin.
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if _, err := client.Discover(ctx); err != nil {
		log.Fatal(err)
	}

Without the middleware

Outside net/http, for example in a game server's own protocol, verify the header or token yourself:

Verify a request
token, err := client.ValidateAuthorizationHeader(ctx, r.Header.Get("Authorization"))
switch {
case auth4.IsOutage(err):
	// Signing keys or introspection were unavailable: report an outage, don't sign the player out.
case err != nil:
	// Missing, malformed, expired or otherwise invalid: reject the request.
default:
	// token.Subject is the player's user ID. Key your game data on it.
}

3. Use the claims

FieldClaimMeaning
SubjectsubThe player's user ID. It stays the same when a guest upgrades.
TenantIDtenant_idYour project ID. Always equal to the configured TenantID.
ClientIDclient_idThe application the token was issued to.
ScopesscopeGranted scopes. Check one with HasScope.
SessionIDsidThe session ID, or empty when the token doesn't belong to a session.
IsGuestauth4_guesttrue while the player hasn't upgraded their guest account. The next token after an upgrade says false.
Rolesauth4_rolesThe player's roles in this application. Check them with HasRole orHasAnyRole. See Roles and metadata.
Metadataauth4_metaThe player's flat metadata. Values are string, float64,bool or nil. Empty when the player has none.
ExpiresAtexpAt most five minutes after issue.

What the SDK checks

  • The RS256 signature, with a key from the configured issuer, and the token typeat+jwt.
  • iss, aud, tenant_id, exp andnbf, with no clock tolerance by default. Allow up to 60 seconds of drift withClockTolerance.
  • The claim rules auth4.dev issues tokens with. A token with an unknown or repeated claim, a malformed role or a nested metadata value is rejected.
  • ID tokens, tokens from other projects and console management tokens are always rejected.

ID tokens have their own method, ValidateIDToken. Use it only to confirm a sign-in for your own client ID, never to authorize an API call. Pass an empty nonce to skip the nonce check:

idToken, err := client.ValidateIDToken(ctx, rawIDToken, "cli_…", nonce)

Failures and key rotation

  • Signing keys are cached for five minutes. A token signed with an unknown key ID triggers one key refresh, at most once every 30 seconds, so key rotation needs no restart.
  • Errors are TokenError values. Only the code invalid_token means the token is bad. jwks_unavailable, jwks_refresh_limited andintrospection_unavailable are outages, and IsOutage() istrue.
  • The middleware answers an outage with 503 and Retry-After: 5instead of 401, so players aren't signed out. SetOutageAsUnauthorized to answer 401 instead.

Introspect mode

Introspect mode asks the project's introspection endpoint whether the token's session is still live after offline verification passes. It needs the client ID and secret of a confidential server client of the same project, set through IntrospectionOptions. The example uses a second client for sensitive handlers only, so other requests don't pay for the extra call:

Excerpt from examples/go/gameapi/main.go
	// Upgraded players only. With server client credentials, a revoked session is refused within
	// five seconds instead of when the access token expires.
	trade := signedIn(auth4.RequireNonGuest(http.HandlerFunc(tradeItem)))
	if id, secret := os.Getenv("AUTH4_SERVER_CLIENT_ID"), os.Getenv("AUTH4_SERVER_CLIENT_SECRET"); id != "" {
		sensitive, err := auth4.New(auth4.Options{
			TenantID: client.TenantID(),
			Issuer:   client.Issuer(),
			Audience: client.Audience(),
			Introspection: &auth4.IntrospectionOptions{
				ClientID:     id,
				ClientSecret: secret,
				CacheTTL:     5 * time.Second,
			},
		})
		if err != nil {
			log.Fatal(err)
		}
		trade = sensitive.Middleware(logOutages)(auth4.RequireNonGuest(http.HandlerFunc(tradeItem)))
	}
	mux.Handle("POST /trade", trade)
  • The endpoint is derived from the configured issuer, never from the token. It is available asIntrospectionURL().
  • An active answer is reused for CacheTTL: 5 seconds by default, at most 60. SetDisableCache to check every request. An inactive answer is kept until the token expires.
  • If the endpoint can't be reached or answers with an error, verification fails closed withintrospection_unavailable.
  • The SDK never logs the secret, and leaves it out when the options or the client are printed or encoded as JSON. Load it from your secret store, never from source code.

Create a server client in the console under Applications; see Server clients and their secrets.

SDK notes: packages/sdk-go/README.md. Example: examples/go/README.md. Tests: packages/sdk-go (go test ./...) and tests/integration/auth4d-170.go-sdk.test.ts.