Deploy your application
Use this guide for the application you are deploying. If Udibo hosts sign-in, you operate the client, BFF, sessions, and API. If your app hosts its own authorization server, you also operate its credential, code, token, and signing-key storage. The package provides protocol handling; your deployment supplies persistent storage, secrets, delivery, and application policy.
Choose the integration boundary
| Responsibility | App using Udibo | App hosting authorization |
|---|---|---|
| Callback and application sessions | Your app | Your app |
| API token validation and record-level access | Your app | Your app |
| Login, password reset, MFA, and email delivery | Hosted sign-in flow | Your app's configured identity flows |
| Registered clients, authorization codes, token issuance | Identity service | Your authorization server |
| Token signing keys | Identity service | Your authorization server, if issuing JWTs |
Udibo's hosted-service setup is described in use Udibo. The rest of this guide documents package integration, not hosted-service administration.
The multi-instance rule
State needed by two requests must be available to either request's process. A local example can keep it in memory; a deployment with replicas, serverless isolates, or restarts needs shared persistence or an appropriate encrypted cookie.
| State | Public contract | Requirement |
|---|---|---|
| BFF session | SessionStore | Enforce expiry and revocation; stateful update must never recreate a missing session |
| Pending browser login | AuthRequestStorage / AuthRequestStorageFactory | Preserve state and PKCE verifier across redirects; bind completion to the initiating browser; atomic single-use take of a shared record |
| Authorization codes | AuthorizationCodeServiceInterface | Atomic single-use consumption, expiry, client and redirect binding |
| Tokens | TokenServiceInterface | Protect stored token material; atomic revocation/rotation claims |
| App credentials | IdentityUserStore | Persist the complete credential, including params; unique identifiers |
| Reset and verification links | TokenFlowStore | Expiring, single-use records; implement invalidation capabilities your app needs |
| Email codes | OtpStore | Atomic attempt increments; boolean consume that retires the pair's older codes; protect low-entropy code hashes |
| MFA | MfaStore | Protect TOTP secrets; atomically consume recovery codes |
| Rate limits and lockout | RateLimitStore / LockoutStore | Shared counters and expiry across instances |
Run the appropriate contract suite from @udibo/oauth2/testing/contract or
@udibo/oauth2/hono/bff/testing against your actual adapter. Test concurrent
callers against the database implementation, not just an in-memory substitute.
Configuration inventory
Keep public app origin, issuer, registered callback URLs, client credentials, requested scopes, session lifetimes, and storage configuration in deployment configuration. Validate required values at startup. Do not silently fall back to demo secrets or localhost URLs in production.
For an app-owned issuer, construct resolve with the configured services and
issuer. Do not derive the issuer or emailed links from an unvalidated Host
header.
Secrets
Store client secrets, cookie encryption keys, signing keys, and mail credentials in your deployment's secret store. Use different values for each environment. Keep token responses, cookies, authorization codes, PKCE verifiers, reset links, and TOTP/recovery secrets out of logs and exception telemetry.
Persist signing and encryption keys across deployments. Generating replacements at startup breaks existing sessions or token verification on every restart.
Signing keys and rotation
This section applies only when your app issues OIDC or JWT tokens. Generate a
signing key with npx udibo-oauth2 oidc keygen, then store its
output as a secret. The package's issuer signs with ES256; verify that your
consumers accept that algorithm.
Use RotatingSigningKeyProvider to publish the previous public key while
signing new tokens with a new key:
import {
importSigningKeyJwk,
RotatingSigningKeyProvider,
} from "@udibo/oauth2/server/authorization";
declare const currentJwk: JsonWebKey;
declare const previousJwk: JsonWebKey;
const signingKeys = new RotatingSigningKeyProvider({
current: await importSigningKeyJwk(currentJwk),
previous: [await importSigningKeyJwk(previousJwk)],
});For routine rotation, keep the previous public key available through the longest outstanding token lifetime and relevant consumer cache windows. Compromise response may require retiring a key sooner and forcing reauthentication. Offline JWT validation cannot provide immediate per-token revocation.
Stores you provision
Choose the stores required by your enabled flows. A client using Udibo does not
need local password or authorization-code tables. An app hosting password login
needs credential storage and session revocation; email codes additionally need
an OtpStore, while reset links and magic links use TokenFlowStore.
Automatic password rehash or import upgrades require
IdentityUserStore.replaceCredential. It must atomically compare all old
credential fields before writing the replacement. A failed comparison writes
nothing and the password is re-verified against the credential now stored;
omitting the capability skips automatic upgrades. Explicit password resets still
use setCredential.
For rotating refresh tokens, implement rotate so the claim of the old
credential and the save of its successor are one atomic step, and serialize it
with revokeFamily, for example with a lock per family taken first in both;
runTokenRotationContractTests, with your store's barrier wired, checks both.
Keep replay tombstones if you enable reuse detection. Without rotate, the
library's calls are separate storage operations: revokeRotated (or the
fallback revoke) must return true only to the caller that consumed the old
credential, your adapter must account for competing writes and failures, and
revokeFamily must record the revoked family so a racing save cannot restore
it.
For authorization codes, implement exchangeCode so the claim of the code and
the save of its token are one atomic step, persist codeVerifierHash with
the token, and have revokeCode honor the client and the codeVerifierHash it
is given; runCodeExchangeContractTests and
runTokenServiceContractTests check them. Without exchangeCode, a code
replayed between the claim and the save leaves the token saved next live.
Sequential OTP resends invalidate prior codes. Concurrent requests use separate
invalidate/create calls, so that alone does not guarantee one outstanding code.
Serialize issuance for each (email, purpose) across instances when your
application requires that policy. Atomic consume separately ensures a given
code succeeds once.
Cookies and sessions
The BFF defaults to Secure, HttpOnly, host-bound cookies and enables CSRF checks. Keep those settings on HTTPS deployments. The pending authorization cookie needs to survive the authorization server's redirect back to your app; use the provided pending-login cookie factory or a correctly scoped app-owned store.
Choosing the BFF's stores
HonoBff requires both sessionStore and authRequestStorage; it has no
default, so a deployment cannot reach production on a store that only works in
one process. Pick each one by where your requests can land:
| Deployment | sessionStore | authRequestStorage |
|---|---|---|
| One long-lived process, tests, local development | MemorySessionStore | MemoryAuthRequestStorageFactory |
| Several instances or serverless isolates, no shared database | EncryptedCookieSessionStore; against an authorization server that rotates refresh tokens it needs sticky routing, since only the instance that ran a refresh answers a pre-refresh cookie with the new session | EncryptedCookieAuthRequestStorage |
| Several instances that need revocation, listings or back-channel logout | A SessionStore over a database or cache every instance shares, implementing withRefreshLock | EncryptedCookieAuthRequestStorage, or an AuthRequestStorageFactory over shared storage |
Give each encrypted-cookie store a secret every instance shares, so any of them
can unseal a cookie another sealed. Use a separate secret per store instance
(each store you construct) and per app: nothing in a sealed cookie binds it to
the app or the store instance that sealed it, so two apps that share a secret
can open each other's session cookies. One secret given to an app's session
store and its pending-sign-in storage does not let either open the other's
cookies, because each derives its own key from the secret with HKDF-SHA-256
under a label of its own. The memory stores keep their records in the process
that wrote them: a restart signs everyone out, a session is unknown to every
other instance, and a callback served by another instance fails with an unknown
state. An AuthRequestStorage that needs no request context, such as one over
your database, is passed as { forRequest: () => storage }; the BFF never falls
back to the DirectClient's own authRequestStorage. Such a
store must return every field of the record it was given, with its type,
maxAge: null included. The login records the max_age the sign-in was started with, and a
store that keeps only the fields it knows makes the callback check against its
own options instead. Check your own stores with runSessionStoreContractTests
from @udibo/oauth2/hono/bff/testing and runAuthRequestStorageContractTests
from @udibo/oauth2/testing/contract.
Prefer a stateful SessionStore when your app needs immediate revocation,
session listings, or backchannel logout. EncryptedCookieSessionStore is an
alternative with a different contract: a copied cookie remains usable until
expiry, even after logout clears the original browser's cookie. It also has
browser cookie-size limits. See
session limitations.
A shared store should also implement withRefreshLock (RefreshLockStore), a
lock that lets one instance at a time refresh a session. Without it, parallel
requests for one session that reach different instances near access-token
expiry each spend the refresh token, and against an authorization server that
rotates refresh tokens the user is signed out. EncryptedCookieSessionStore
cannot hold such a lock, so with a rotating authorization server a request
still carrying the cookie from before a refresh replays the old token, unless
it reaches the instance that ran the refresh within rotatedCookieGrace. See
session limitations for
both cases. runSessionStoreContractTests checks the lock when a store
implements it.
The lock has to expire when its holder dies, so give each withRefreshLock
call a RefreshLease and renew it while the refresh runs. HonoBff writes and
destroys the session only through the lease, which keeps a holder that stalled
past the expiry from overwriting or ending a session another instance has
since refreshed. Keep the lease on the session record itself, so its
conditional write is one compare-and-set, and never hold a database
transaction open across the refresh: the refresh waits on the authorization
server. runSessionStoreContractTests fails a lock store that passes no lease.
Set its refreshLockNeverExpires: true option only for a lock no holder can
lose while its refresh runs, such as one held in the single process that runs
every refresh; a holder that hangs then holds that session's refresh until it
settles.
Keep the session lifetime consistent between the store and
HonoBff.sessionMaxAgeMs. A stateful store's update must reject revoked,
expired, or missing records; a refresh finishing after logout must not insert a
session again.
If you expose a session list, return a separate, non-secret record ID as
SessionSummary.id. The BFF's cookieValue is a credential and must never
appear in listings, URLs, analytics, or browser-readable session summaries.
TLS and the reverse proxy
Serve deployed callbacks, token endpoints, and APIs over HTTPS. Configure your proxy to preserve the external origin in a trusted way and forward the request headers required by your CSRF policy. Do not trust arbitrary client-supplied forwarded headers.
Generic OIDC connectors rely on direct TLS token exchange for ID-token integrity. They validate claims but do not verify signatures. Configure trusted issuers, retain endpoint HTTPS checks, and never use these connectors to accept ID tokens obtained through unrelated channels. Apple uses separate signature verification.
CSRF, CORS, and security headers
Keep the BFF and browser on the same origin where possible. BffClient sends
the required CSRF header. Handwritten requests must follow the same contract. A
React route guard changes rendering; it does not authorize an API request.
honoIdentityRoutes includes a same-origin guard on unsafe requests. Custom
login, social-linking, MFA, session-revocation, and password-change routes must
apply their own authentication and CSRF policy. Bind sensitive actions to the
current authenticated user rather than trusting a submitted user ID.
Configure security headers for your app's content and hosting environment. Do not log or place reset tokens in third-party analytics URLs. Use a restrictive referrer policy on pages that receive credentials in URLs.
Redirect-URI policy in production
Register exact HTTPS callbacks for web applications. Keep local development registrations separate. Native loopback IP redirects have protocol-specific port matching; do not treat that exception as a general wildcard.
If your own server accepts wildcard registrations, supply isPublicSuffix from
@udibo/oauth2/server/public-suffix and constrain patterns to a hostname
namespace your application controls. Shared hosting domains are not an ownership
boundary. Exact callbacks avoid this additional policy surface.
Rate limiting, lockout, and password policy
IdentityService always applies password policy, but rate limiting, account
lockout, and CAPTCHA are opt-in integrations. Construct the protections you
need; protectionMode: "log-only" records decisions without enforcing them.
Default deployed configuration should enforce. Wire bot challenges in an
app-owned route using the
CAPTCHA example; IdentityService
does not apply them implicitly.
RateLimiter and AccountLockout require a store; neither has a default.
Give each a store over Redis or a database every instance shares, with an
atomic increment, so a limit or a lock applies however requests are
balanced. MemoryRateLimitStore and MemoryLockoutStore are bounded but
per-process: with more than one instance each grants its own attempts, so keep
them to a single process, tests and local development.
Apply IP/request throttling at the route layer as well as identifier-based limits within identity flows. Rate-limit mail requests and code verification. A six-digit code has only a million possibilities: storing its hash does not make a leaked code database resistant to offline guessing.
For password reset, implement TokenFlowStore.deleteBySubject to invalidate
outstanding sign-in links. Reset also attempts OTP invalidation when the reset
token contains the email. These cleanup operations are best-effort; decide how
your app responds to failures and monitor them. See
known limitations.
Backups and restore
Back up persistent credentials and MFA records with appropriate access controls. Keep encryption keys separately protected and rehearse restoration. A restore can reintroduce sessions or tokens that were revoked after the backup; include reauthentication or explicit revocation in the recovery procedure.
Apply retention to expired codes, tokens, sessions, and audit events. Retain rotation tombstones long enough for your chosen replay-detection policy.
Observability, audit, and health checks
Record request IDs, flow outcomes, authenticated client IDs, and stable error codes. Avoid logging raw request bodies on credential endpoints. Treat audit callbacks as observation: a callback whose errors are isolated is not an enforcement gate. Use the extension reference to check when a callback runs and how failures propagate.
Monitor failed callbacks, refresh rejection, storage errors, email-delivery failures, and token-validation timeouts. Test recovery from an unavailable issuer or database without relabeling those failures as bad credentials.
Before you go live
Complete the deployment checklist against your chosen integration. Test in the actual proxy, cookie, and storage environment; a local example passing does not validate production configuration.

