What a deployment must do before real clients rely on it. Each item
links to the part of GETTING_STARTED.md that
explains it. New refuses many of these mistakes at startup under
production assurance, but not all of them. The items marked (yours)
are things no constructor can check.
-
Config.Assurance = server.AssuranceProduction, withConfig.Deploymentset.AssuranceDevelopmentis not FAPI 2.0 conformant, though its metadata looks the same. Step 2 - Durable stores that declare
storage.StoreAssuranceand pass the contract tests in your own test suite, since the declaration isn't verified:storage.TestClientRepositoryContract,TestTransactionStoreContract,TestGrantStoreContract,TestReplayStoreContract,TestRevocationStoreContract, plusTestAccessTokenStoreContract(opaque access tokens),TestNonceStoreContract(DPoP nonces) andTestBackchannelAuthenticationStoreContract(CIBA). WithDeploymentHorizontallyScaled, every instance must share them. Thesqlstoremodule provides the client repository, transaction, grant, replay, nonce, revocation, opaque access token, CIBA and pairwise subject stores on PostgreSQL (any deployment) or SQLite (DeploymentSingleInstanceonly), already passing those contracts: see Storage withsqlstore. Step 1 - Durable signing keys that declare
keys.KeyCustodyAssurance(keys.NewKeyManagerFromSignerswithkeys.DeclareCustody), and a plan for rotating them. - Client keys from
keys.NewStaticClientKeysorkeys.NewClientJWKSSource. Akeys.ClientKeySourceof your own must passkeys.TestClientKeySourceContract. Step 1 - An audit sink (
Dependencies.Audit),server.SystemClock{}andcrypto/rand.Reader. Step 4 - A revocation choice: a revocation store the resource servers
share, or
server.NoRevocation{}with short access token lifetimes, knowing that code reuse andRevokeGrantthen revoke nothing. Step 2 - A DPoP nonce choice: a nonce store every instance shares
(
Dependencies.Nonces, withLimits.DPoPNonceLifetimeof at most 15 minutes), so a DPoP proof generated ahead of time or captured is refused, at the cost of one extra round trip per client on first use; orserver.NoDPoPNonces{}to decline, as an mTLS-only deployment can. Step 4 - A client certificate trust choice:
NoPKIClientAuthentication{}unless a client usestls_client_auth; otherwiseTrustedClientCAswith revocation checking, orClientCertificateChainVerifiedUpstream{}only when your TLS termination verifies every chain. Step 4 - (yours) Authenticate before redirecting. Before a user has
authenticated, end with
DenyLocally, neverDenyorAuthenticationFailed. Step 5 - (yours) Pass the real authentication time to
server.NewAuthenticationContext: when the user actually authenticated, nottime.Now()when you reuse a session, somax_ageandprompt=loginhold. Step 5 - (yours) Bind the interaction to the browser with
server/interactioncookie(its keys shared by every instance) or the equivalent, and protect the consent form against CSRF. Step 5 - (yours) Wrap
/authorizeand every login, consent and error page inserver.AuthorizationPageHeaders(anti-framing,Referrer-Policy: no-referrer,no-store). Load no third-party scripts or images there, and don't log full/authorizeURLs. Redirect with 303 after a form POST, and render errors withWriteText/WriteJSON, neverError(). Step 5 - (yours) The HTTP layer: TLS 1.2 or later with BCP 195 cipher suites (at the proxy, if one terminates TLS), HSTS on browser-facing endpoints, and no CORS on the authorization endpoint. Step 6
- (yours) Client certificates behind a proxy: read a forwarded
certificate with
fapihttp.ForwardedClientCertificate(RFC 9440Client-Cert), passing atrustedcheck such asfapihttp.RemoteAddrInthat accepts only your own proxy, and have the proxy strip any copy of the header a client sent. Step 7 - Subjects: decide between public and pairwise subjects before
clients store any
sub. Pairwise subjects
-
resource.AssuranceProductionwithDeploymentset, and stores that pass the same contract tests as above. Build aConfig -
Config.ResourceURLsset to this API's own external URLs; behind a proxy that rewrites paths, setVerifyRequest.URL. Verify a request - The access token format matches the authorization server's.
JWT:
MaxTokenLifetimeat least the server'sAccessTokenLifetime, andAudienceset. Opaque: the samestorage.AccessTokenStorethe authorization server writes to, since there is no introspection endpoint. WireDependencies - The same revocation store as the authorization server, or
resource.NoRevocationUntilExpiry{MaxTokenLifetime: ...}(at most 15 minutes) to decline checking it; production refusesNoRevocation{}. WireDependencies - A DPoP nonce choice: a nonce store of the resource server's
own (never the authorization server's; with
sqlstore,Nonces()from a DB with aTablePrefixof its own), shared by its instances, withRandomandLimits.DPoPNonceLifetime; orresource.NoDPoPNonces{}to decline.serverresource.NewVerifiertakes it asOptions.Nonces. WireDependencies - (yours) Authorize anything a user owns by
AuthorizationContext.EndUser(), never byInternalSubjectalone (a client credentials token's subject is a client), and useClientSubjectfor anything returned to a client. Pairwise subjects at a resource server - (yours)
PeerCertificatebehind a proxy: the same rule as the authorization server's, withfapihttp.ForwardedClientCertificate. Verify a request
-
client.AssuranceProduction, withDependencies.HTTPfromfapihttp.NewClientand any client certificate or private roots given when it's built. Step 10 - A durable session store that passes
storage.TestSessionStoreContract(sqlstore'sSessions()does; for a native app withCallbackBindingDeviceLocalStore, on SQLite), and durable keys declaringkeys.KeyCustodyAssurance. - (yours) Bind each authorization to the browser that began it
(
client/sessioncookie), against login CSRF. Step 10 - (yours) Serve the redirect URI with
Referrer-Policy: no-referrer, and no third-party content, so the authorization code doesn't leak (RFC 9700 §4.2.4). -
RequireAuthorizationResponseIss, andMaxIDTokenLifetimeset to your provider's ID token lifetime if you know it. - (yours) Seal stored tokens with
client.TokenSetSealerif you keep them past the request. Step 10
For a deployment keeping its stores in the
sqlstore module.
Durable storage
- The right database for the deployment: PostgreSQL for
DeploymentHorizontallyScaled; SQLite (WAL mode, a busy timeout) only forDeploymentSingleInstance, on the one host that serves. - The schema migrated before serving:
Migrateat startup as a role that may create tables, or from a deployment step, withCheckSchemaat startup to fail fast when it's behind. -
Sweepon a schedule (every few minutes) with the server's time, or the tables grow without bound. A CIBA request is keptBackchannelExpiredRetention(an hour) past its expiry, so a late poll is toldexpired_token. - One
TablePrefixper role and tenant: the authorization server and the resource servers reading its opaque access tokens or revocations share the server's prefix (andSealer); a resource server's DPoP nonces get a prefix of their own; servers with different issuers never share one. - (yours) A least-privilege database role for each resource
server:
SELECTon the server'saccess_tokens,revocationsandschema_migrations,SELECT/INSERTonreplay, and its own prefix's tables; only the authorization server sweeps the tables they share. - (yours) A sealing key kept outside the database and its
backups (
Options.Sealer,AESGCMSealer), with a rotation plan:RotatingSealer, which opens with the old and new keys until the old one's records expire, well before a key's 2^32 seals (weeks, on a busy server). -
ReadUnsealedonly while migrating an existing database to sealing: deploy with it, runSealRecords, then deploy without it. - (yours) Write access to the database as tight as read access: sealing keeps records confidential and bound to their row, but anyone who can write can still delete rows or change lookup columns.