Security & authentication#

rackattack validates signed OIDC/JWT bearer tokens at every transport edge and gates the surface with role-based authorization in the shared service core — so the same rules apply whether a request arrives over gRPC, REST, or GraphQL.

How it works#

  • Token validation at the edge. A gRPC unary interceptor and an HTTP middleware (covering both REST and GraphQL) validate the bearer token and extract a principal.
  • Authorization in the core. The principal flows into a shared Authorizer that gates the surface: reads are open to any authenticated principal, while mutations and bulk operations require a writer/admin role. Because REST and GraphQL call the service in-process, authorization lives in the wrapper they all share — not only in the gRPC chain.

Both RS256/ES256 (via a JWKS endpoint) and HS256 (via a configured symmetric secret) are supported.

Configuration#

Config is environment-driven:

EnvMeaningDefault
AUTH_ENABLEDturn auth on; off = dev bypass (full surface open, no token)false
AUTH_JWKS_URLOIDC JWKS endpoint for RS/ES tokens
AUTH_HS256_SECRETconfigured symmetric key (use instead of JWKS)
AUTH_ISSUER / AUTH_AUDIENCEchecked against the token’s iss / aud when set
AUTH_ROLES_CLAIMclaim holding roles; dotted for nested (e.g. realm_access.roles)roles
AUTH_WRITER_ROLESroles permitted to mutatewriter,admin

Browser login (web UI)#

API clients (REST/GraphQL/MCP) send a bearer token directly. A browser can’t, so the web UI uses a distinct OIDC authorization-code login: an unauthenticated HTML navigation to /ui is redirected to /auth/login, which bounces through your identity provider (state + PKCE) and lands at /auth/callback. The callback verifies the returned id_token with the same verifier the API path uses, then sets a signed, HttpOnly, SameSite=Lax session cookie (Secure in production). /auth/logout clears it; if the IdP advertises an end_session_endpoint, logout also RP-initiates there (with id_token_hint + client_id) so the IdP session ends too, not just the local cookie. The cookie is a stateless HS256 token — no server-side session store — so it works across replicas; its TTL (not the id_token lifetime) governs how long a login lasts, so no refresh-token handling is needed.

This is enabled when AUTH_ENABLED=true and the OIDC variables below are set; otherwise a tokenless browser request to /ui just gets a 401, as before.

EnvMeaningDefault
AUTH_OIDC_ISSUERIdP discovery base (/.well-known/openid-configuration)falls back to AUTH_ISSUER
AUTH_OIDC_CLIENT_IDOAuth client id registered at the IdP
AUTH_OIDC_CLIENT_SECRETclient secret (omit for a public client)
AUTH_OIDC_REDIRECT_URLmust match an IdP-registered redirect, e.g. https://host/auth/callback
AUTH_OIDC_SCOPESrequested scopesopenid,profile,email
AUTH_SESSION_SECRETHS256 key signing the session + login-transaction cookies (required when OIDC is on)
AUTH_SESSION_TTLhow long a login lasts before re-auth12h
AUTH_OIDC_POST_LOGOUT_REDIRECT_URIabsolute URL the IdP returns to after RP-initiated logout (must be IdP-registered); unset → the IdP shows its own page

AUTH_JWKS_URL should point at the same IdP, since the browser id_token is verified the same way as an API bearer token.

Dev-mode bypass#

Local dev runs with AUTH_ENABLED unset, so the edges inject a dev principal carrying the writer role and the whole surface stays open — no identity provider required. To lock it down, set AUTH_ENABLED=true with a key source:

AUTH_ENABLED=true \
AUTH_JWKS_URL=https://idp.example.com/.well-known/jwks.json \
AUTH_ISSUER=https://idp.example.com/ \
AUTH_AUDIENCE=rackattack \
rackattack serve

The /metrics endpoint sits outside the auth middleware so Prometheus can scrape it without a token (see Observability).

The implementation lives in internal/auth.