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
Authorizerthat 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:
| Env | Meaning | Default |
|---|---|---|
AUTH_ENABLED | turn auth on; off = dev bypass (full surface open, no token) | false |
AUTH_JWKS_URL | OIDC JWKS endpoint for RS/ES tokens | — |
AUTH_HS256_SECRET | configured symmetric key (use instead of JWKS) | — |
AUTH_ISSUER / AUTH_AUDIENCE | checked against the token’s iss / aud when set | — |
AUTH_ROLES_CLAIM | claim holding roles; dotted for nested (e.g. realm_access.roles) | roles |
AUTH_WRITER_ROLES | roles permitted to mutate | writer,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.
| Env | Meaning | Default |
|---|---|---|
AUTH_OIDC_ISSUER | IdP discovery base (/.well-known/openid-configuration) | falls back to AUTH_ISSUER |
AUTH_OIDC_CLIENT_ID | OAuth client id registered at the IdP | — |
AUTH_OIDC_CLIENT_SECRET | client secret (omit for a public client) | — |
AUTH_OIDC_REDIRECT_URL | must match an IdP-registered redirect, e.g. https://host/auth/callback | — |
AUTH_OIDC_SCOPES | requested scopes | openid,profile,email |
AUTH_SESSION_SECRET | HS256 key signing the session + login-transaction cookies (required when OIDC is on) | — |
AUTH_SESSION_TTL | how long a login lasts before re-auth | 12h |
AUTH_OIDC_POST_LOGOUT_REDIRECT_URI | absolute 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 serveThe
/metricsendpoint sits outside the auth middleware so Prometheus can scrape it without a token (see Observability).
The implementation lives in
internal/auth.