jwt
Authenticate requests
gas/auth provides four credential types behind one interface. Core declares
gas.Authenticator, gas.Authorizer, and gas.PrincipalRevoker; the module
implements them and adds middleware.
go get github.com/gasmod/gas/authsession
apikey
token
Wiring
Section titled “Wiring”// Start from DefaultConfig: it fills in SigningMethod and Expiry, and a// zero Expiry fails validation.jwtCfg := jwt.DefaultConfig()jwtCfg.JWT.SigningKey = "a-signing-key-of-at-least-32-bytes!!"
app := gas.NewApp( gas.WithServiceInstance[gas.ConfigProvider](cfg), gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()),
// session, apikey and token persist credentials and own migrations. gas.WithSingletonService[gas.DatabaseProvider](database.New()), gas.WithSingletonService[gas.MigrationManager](migrate.New()),
gas.WithSingletonService[*jwt.Service](jwt.New(jwt.WithConfig(jwtCfg))), gas.WithSingletonService[*session.Service](session.New()),)Build configs from DefaultConfig(). A bare &jwt.Config{} literal leaves
Expiry at zero, which fails validation at startup.
Protecting routes
Section titled “Protecting routes”auth.Middleware runs an authenticator and puts the resulting gas.Principal
on the request context. auth.Chain tries several in order, so one group can
accept a JWT, a session cookie, or an API key.
// Middleware turns a request into a gas.Principal and stores it on the// context. Chain tries each authenticator in order, so one route group can// accept a JWT, a session cookie, or an API key.func protect(router *gas.Router, jwtSvc *jwt.Service, sessSvc *session.Service, keySvc *apikey.Service) { chain := auth.Chain{jwtSvc, sessSvc, keySvc}
router.Route("/api", func(sub *gas.Router) { sub.UseMiddlewareFunc(auth.Middleware(chain)) sub.Handle("notes", http.MethodGet, "/notes", listNotes) })}Add auth.RequireScheme(auth.SchemeSession) when a route must insist on one
specific credential type, for example a settings page that a long-lived API key
should not reach.
Reading the principal
Section titled “Reading the principal”// Downstream, the principal is on the context. Metadata is read type-safely.func listNotes(ctx gas.Context) error { p := gas.PrincipalFromContext(ctx) if p == nil { return gas.Unauthorized("sign in to continue") }
if role, ok := gas.MetadataValue[string](p.Metadata(), "role"); ok && role != "admin" { return gas.Forbidden("admins only") }
return ctx.JSON(http.StatusOK, map[string]string{"subject": p.Subject()})}A Principal carries Subject() (the stable user id), Scheme() (jwt,
session, apikey), CredentialID() (the specific session or key), and
metadata. gas.MetadataValue[T] reads metadata without an unchecked type
assertion.
Issuing credentials
Section titled “Issuing credentials”// Sign a token after verifying a password, then hand it to the client.func issueToken(jwtSvc *jwt.Service, userID string) (string, error) { return jwtSvc.Sign(userID, map[string]any{"role": "admin"})}Sessions are created with Create(ctx, subject, meta, r) and written with
SetCookie. API keys come from Generate(ctx, subject, name, scopes, opts...),
which returns the plaintext key exactly once; only a SHA-256 hash is stored.
Single-use tokens come from Issue and are consumed by Verify, which fails on
the second attempt.
Error responses
Section titled “Error responses”By default a failed authentication writes a plain 401. To return the
unified error shape instead, take over the response:
// By default the middleware writes a plain 401. WithOnError takes over the// response, which is how you return the unified JSON error shape instead.func jsonUnauthorized() auth.MiddlewareOption { return auth.WithOnError(func(w http.ResponseWriter, r *http.Request, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) })}Revoking
Section titled “Revoking”session and apikey implement gas.PrincipalRevoker: Revoke kills one
credential, RevokeAll every credential for a subject, and RevokeAllByScheme
every credential of one type. JWTs are stateless and cannot be revoked
individually, which is the trade you accept for not touching a database on every
request. Keep their expiry short if that matters.
API key operations can join your own transaction. apiKeyService.WithTx(tx)
returns a provider scoped to that transaction, so creating a user and their
first key either both happen or neither does.
Databases
Section titled “Databases”session, apikey, and token support PostgreSQL, MySQL, and SQLite. The
dialect is chosen automatically from the driver name.
Testing
Section titled “Testing”authtest provides MockAuthenticator, MockAuthorizer, and MockRevoker,
each recording calls and delegating to an optional Fn field, so a protected
handler can be tested without issuing real credentials. See
Test a service.