auth

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2026 License: MIT Imports: 12 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 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)
}

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

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