Skip to content

feat(server-sdk): return the user from getSession on refresh, refresh the JWKS in the background - #2290

Merged
albertoelias-crossmint merged 4 commits into
mainfrom
albertoe/server-auth-session-user
Nov 6, 2026
Merged

albertoelias-crossmint merged 4 commits into
mainfrom
albertoe/server-auth-session-user

Conversation

@albertoelias-crossmint

@albertoelias-crossmint albertoelias-crossmint commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

feat(server-sdk): return the user from getSession on refresh, refresh the JWKS in the background

Problem

An app that needs the user on the server calls getSession and then getUser on each request. That is one more round trip (0.35–1.5 s from a cold host to staging) just to get the email, because the Crossmint Auth JWT carries only sub, aud, iss and exp.

When getSession refreshes, the refresh response already contains that user. The SDK dropped it.

Separately, the JWKS cache expired every 10 minutes. The next verification then waited for a new download, about 1 s from a cold host, because jose fetches over its own connection.

This is a correction to what we thought: a new CrossmintAuth.from() does not re-download the keys. They have been cached per URI across instances since #912; a new instance verifies in 8 ms.

What this PR does

  • getSession returns user (email, phone number, linked accounts) when it refreshed the session. verifyRefreshedAuthMaterial already checks that this user is the JWT's sub. AuthSession in common-sdk-auth gains the optional user. When the JWT was still valid, user stays undefined. We decided not to add identity claims to the JWT, so getUser is still needed in that case.
  • jose no longer expires the keys by age (cacheMaxAge: Infinity). A wrapper in jwksClient.ts tracks their age instead:
    • After 10 minutes it re-downloads them in the background and keeps verifying with the current keys.
    • While the endpoint fails, it retries at most every 30 s.
    • Keys older than an hour are not used: verification waits for a download, and fails with JwksUnavailableError if the download fails.
  • A token signed with an unknown kid still goes through jose's own blocking download, so key rotation behaves as before.

What to check when reviewing

Test plan

  • pnpm --filter @crossmint/server-sdk test:vitest: 79 passed, after merging main.
  • New tests fail on the old code:
    • The refreshed-session test, without the user pass-through.
    • Two background-refresh tests, against the old JWKS client: verification blocks on the download and then times out or fails.
  • The JWKS tests also cover:
    • keys under 10 minutes old: no download;
    • a failing endpoint: no more than one retry every 30 s;
    • after an hour, failure until the endpoint recovers;
    • an unknown kid: a blocking download.
  • tsc --noEmit and biome check on the server package are clean.
  • Not tested against the staging JWKS endpoint; the local server serves the same JWKS format.

🤖 Generated with Claude Code


Devin Review

AlbertoElias and others added 3 commits November 5, 2026 10:54
A server that needs the user's email called getUser after getSession: one
more round trip per request. When getSession has to refresh the session, the
refresh response already carries the user (and verifyRefreshedAuthMaterial
already checks it is the JWT's subject), so return it as `session.user`.

With a still-valid JWT there is no user to return: the JWT only carries
sub/aud/iss/exp. Adding email and phone_number claims to the JWT would cover
that case too; that is a backend change, proposed separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The remote key set expired after 10 minutes, and the next verification then
waited for a new download (a separate TLS connection, about 1 s from a cold
host). Keys now never expire inside jose; a wrapper re-downloads them in the
background once they are 10 minutes old and keeps verifying with the current
ones meanwhile. While the endpoint fails it retries at most every 30 s and
stops trusting the keys after an hour. Unknown key ids still trigger jose's
own blocking download, so rotation works as before.

Tests now run verifyCrossmintJwt against a real JWKS endpoint on localhost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/server/src/auth/CrossmintAuthServer.ts
@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5ccec04

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@crossmint/server-sdk Patch
@crossmint/common-sdk-auth Patch
@crossmint/auth-ssr-nextjs-demo Patch
crossmint-auth-node Patch
@crossmint/client-sdk-auth Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-native-ui Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/wallets-sdk Patch
@crossmint/wallets-playground-expo Patch
@crossmint/client-sdk-nextjs-starter Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/wallets-playground-react Patch
@crossmint/integration-tests Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Critical impact] The PR appears safe to merge; no blocking issues were found.

Reviews (2) · Last reviewed commit: "fix(server-sdk): count JWKS age from eve..." · Reviewed by Greptile

greptile-apps[bot]

This comment was marked as resolved.

jose re-downloads the key set by itself when a token names an unknown key id,
but the wrapper only timed its own downloads. An hour after its last one,
verification then failed during an endpoint outage although the keys were
minutes old. Read the age from jose's jwksCache stamp instead, which jose
updates after every successful download.

Tests: the age after a download triggered by a new key; a completed
background refresh replaces the keys without a second download. The
console.error spy is restored after each test.
@albertoelias-crossmint

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@albertoelias-crossmint
albertoelias-crossmint merged commit a2181ff into main Nov 6, 2026
10 checks passed
@albertoelias-crossmint
albertoelias-crossmint deleted the albertoe/server-auth-session-user branch November 6, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants