feat(auth0-express): migration stores for express-openid-connect migration#7
Open
frederikprijck wants to merge 5 commits into
Open
feat(auth0-express): migration stores for express-openid-connect migration#7frederikprijck wants to merge 5 commits into
frederikprijck wants to merge 5 commits into
Conversation
nandan-bhat
reviewed
Jun 19, 2026
e620ec7 to
b54d666
Compare
Add zero-downtime session migration from express-openid-connect via a `legacyCompatibility` option on createAuth0. When enabled, the SDK reads existing express-openid-connect sessions (stateless cookie or stateful server-side store) and upgrades them to the auth0-server-js format on the next write, so users are not forced to re-authenticate. - MigrationStatelessStateStore: decrypts legacy A256GCM (HKDF, "JWE CEK") cookies, enforces the header-level exp, and transforms them into StateData. - MigrationStatefulStateStore: resolves a legacy plain or JWS-signed store key (routing modern JWE cookies to the base decrypt by shape), upgrades the session in place on write, and supports requireSignedLegacyCookie to mirror express-openid-connect's requireSignedSessionStoreCookie. - Shared HKDF derivation and legacy-session transformer; `legacyAudience` must match the requested audience for carried-over access tokens. - Wire it through getStateStore and expose the option via the /migration subpath export. Includes unit tests covering tamper rejection, expiry (missing/non-numeric exp), JWS verification against real express-openid-connect fixtures, and key/secret rotation.
Add examples/migration-express-openid-connect with before/ (an express-openid-connect app), after/ (the same app on @auth0/auth0-express with legacyCompatibility enabled), and shared/ Redis infra. Demonstrates zero-downtime migration for stateless and stateful sessions, access-token carryover by audience, and backchannel logout, with a verification runbook. Register the example folders in the workspace globs.
Add a "Zero-Downtime Session Migration" section to MIGRATION.md covering the legacyCompatibility option (enabled/legacySecret/legacyAudience/legacyScope/ requireSignedLegacyCookie), stateless vs. stateful behavior, and the legacyAudience-must-match-audience rule. Also correct stale option references (sessionConfiguration, inactivityDuration in seconds, customFetch) and repoint the README license link.
b54d666 to
a9b7e1f
Compare
The migration example apps rendered the raw access token into an HTML page and the runbook told reviewers to eyeball it. An access token is a bearer secret and does not belong in a page, and a long opaque string is a weak signal that anything migrated. Show non-secret session facts instead: user sub, per-token-set audience / scope / expiry, and whether a refresh + id token are present. The before/ and after/ apps now render the same facts so the migration is verified by comparing them side by side (same sub, same audience/scope, refresh token still present). Reframe /refresh-token as the strongest proof — exchanging the carried-over refresh token succeeding demonstrates the migrated token set is intact — and report the new set's metadata, never the token value.
Both migration example apps spelled out connection config that the frameworks already read from the environment: express-openid-connect reads ISSUER_BASE_URL / BASE_URL / CLIENT_ID / CLIENT_SECRET / SECRET, and createAuth0 reads AUTH0_* / APP_BASE_URL. Drop those explicit lines so each config block shows only what is specific to the migration (legacyCompatibility, the appSession cookie name, the session store, and eoc's authRequired: false / authorizationParams). Replace the hand-rolled requireSession middleware in the after/ app with the SDK's exported requiresAuth() — the direct counterpart to eoc's requiresAuth() — which also returns 401 for JSON clients instead of only redirecting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds zero-downtime session migration from
express-openid-connectto@auth0/auth0-express. When enabled via alegacyCompatibilityoption oncreateAuth0, the SDK transparently reads existingexpress-openid-connectsessions and upgrades them to theauth0-server-jsformat on the next write, so users are not forced to re-authenticate.express-openid-connectJWE cookies (dir/A256GCM, HKDF'JWE CEK'), enforces the header-levelexp, and converts them toStateData. The next write re-encrypts in the modern format.express-openid-connectsession envelopes ({ header, data, cookie }) from Redis/Mongo/etc., resolving a plain or JWS-signed store-key cookie, and upgrades the session in place on the next write. Backchannel logout works for migrated sessions once re-written.legacyCompatibilityconfig; the stores live behind the@auth0/auth0-express/migrationsubpath (not the main entrypoint).Key details
requireSignedLegacyCookie: mirrorsexpress-openid-connect'srequireSignedSessionStoreCookie. Whentrue, an unsigned or bad-signature store cookie resolves to no session instead of the raw value. Defaultfalse, matching eoc. Stateful-only.expis missing/non-numeric or in the past, mirroring eoc'sassert(exp > epoch()).legacyAudience/legacyScope: stamped onto the single migrated token set.legacyAudiencemust equal theaudienceyour app requests, sincegetAccessTokenlooks tokens up by audience.legacySecretaccepts a string array; secrets are tried in order.JWEDecryptionFailed/JWEInvalid; programming errors propagate.deriveHkdfKey(secret, info)inexpress-oidc-hkdf.ts, used by both stores.How to test
Automated
Unit tests cover tamper rejection, expiry (missing / non-numeric / past
exp), cookie-shape routing, in-place upgrade on write, secret rotation,requireSignedLegacyCookie, and — importantly — fixtures produced by the realexpress-openid-connectlibrary (signing a stateful cookie with eoc's ownsignCookie, and encrypting a stateless cookie with eoc'sencryption()CEK derivation) to catch any drift in HKDF salt/info or the JWS header.Manual (end-to-end migration)
Full runbook:
examples/migration-express-openid-connect/README.md. In short:Prerequisites —
npm install && npm run build; a test Auth0 Regular Web App with callback URLshttp://localhost:3000/callback(before) andhttp://localhost:3000/auth/callback(after); copy each app's.env.exampleto.envusing the same session secret in both.The example apps render a Session facts panel (user
sub, token audience/scope/expiry, refresh + id token present) rather than the access token itself — an access token is a bearer secret and does not belong in a page. You verify the migration by comparing those facts before vs after (samesub, same audience/scope, refresh token still present).Scenario 1 — Stateless (cookie):
npm start --workspace examples/migration-express-openid-connect/before— log in athttp://localhost:3000, confirm theappSessioncookie and note the Session facts panel.npm start --workspace examples/migration-express-openid-connect/after./privateaccessible without re-login.AUTH0_SECOND_AUDIENCEset, hit/refresh-token— it exchanges the carried-over refresh token for a fresh token set (succeeding proves the migrated refresh token is intact) and reports the new set's metadata, not the token.Scenario 2 — Stateful (Redis) + backchannel logout:
docker compose -f examples/migration-express-openid-connect/shared/docker-compose.yml up -d; setREDIS_URL=redis://localhost:6379in both.envs.before/, log in, confirm a session key in Redis. Stop it, startafter/, reload → still logged in.StateData+logout:sid:<sid>index is written.POST /auth/backchannel-logoutwith a reallogout_tokenfrom your tenant →204; confirm the session key and itssidindex are removed from Redis.Notes
sidindex yet, so it is not deletable bydeleteByLogoutTokenuntil the first write — expected and documented in the design.main.