auth

package
v0.20.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 13 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var ErrForbidden = errors.New("auth: forbidden")

ErrForbidden is returned when an identity is present but lacks the required permissions. Middleware converts this to 403.

View Source
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

func DefaultPermissions(id *Identity, required []string) bool

DefaultPermissions is the built-in permission check: every required permission must appear in the identity's Roles or Scopes.

func Module

func Module(cfg Config) nexus.Option

Module wires auth into the nexus app:

  1. Installs a global middleware that extracts + (optionally caches) resolves the identity per request, then stashes it on the request context.
  2. Stashes the shared moduleState on the context so per-op Required / Requires bundles can read custom PermissionFn / cache config.
  3. 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"))

func User

func User[T any](ctx context.Context) (*T, bool)

User is the typed convenience accessor: pulls the Identity from ctx and type-asserts Extra to T. Returns (zero, false) if either step fails — a single check at the top of a resolver suffices.

user, ok := auth.User[MyUser](ctx)
if !ok { return nil, fmt.Errorf("no user") }

func WithIdentity

func WithIdentity(ctx context.Context, id *Identity) context.Context

WithIdentity returns a new context with the Identity attached. The global middleware calls this after a successful resolve; tests and custom transports can call it directly.

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

type CachedIdentity struct {
	TokenPrefix string
	Identity    *Identity
	ExpiresAt   time.Time
}

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

type Extractor interface {
	Extract(r *http.Request) (string, bool)
}

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

func APIKey(header string) Extractor

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.

func Chain

func Chain(extractors ...Extractor) Extractor

Chain runs extractors in order and returns the first hit. Useful when an app accepts both a Bearer token (programmatic clients) and a session cookie (browser clients) on the same handler:

auth.Chain(auth.Bearer(), auth.Cookie("session"))
func Cookie(name string) Extractor

Cookie reads the value of a named cookie (typically a session ID). Non-existent cookie → ("", false), same treatment as a missing Bearer header, so public routes keep working.

type ExtractorFunc

type ExtractorFunc func(r *http.Request) (string, bool)

ExtractorFunc adapts a plain function to the Extractor interface.

func (ExtractorFunc) Extract

func (f ExtractorFunc) Extract(r *http.Request) (string, bool)

Extract satisfies the Extractor interface for ExtractorFunc.

type Identity

type Identity struct {
	ID     string
	Roles  []string
	Scopes []string
	Extra  any
}

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

func IdentityFrom(ctx context.Context) (*Identity, bool)

IdentityFrom returns the Identity on ctx, if any. Returns (nil, false) for anonymous requests — Required() is what turns that into a 401.

func (*Identity) Has

func (i *Identity) Has(perm string) bool

Has reports whether the identity carries the given permission in either Roles or Scopes. Used by the default PermissionFn.

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

func (m *Manager) Invalidate(token string)

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

func (m *Manager) InvalidateByIdentity(id string) int

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".

func (*Manager) Resolve added in v0.5.1

func (m *Manager) Resolve(ctx context.Context, token string) (*Identity, error)

Resolve is a direct synchronous resolution path for code that has a token in hand outside the HTTP request cycle — background jobs, WS message handlers, CLI tools bolted onto the same app. Honors the configured cache.

type PermissionFn

type PermissionFn func(id *Identity, required []string) bool

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.

type Resolver

type Resolver func(ctx context.Context, token string) (*Identity, error)

Resolver turns a raw token into an Identity. Callers implement this to plug their auth backend in — a DB lookup, a JWT verification, an external API call, anything. Returning an error fails authentication for this request (401 when Required() is attached).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL