Environment-Aware Authorization
QAuth treats environment — development, staging, or production — as a
first-class policy-profile dimension. One attribute flips a coordinated bundle
of security and operational defaults, so an operator hardens a deployment by
setting a single value instead of getting a dozen independent switches right.
This is the operator/how-to guide. For the design rationale, the fail-safe
reasoning, and the prior-art comparison, see
ADR-008. The resolver itself lives
in apps/auth-server/src/app/helpers/environment-policy.ts.
The two knobs
Section titled “The two knobs”Environment is set on two columns, both defaulting to production:
| Column | Meaning |
|---|---|
oauth_clients.environment | The client’s declared environment. |
realms.max_environment_laxity | A realm-level ceiling on how lax any client in that realm may be. |
The effective environment is the stricter of the two
(production > staging > development). A realm pinned to production forces
every client in it to the production profile regardless of the client’s own field.
effective = stricter(client.environment, realm.max_environment_laxity)Fail-safe defaults
Section titled “Fail-safe defaults”- Unset / unknown client
environment→production. - Unset / unknown realm
max_environment_laxity→production.
A fresh realm caps everything at production until an operator deliberately widens it, and any malformed value resolves to the strictest profile. Misconfiguration fails closed — the relaxed posture is always opt-in and bounded.
Relaxation is operator-set, never self-asserted
Section titled “Relaxation is operator-set, never self-asserted”The relaxation direction is set only by an operator — seed/manifest, admin
API, or realm config. It is not accepted from POST /oauth/register (dynamic
client registration) or a CIMD metadata document. A client cannot declare itself
development to escape production gates, exactly as it cannot self-grant
max_agent_mode (ADR-007 §2).
The profiles
Section titled “The profiles”Security-relevant relaxations apply to development only. staging keeps
production-grade security and relaxes only operational conveniences (rate limits,
token lifespan), so promoting dev → staging surfaces the real security posture
before production.
Knob (EnvironmentPolicy field) | development | staging | production |
|---|---|---|---|
staticApiKeysAllowed | ✅ allowed | ❌ off | ❌ off |
localhostRedirectAllowed | ✅ | ❌ | ❌ |
pkceRequired | recommended | ✅ | ✅ |
accessTokenLifespanTier | long | short | short |
refreshRotationRequired | ❌ | ✅ | ✅ |
rateLimitTier | lenient | lenient | strict |
openDynamicRegistration | open | gated | gated |
agentStepUpEnforced | ❌ | ✅ | ✅ |
t3SecurityEnforced (style CSP on consent) | ❌ | ✅ | ✅ |
Notes:
t3SecurityEnforcedis narrower than its name suggests, and the row above is the whole of it. The flag has exactly one consumer: when it is false, the consent screen serves a relaxed style CSP (style-src 'self' 'unsafe-inline'). The rest of the T3 browser hardening is unconditional and never reads the environment profile —@fastify/helmetis registered globally (apps/auth-server/src/app/plugins/security-headers.ts), the/ui/loginand/ui/consentCSRF checks always run, and the session cookie’sSecureattribute is driven solely bySESSION_COOKIE_SECURE(apps/auth-server/src/app/helpers/session-cookie.ts). Marking a clientproductiondoes not turn any of those on, and marking itdevelopmentdoes not turn them off. See Browser Security.pkceRequiredhere governs whether the environment profile hard-requires PKCE. QAuth’s project-wide floor still defaultsoauth_clients.require_pkce=trueregardless, so PKCE is on unless a client is explicitly adevelopmentclient that opts out.stagingandproductionare https-only for redirect URIs. The RFC 8252http://loopback carve-out for native / CLI clients (including MCP clients) is handled by redirect validation and is permitted in any PKCE-enforcing environment — it is not the same flag aslocalhostRedirectAllowed.- Hard security floors always hold and are not environment-tunable: client
secrets are always hashed (Argon2id), and audience (
aud) binding always holds.
Static developer API keys
Section titled “Static developer API keys”Static, long-lived API keys are the deliberate developer-experience half of this feature — and they are environment-gated from day one:
development: a client may issue and authenticate with static API keys.staging/production: the API-key path is off; use OAuthclient_credentialsinstead.
Keys have the layout qauth_<keyId>_<secret> and are presented as a bearer
credential:
Authorization: Bearer qauth_<keyId>_<secret>Only keyHash / prefix (qauth_<keyId>) / last4 are ever persisted; the full
key is surfaced exactly once at creation. Manage them from the developer portal
(API keys section) or the /api/clients API. The gate is enforced centrally
via resolveEnvironmentPolicy(client, realm).staticApiKeysAllowed, so a key that
exists on a client later moved to production simply stops authenticating.
Concrete values behind the tiers
Section titled “Concrete values behind the tiers”The profile table is deliberately coarse — it carries legible tier labels, not magic numbers. The concrete values come from configuration so an operator tunes one place:
- Access-token lifespan —
shortmaps toACCESS_TOKEN_LIFESPAN(default900seconds);longis the development convenience. - Rate limits —
strictvslenientselect between configured per-window caps (RATE_LIMIT_MAX/RATE_LIMIT_WINDOWand the per-endpoint limits); only the cap moves with environment, the window is unchanged.
Recommended operator setup
Section titled “Recommended operator setup”- Production deployment: leave both columns at their
productiondefault, or explicitly pinrealms.max_environment_laxity = productionto lock the entire realm. Nothing else to configure on this dimension — but note the T3 browser hardening (security headers, CSRF, the cookieSecureflag) is not part of it: those are unconditional and configured separately, chiefly viaSESSION_COOKIE_SECUREand theSECURITY_HSTS_*variables. - Local development: set the developer’s realm
max_environment_laxitytodevelopment(orstaging) and mark specific clientsenvironment = developmentto opt into static API keys, localhost redirects, and long-lived tokens — without ever weakening the production realm. - Staging / load testing: use
stagingto keep full production security while relaxing only rate limits and token lifespans.
Related
Section titled “Related”- ADR-008: Environment-Aware Authorization Posture — rationale, fail-safe design, prior art.
- ADR-007: MCP-First Positioning — the operator-set
max_agent_modeprecedent this reuses. - Agent Authorization — agent step-up, which this posture enforces in
staging/production. - Browser Security — the T3 hardening bundle. This profile does not gate it: Helmet, the CSRF checks, and the session cookie’s
Secureflag are all unconditional.t3SecurityEnforcedrelaxes exactly one thing — the consent-screen style CSP.