Upgrade @auth0/nextjs-auth0 from v3 to v4 in the identity webapp#13159
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v4 SDK mounts the auth routes from Next.js middleware instead of a catch-all API route. The client keeps the same public URLs as v3 via the routes config, keeps every ID-token claim on the session (v4 strips non-default claims, but the site relies on the namespaced wellcomecollection.org ones), and replicates v3's callback redirect and relative logout returnTo behaviour, which v4 handles differently. Configuration moves from serverRuntimeConfig to process.env because the middleware bundle can't use next/config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catch-all auth route is gone: login, logout and callback are served by the middleware, with the callback error redirect and logout returnTo customisations moved to the client config. The /me endpoint becomes our own handler to keep the v3 contract UserContextProvider relies on (204 when signed out, ?refetch to re-sync the profile), and signup becomes a plain redirect since the login route forwards authorization params like screen_hint from the query string. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getSession takes only the request in v4, getAccessToken persists the refreshed session itself, and the Claims type is replaced by User. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The session/SDK configuration now lives in utils/auth0.ts (read from the environment), so remove it from serverRuntimeConfig where it would otherwise look editable but do nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Locally the app is reached from several origins - localhost:3003 directly, the nginx proxy at www-dev.wellcomecollection.org, and the content webapp's /account rewrite - but SITE_BASE_URL can only name one of them: the OAuth callback then lands on a different origin from the browser, where the transaction cookie isn't sent, and the callback fails with 'The state parameter is invalid'. Leaving v4's appBaseUrl unset in development makes the SDK resolve the origin from each request's forwarded headers instead. Production keeps the configured SITE_BASE_URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v4 defaults to one __txn_<state> cookie per login attempt, each lasting an hour. Abandoned attempts accumulate until requests exceed proxy header size limits (nginx responds 400 Request Header Or Cookie Too Large). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Absolutising the returnTo by constructing a modified copy of the request broke the SDK's route matching: a hand-built NextRequest loses nextUrl.basePath, so the logout route no longer matched and the request fell through to the page router's 404. Redirect back to the same route with the absolutised returnTo instead, deriving the origin from the request in development to stay on whichever host the browser is using. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The single-workspace 'yarn add' for the auth0 upgrade left the lockfile with a stale @prismicio/client entry that the root resolutions field collapses, and left node_modules missing packages (breaking the content webapp locally with "Can't resolve '@prismicio/react'"). A full root install fixes both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR upgrades @auth0/nextjs-auth0 in identity/webapp from v3 to v4, moving Auth0 route handling to Next.js middleware while preserving the existing public /account/api/auth/* URLs and the session/profile behaviours relied on across the Wellcome Collection webapps.
Changes:
- Upgrade to
@auth0/nextjs-auth0@^4.22.0and introduce middleware-based auth route handling (login/logout/callback). - Rewrite
identity/webapp/utils/auth0.tsaroundnew Auth0Client(...), reading configuration directly fromprocess.envfor edge compatibility. - Reintroduce the v3
/account/api/auth/mecontract (204 when signed out +?refetch) via a custom API route and add dedicated unit tests.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Updates dependency lockfile for @auth0/nextjs-auth0 v4 and transitive deps. |
| identity/webapp/package.json | Bumps @auth0/nextjs-auth0 dependency to v4. |
| identity/webapp/utils/auth0.ts | Replaces initAuth0() with Auth0Client config suitable for middleware/edge bundling; custom callback handling and session settings. |
| identity/webapp/middleware.ts | Adds Auth0 middleware integration and logout returnTo absolutisation. |
| identity/webapp/pages/api/auth/me.ts | Implements a v3-compatible /api/auth/me endpoint (204 + ?refetch). |
| identity/webapp/test/api-auth-me.test.ts | Adds unit tests for the new /api/auth/me behaviour. |
| identity/webapp/pages/api/auth/signup.ts | Changes signup to redirect to login with screen_hint=signup. |
| identity/webapp/pages/api/users/[[...users]].ts | Updates imports and token extraction for Auth0 v4 API surface. |
| identity/webapp/pages/index.tsx | Adjusts SSR session lookup for v4 getSession usage and email nullability. |
| identity/webapp/pages/validated.tsx | Removes redundant getSession call after forced token refresh. |
| identity/webapp/views/pages/index.tsx | Updates Auth0 user typing to v4 types. |
| identity/webapp/next.config.js | Ensures NEXT_PUBLIC_BASE_PATH is set for SDK URL construction under basePath. |
| identity/webapp/config.js | Removes Auth0/session config that can’t be read from next/config in middleware/edge. |
| identity/webapp/test/registration.test.ts | Updates next/config mock to match the reduced runtime config shape. |
| identity/webapp/pages/api/auth/[...auth0].ts | Removes the v3 catch-all API route handler (replaced by middleware). |
A BDD-style regression run sheet covering signup/registration, sign in/out, email verification, account management, item requesting and restricted access, for use after auth changes like the nextjs-auth0 v4 upgrade. Lives with the existing user stories in playwright/user-stories so the scenarios can graduate to Playwright tests later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
So it can be run after the production deployment as well as stage: the base URL and where email lands differ per environment (Mailtrap is stage-only), while the production-Sierra cleanup warning applies to both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v3 signup handler delegated to handleLogin, which honoured a returnTo query param; the redirect-based v4 replacement dropped any incoming params. Forward them all and force screen_hint=signup so callers that pass extra login params keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refetch tests replace global.fetch with a mock and never put the original back, which could leak into other suites sharing the environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I went through the manual test script and most things worked as described, save the following: Scenario 1.4"Then I am signed out and land on the application-received page showing the email address I signed up with" I remained signed in and was taken back to the work I clicked the sign in link from (i.e. the behaviour described in 2.3). The weird thing was it showed my initials as 'AA' at the top of the page. When I clicked on the my account link then it did log me out and land me on the application-received page. Scenario 2.1. and 2.2"...the header shows my name instead of Sign in" "... the menu shows my name" In 2.1 I see my initials and 'library account' and 'sign out' links in the dropdown I think these behaviours are correct though Scenario 3.2: unverified email banner (which I've suggested)Given my email address is not verified I got a "something went wrong message" and no email was sent (this was after I changed my email to get the banner to appear again) |
Move the email-verification scenario after first sign-in and pull the unverified-email banner up into section 1, so the account-page scenarios can be exercised without changing the test email. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Following up on the "Send a new verification email" failure (Scenario 3.2) from the manual test pass: I investigated and confirmed it's a pre-existing bug, not introduced by this v4 upgrade. It reproduces identically on Raised separately as #13185 — out of scope for this PR. |
The header shows the user's initials (not their full name), and the mobile menu shows account links rather than a name. Update scenarios 2.1/2.2 to match what's actually rendered, per review testing feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The namespaced patron claims (patron_barcode, patron_role) are added to the ID token by the add_custom_claims Auth0 Action only when the matching weco: scope is in event.transaction.requested_scopes. That is populated on interactive logins but not on refresh-token grants, so a refreshed ID token carries no patron claims. Because the v4 SDK rebuilds session.user from whichever ID token it last saw, the first token refresh (eg the /api/users proxy during an email change, or any expiry-driven refresh) overwrote session.user with a claim-less refreshed token. That made isFullAuth0Profile fail and put UserContextProvider into its failed state across every webapp, dropping the signed-in header and the library card number until the next full login. v3 stored the login claims once and never re-derived them, so it never surfaced. Fix it in beforeSessionSaved (the single hook that runs on every session save, login and refresh): stash the patron claims into a non-claim session field when a login provides them, and restore them onto session.user when a refresh has dropped them. The stash round-trips through the encrypted session cookie and is never exposed to clients. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@gestchild thanks for the thorough test pass. The missing account options turned out to be a real v4 bug, and a couple of the others are behaviour/wording clarifications. Here's what was going on. The broken header (and a lot of the odd registration behaviour in 1.4) came down to our namespaced patron claims (patron_barcode / patron_role) going missing from the session. Those claims get added to the ID token by the add_custom_claims Auth0 Action, but only when the matching weco: scope is in event.transaction.requested_scopes. That's present on an interactive login, but not on a refresh-token grant, so a refreshed ID token comes back without them. The v4 SDK rebuilds session.user from whatever ID token it last saw, so the first token refresh (the /api/users proxy when you change your email, or just an expiry-driven refresh) overwrites session.user with a claim-less token. Once that happens isFullAuth0Profile fails, UserContextProvider (which every webapp uses) drops into its failed state, and the signed-in header and library card number disappear until the next login. v3 didn't hit this because it kept the login claims and never re-derived them on refresh. I could reproduce it locally: a fresh login has the claims, a forced refresh drops them, and after an email change the session stayed broken, which matches what you saw. The fix (babd514) keeps the claims in beforeSessionSaved, the one hook that runs on every session save. It stashes them when a login provides them and restores them onto session.user when a refresh has dropped them. After the fix, a forced refresh and an email change both keep the claims, and the header and card number stay put. Unit tests added. On the initials in the header (2.1/2.2): that's intended, the header shows initials rather than the full name, so I've tweaked the scenario wording to match (44023a9). I think the header bug is fixed, but it's worth confirming on stage rather than locally. The full registration flow can't run locally (the post-signup Action redirects to www-stage), and stage exercises the claims fix against the real Action and the real registration and email journeys, including 1.4. Want to deploy this branch to stage and try running through the script again? |
gestchild
left a comment
There was a problem hiding this comment.
this looks good to go, we'll plan at FE catch up when it will be best to merge and test
What does this change?
Upgrades
@auth0/nextjs-auth0from 3.5.0 to 4.22.0 inidentity/webapp.v4 is a ground-up rewrite of the SDK. The biggest structural change is that the auth routes (login, logout and the OAuth callback) are now served by Next.js middleware rather than a catch-all API route. The client is configured with
new Auth0Client(...)inutils/auth0.ts, reading fromprocess.envbecause the middleware bundle cannot usenext/config.The upgrade is deliberately conservative, and nothing outside the identity webapp changes:
/account/api/auth/*), so links in the common and content webapps, and the callback/logout URLs registered with the Auth0 tenant, are untouched.beforeSessionSavedhook keeps every ID-token claim on the session. By default v4 strips non-standard claims, which would have dropped the namespacedpatron_barcodeandpatron_roleclaims that sign-in state across the site relies on.onCallbackreproduces v3's redirect behaviour: errors land on/account/error, and returnTo paths pointing outside the identity app (for example signing in from a works page) are not prefixed with/accountas the v4 default would have done./account/api/auth/meis now our own handler, preserving the contract thatUserContextProviderdepends on: a 204 when signed out (v4's profile route returns a 401) and the?refetchoption for re-syncing the profile, which v4 removed.screen_hint=signup, which v4 forwards to the authorization endpoint.enableParallelTransactions: false), as in v3. The v4 default sets one cookie per login attempt, and abandoned attempts accumulated until requests exceeded proxy header size limits.SITE_BASE_URL.The PR also adds a manual BDD regression script covering all the identity user flows:
playwright/user-stories/identity.md.How to test
Unit tests pass (
yarn test:identity, 112 tests), along with typecheck, eslint andnext build.All flows have been manually tested locally through the www-dev nginx proxy: signing in and out from the content app header, the account page, signup, the full registration flow, email validation, changing email (including the UserContext refetch), and item requests.
After the stage deployment, and again after the production deployment, run the manual regression script linked above. It covers signup and registration, signing in and out, email verification, account management, item requesting and restricted access. It is environment aware: each environment's base URL is listed, and email lands in Mailtrap on stage but in real inboxes in production. The registration flow particularly needs the stage run, because the local test had to detour via the stage registration form (the stage Auth0 Action always redirects to www-stage).
Note that both stage and production work against production Sierra, so a registration test creates a real patron record. Always finish the script with its account deletion feature so the cleanup request is raised with the library team.
Have we considered potential risks?
SESSION_KEYSentry is used, and rotating it now signs everyone out (sessions last at most 7 days).NEXT_PUBLIC_BASE_PATHis set bynext.config.js.Generated with Claude Code