Authentication
api.MountWithConfig is api.Mount plus a Config: a global default
auth policy and generic middleware (logging, CORS, …) applied to every
resource passed to it.
Auth is an object, not a route-name string list — a
goninja.Authenticator you attach at the point of registration,
mirroring how Django Ninja’s AuthBase works rather than DRF’s
permission_classes:
type BearerAuth struct{}
func (BearerAuth) Name() string { return "bearer" }
func (BearerAuth) SecurityScheme() openapi.SecurityScheme {
return openapi.SecurityScheme{Type: "http", Scheme: "bearer"}
}
func (BearerAuth) Authenticate(r *http.Request) (goninja.User, bool) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
user, ok := lookupUser(token) // yours
return user, ok
}
cfg := goninja.Config{
DefaultAuth: goninja.AuthPolicy{
Routes: []goninja.Route{goninja.RouteCreate, goninja.RouteUpdate, goninja.RouteDelete},
Auth: []goninja.Authenticator{BearerAuth{}},
},
Middleware: []func(http.Handler) http.Handler{LoggingMiddleware()},
}
app.MountWithConfig(mux, cfg,
api.NewAuthorResource(db),
api.NewBookResource(db),
)DefaultAuth.Routes names the routes that require auth by default;
DefaultAuth.Auth is the list of Authenticators tried, in order,
against a protected request — the first one whose Authenticate returns
ok wins and its User is attached to the context, and the request is
rejected with 401 only once every Authenticator has declined.
Authenticate itself never writes to the response, which is what makes
trying several Authenticators in sequence safe. Config.Middleware
wraps every route on every resource unconditionally, public or not — for
logging/CORS-style concerns that aren’t about identity. A resource that
reads the authenticated user retrieves it with
goninja.UserFromContext(ctx), the User interface being just ID() string; goninja never constructs one itself.
Because an Authenticator self-describes its own SecurityScheme(), the
same object that enforces auth at runtime is also the sole source of
truth for how it’s documented — every protected route’s generated
OpenAPI() carries a matching Security requirement, so enforcement and
documentation can never drift apart.
Override the default per resource or per route with
ResourceConfig.Auth map[goninja.Route]goninja.RouteAuth — see
Custom Path and Restricted Routes. Plain api.Mount still
works exactly as before — a resource it mounts gets a zero Config, so
nothing is protected and no middleware runs, unless you switch that
resource to api.MountWithConfig.
Built-in Authenticators
Writing Name()/SecurityScheme()/Authenticate by hand for a common
scheme is boilerplate goninja ships ready-made — each takes only the
Verify closure that turns a raw credential into a User:
goninja.HTTPBearer{Verify: func(token string) (goninja.User, bool) { ... }}
goninja.HTTPBasic{Verify: func(username, password string) (goninja.User, bool) { ... }}
goninja.APIKeyHeader{Verify: func(key string) (goninja.User, bool) { ... }} // default header: X-API-Key
goninja.CookieKey{Verify: func(value string) (goninja.User, bool) { ... }} // default cookie: sessionEach has an optional field to change what it reads (HeaderName,
CookieName) and an AuthName to rename its OpenAPI security scheme.
Anything more unusual — a custom header, combining multiple credentials,
a scheme not listed here — still just implements Authenticator
directly, as in the BearerAuth example above.
A runnable proof of this end to end lives in examples/prototype:
auth.go’s newAPIKeyAuth builds a goninja.APIKeyHeader and main.go
wires it in — set PROTOTYPE_API_KEY before starting the server and
create/update/delete on every resource require an X-API-Key header
matching it; leave it unset and the prototype stays fully public, for
frictionless local exploration.