Documentation
¶
Overview ¶
Package auth is nexus's built-in authentication surface. It owns the plumbing — token extraction, identity caching, per-op enforcement, context propagation — while leaving the *resolution* step (token → Identity) user-supplied. That keeps auth.Module unopinionated: works with JWTs, opaque bearer tokens, API keys, session cookies, or any custom scheme, as long as the caller can turn a raw token into an *auth.Identity.
Minimal wiring:
nexus.Run(nexus.Config{...},
auth.Module(auth.Config{
Resolve: func(ctx context.Context, tok string) (*auth.Identity, error) {
u, err := myAPI.ValidateToken(ctx, tok)
if err != nil { return nil, err }
return &auth.Identity{
ID: u.ID,
Roles: u.Roles,
Extra: u,
}, nil
},
Cache: auth.CacheFor(15 * time.Minute),
}),
advertsModule,
)
Per-op enforcement (cross-transport — same bundle works on REST + GraphQL via the existing nexus.Use attachment):
nexus.AsMutation(NewCreateAdvert,
auth.Required(), // 401 if no valid identity
auth.Requires("ROLE_CREATE_ADVERT"), // 403 if missing permission
)
Resolver access from a handler:
func NewListAdverts(db *DB) func(ctx context.Context) ([]Advert, error) {
return func(ctx context.Context) ([]Advert, error) {
user, ok := auth.User[MyUser](ctx)
if !ok { /* Required() would have caught this earlier */ }
return db.ListFor(user.ID)
}
}
Coexistence with the existing (*Service).Auth API: auth.Module operates at the app layer via a global middleware, so services that still call (*Service).Auth(UserDetailsFn) keep working as before. Over time, migrate resolvers from graph.GetRootInfo to auth.IdentityFrom/User.
Index ¶
- Variables
- func DefaultPermissions(id *Identity, required []string) bool
- func Module(cfg Config) nexus.Option
- func Optional() nexus.MiddlewareOption
- func Required() nexus.MiddlewareOption
- func Requires(perms ...string) nexus.MiddlewareOption
- func User[T any](ctx context.Context) (*T, bool)
- func WithIdentity(ctx context.Context, id *Identity) context.Context
- type CacheOption
- type CachedIdentity
- type Config
- type Extractor
- type ExtractorFunc
- type Identity
- type Manager
- type PermissionFn
- type Resolver
Constants ¶
This section is empty.
Variables ¶
var ErrForbidden = errors.New("auth: forbidden")
ErrForbidden is returned when an identity is present but lacks the required permissions. Middleware converts this to 403.
var ErrUnauthenticated = errors.New("auth: unauthenticated")
ErrUnauthenticated is returned by helpers when no identity is on ctx. Middleware converts this to 401 / GraphQL error uniformly.
Functions ¶
func DefaultPermissions ¶
DefaultPermissions is the built-in permission check: every required permission must appear in the identity's Roles or Scopes.
func Module ¶
Module wires auth into the nexus app:
- Installs a global middleware that extracts + (optionally caches) resolves the identity per request, then stashes it on the request context.
- Stashes the shared moduleState on the context so per-op Required / Requires bundles can read custom PermissionFn / cache config.
- Registers a few "auth" middleware names in the registry so the dashboard's middleware chip list labels them consistently.
Module does NOT touch (*Service).Auth. Services using the older UserDetailsFn hook continue to work alongside; migration is a per-resolver switch from graph.GetRootInfo to auth.User[T].
func Optional ¶
func Optional() nexus.MiddlewareOption
Optional is a no-op bundle that exists purely as dashboard signal — it labels the endpoint as auth-aware without enforcing presence. Useful for public endpoints that still personalize when a user is logged in, so the UI surfaces "this endpoint reads identity".
func Required ¶
func Required() nexus.MiddlewareOption
Required returns a cross-transport middleware bundle that rejects any request lacking a resolved Identity on ctx. 401 on REST, a graphql- native error on GraphQL — same bundle attaches cleanly to both.
nexus.AsMutation(NewCreateAdvert, auth.Required())
func Requires ¶
func Requires(perms ...string) nexus.MiddlewareOption
Requires returns a cross-transport bundle that rejects requests whose Identity doesn't satisfy every listed permission (roles / scopes). Implies authentication — attaching Requires without Required still 401s on anonymous requests, because you can't evaluate permissions on a nil identity.
nexus.AsMutation(NewCreateAdvert, auth.Requires("ROLE_CREATE_ADVERT"))
Types ¶
type CacheOption ¶
type CacheOption struct {
// TTL is how long a resolved identity stays in cache. 0 disables.
TTL time.Duration
// MaxEntries bounds the cache so a misbehaving client can't OOM
// the app by sending many unique tokens. 0 means unbounded.
MaxEntries int
}
CacheOption configures how resolved identities are memoized in-memory. The cache is process-local on purpose — auth state should be short- lived (minutes), and a cross-process cache adds invalidation pain that's rarely worth it. Callers that need cross-process cache can handle it inside their Resolve function.
func CacheFor ¶
func CacheFor(ttl time.Duration) CacheOption
CacheFor is a one-liner for the common case — time-only TTL. Entries are bounded to 4096 by default so an attacker firing endless distinct tokens can't trigger unbounded growth.
type CachedIdentity ¶ added in v0.5.1
CachedIdentity is a redacted snapshot of a cache entry for dashboard / admin display. TokenPrefix is the first 8 characters of the raw token followed by "…"; the full token never leaves the cache.
type Config ¶
type Config struct {
// Extract pulls the raw token from the request. Defaults to Bearer()
// (Authorization: Bearer <token>). Combine strategies with Chain.
Extract Extractor
// Resolve is REQUIRED — the function that turns a raw token into an
// Identity. The package owns extraction, caching, and enforcement;
// Resolve is the single plug the caller supplies.
Resolve Resolver
// Cache memoizes resolved identities so the backend call fires at
// most once per TTL per token. Zero TTL disables caching entirely.
Cache CacheOption
// Permissions overrides the default roles+scopes check. Useful when
// an app has a hierarchical role model or non-trivial scope matching.
Permissions PermissionFn
// OnResolve fires after every successful resolution — good for
// audit logging or per-user metrics.
OnResolve func(ctx context.Context, id *Identity)
// OnFail fires on extraction / resolution failure. The token is
// passed so handlers can log prefixes for diagnostics; do NOT log
// the full token in production.
OnFail func(ctx context.Context, token string, err error)
// OnUnauthenticated customizes the REST 401 response. Default:
// AbortWithStatusJSON(401, {"error": err.Error()}). Override to
// match your app's error envelope (e.g. pkg.Response-style
// success:false payload).
//
// The handler is responsible for calling c.Abort* — return without
// aborting and auth falls back to its default 401 so a misconfigured
// hook never accidentally authorizes a request.
OnUnauthenticated func(c *gin.Context, err error)
// OnForbidden customizes the REST 403 response. Same contract as
// OnUnauthenticated.
OnForbidden func(c *gin.Context, err error)
// GraphQLErrorWrap transforms ErrUnauthenticated / ErrForbidden
// before they're returned from a resolver. Default: pass through
// so the standard "auth: unauthenticated" / "auth: forbidden"
// messages appear in the GraphQL errors array. Override to wrap
// in a typed error the client expects.
GraphQLErrorWrap func(err error) error
}
Config drives auth.Module. Only Resolve is required; everything else has a sensible default.
type Extractor ¶
Extractor pulls a raw token from an HTTP request. Returns ("", false) when no token is present — callers treat that as "anonymous request" rather than an error, so public endpoints still resolve a nil Identity and proceed.
func APIKey ¶
APIKey reads a named header (e.g. "X-API-Key"). Trailing whitespace stripped for ergonomics; empty values are treated as absent.
func Bearer ¶
func Bearer() Extractor
Bearer reads "Authorization: Bearer <token>". Case-insensitive on the scheme name per RFC 7235. Empty tokens ("Bearer ") are treated as absent so downstream Required() correctly fires 401.
type ExtractorFunc ¶
ExtractorFunc adapts a plain function to the Extractor interface.
type Identity ¶
Identity is the resolved authenticated user. Roles and Scopes are the two first-class permission buckets; Extra carries any backend-specific payload the caller wants to thread through to resolvers.
func IdentityFrom ¶
IdentityFrom returns the Identity on ctx, if any. Returns (nil, false) for anonymous requests — Required() is what turns that into a 401.
type Manager ¶ added in v0.5.1
type Manager struct {
// contains filtered or unexported fields
}
Manager is the runtime handle for auth state. fx.Provide'd by Module so application code can inject it wherever it needs to invalidate cached identities (logout flows) or inspect current auth state (admin dashboards).
func NewLogoutHandler(am *auth.Manager) func(ctx, p Params[Args]) (...) {
return func(ctx context.Context, p Params[Args]) (..., error) {
am.Invalidate(p.Args.Token)
return ok, nil
}
}
func (*Manager) Identities ¶ added in v0.5.1
func (m *Manager) Identities() []CachedIdentity
Identities returns a snapshot of every currently-cached identity. Safe to call on a disabled cache (returns empty slice). Token prefixes are truncated to 8 chars — never log or return the full token back to clients.
func (*Manager) Invalidate ¶ added in v0.5.1
Invalidate drops the cached identity for the given token. The next request bearing that token will re-run Resolve. No-op when the cache is disabled.
func (*Manager) InvalidateAll ¶ added in v0.5.1
func (m *Manager) InvalidateAll()
InvalidateAll flushes the entire identity cache. Use sparingly — every active session will pay a Resolve round-trip on its next request. Intended for credential-schema migrations or incident response.
func (*Manager) InvalidateByIdentity ¶ added in v0.5.2
InvalidateByIdentity removes every cache entry whose Identity.ID matches the argument. Use for "force-logout user X" flows when the caller knows the stable identity but not the tokens (users may have multiple active sessions). Returns the number of entries dropped so the caller can distinguish "forced logout of 3 sessions" from "no cached sessions to drop".
type PermissionFn ¶
PermissionFn decides whether an identity satisfies a set of required permissions. The built-in default (DefaultPermissions) requires the identity to have every listed permission in Roles or Scopes.
func AllOf ¶ added in v0.5.1
func AllOf(perms ...string) PermissionFn
AllOf is the same shape as DefaultPermissions but fixes the required set at construction time. Useful for app-wide baseline policies:
auth.Module(auth.Config{
Permissions: auth.AllOf("authenticated"),
})
Per-op Requires() can still add on top via the default path by changing Permissions to a composite — see the docs example.
func AnyOf ¶ added in v0.5.1
func AnyOf(perms ...string) PermissionFn
AnyOf returns a PermissionFn that passes when the identity has AT LEAST ONE of the configured permissions. Use when a handler should accept multiple overlapping roles — "admin OR editor" — without spelling out the OR at the call site.
auth.Module(auth.Config{
Permissions: auth.AnyOf("admin", "editor"),
})
Note: the `required` argument to the returned PermissionFn is IGNORED — AnyOf's behavior is fully configured at construction. Use Requires("role") + a custom permission model when you need the Requires call site to drive the set.