Skip to content

Upgrade @auth0/nextjs-auth0 from v3 to v4 in the identity webapp#13159

Merged
gestchild merged 18 commits into
mainfrom
upgrade-identity-nextjs-auth0-v4
Jul 1, 2026
Merged

Upgrade @auth0/nextjs-auth0 from v3 to v4 in the identity webapp#13159
gestchild merged 18 commits into
mainfrom
upgrade-identity-nextjs-auth0-v4

Conversation

@kenoir

@kenoir kenoir commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What does this change?

Upgrades @auth0/nextjs-auth0 from 3.5.0 to 4.22.0 in identity/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(...) in utils/auth0.ts, reading from process.env because the middleware bundle cannot use next/config.

The upgrade is deliberately conservative, and nothing outside the identity webapp changes:

  • The auth routes keep their existing public URLs (/account/api/auth/*), so links in the common and content webapps, and the callback/logout URLs registered with the Auth0 tenant, are untouched.
  • A beforeSessionSaved hook keeps every ID-token claim on the session. By default v4 strips non-standard claims, which would have dropped the namespaced patron_barcode and patron_role claims that sign-in state across the site relies on.
  • A custom onCallback reproduces 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 /account as the v4 default would have done.
  • /account/api/auth/me is now our own handler, preserving the contract that UserContextProvider depends on: a 204 when signed out (v4's profile route returns a 401) and the ?refetch option for re-syncing the profile, which v4 removed.
  • Signup redirects to the login route with screen_hint=signup, which v4 forwards to the authorization endpoint.
  • Logout requests with a relative returnTo are absolutised in the middleware to match v3, since v4 forwards them verbatim to Auth0.
  • A single transaction cookie is used (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.
  • In development the auth base URL is inferred from each request, so the app works on localhost, through the www-dev nginx proxy and via the content webapp rewrite. Production keeps the configured 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 and next 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?

  • Everyone is signed out once on deploy. v4's session cookie encryption is incompatible with v3, so existing sessions become unreadable and users will need to sign in again.
  • Session secret rotation behaves differently. v4 supports a single secret, so only the first SESSION_KEYS entry is used, and rotating it now signs everyone out (sessions last at most 7 days).
  • No Auth0 tenant or infrastructure changes are required. All public URLs and environment variable names are unchanged, and NEXT_PUBLIC_BASE_PATH is set by next.config.js.

Generated with Claude Code

kenoir and others added 10 commits June 11, 2026 09:02
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>
Copilot AI review requested due to automatic review settings June 11, 2026 10:33
@kenoir kenoir requested a review from a team as a code owner June 11, 2026 10:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.0 and introduce middleware-based auth route handling (login/logout/callback).
  • Rewrite identity/webapp/utils/auth0.ts around new Auth0Client(...), reading configuration directly from process.env for edge compatibility.
  • Reintroduce the v3 /account/api/auth/me contract (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).

Comment thread identity/webapp/utils/auth0.ts Outdated
Comment thread identity/webapp/pages/api/auth/signup.ts
Comment thread identity/webapp/test/api-auth-me.test.ts
kenoir and others added 4 commits June 11, 2026 11:51
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>
Comment thread playwright/user-stories/identity.md Outdated
Comment thread playwright/user-stories/identity.md Outdated
@gestchild

Copy link
Copy Markdown
Contributor

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
In 2.2 I see 'library account' and 'sign out' links at the bottom of the menu (no name)

I think these behaviours are correct though

Scenario 3.2: unverified email banner (which I've suggested)

Given my email address is not verified
When I view my account page
Then I see a banner asking me to verify my email
And when I choose "Send a new verification email" I see a confirmation message and a new email arrives in the test inbox

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)

@rcantin-w rcantin-w moved this from Backlog to Ready for review in Digital experience Jun 18, 2026
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>
@kenoir

kenoir commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

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 main (v3 SDK 3.8.0) and on this branch: the empty-body POST /account/api/users/me/send-verification-email is serialised to invalid JSON by the shared FetchClient and rejected by the Identity API with a 500, which the client collapses into the generic "something went wrong". The FetchClient (commit 8e96225) is already on main, and this PR doesn't touch the hook, api-client.ts, fetch-helpers.ts, or the proxy's body-forwarding.

Raised separately as #13185out 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>
@kenoir

kenoir commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@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 gestchild left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks good to go, we'll plan at FE catch up when it will be best to merge and test

@gestchild gestchild merged commit 017fb66 into main Jul 1, 2026
11 checks passed
@gestchild gestchild deleted the upgrade-identity-nextjs-auth0-v4 branch July 1, 2026 09:44
gestchild added a commit that referenced this pull request Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready for review

Development

Successfully merging this pull request may close these issues.

4 participants