Skip to content

Move to stateful session auth - #757

Merged
dowski merged 1 commit into
rebootfrom
dowski/sid-in-jwt
Oct 26, 2026
Merged

dowski merged 1 commit into
rebootfrom
dowski/sid-in-jwt

Conversation

@dowski

@dowski dowski commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Prior to this change, a non-expired JWT was all that was needed to run functions as a user. Now there needs to be a live session associated with the JWT.

To make this happen, the JWT now carries the session ID in the sub claim, not the user ID. In order for functions to access the user ID, it has to be looked up from a live session.

The getAuthUserId function now takes an optional reference to the core auth component. It works out of the box by default, but allows passing a custom reference if an app renames it.

Most of the other changes are to examples and tests.

@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
auth-v2 Ready Ready Preview Oct 26, 2026 11:45pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
convex-auth-docs Ignored Ignored Oct 26, 2026 11:45pm UTC

Request Review

dowski commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

Re-trigger cubic

@dowski
dowski requested a review from Nicolapps October 20, 2026 16:26
Comment thread packages/core/src/component/crypto.ts Outdated
Comment thread packages/core/src/component/crypto.ts Outdated
const token = await new SignJWT()
const token = await new SignJWT({ sid: opts.sessionId })
.setProtectedHeader({ alg: ALG, kid: opts.kid, typ: "JWT" })
.setSubject(opts.subject)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we consider removing the sub claim here?

Since we're going to use sessions, people should always convert the session ID to a user ID through a DB query. Including a sub field incentivizes people to use the user ID directly, which would fail open on revocation

If developers want to authenticate to another system, they should take the responsibility of issuing their own JWTs (potentially through the new syscall)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

After live and chat discussion, the session ID is now getting stored in the sub.

Comment thread packages/core/src/component/crypto.ts Outdated
const nowSeconds = Math.floor(Date.now() / 1000);
const expSeconds = nowSeconds + opts.expiresInSeconds;
const token = await new SignJWT()
const token = await new SignJWT({ sid: opts.sessionId })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sid seems to be a field defined by the OIDC spec, not by the “pure” JWT spec. But it seems fine to use it still, even though we aren’t implementing OIDC.

I considered the alternatives and nothing looks better. Using sub to store the session ID sounds more misleading and potentially spec-breaking if we wanted to implement OIDC later. And using a custom claim name isn’t really helpful either.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

If we implement OIDC later, that would be more of an outward facing system and we'd issue different tokens.

@dowski
dowski changed the base branch from reboot to graphite-base/757 October 21, 2026 14:16
@dowski
dowski force-pushed the dowski/sid-in-jwt branch from 850b78d to 78fe425 Compare October 21, 2026 14:16
@dowski
dowski force-pushed the graphite-base/757 branch from 56367bb to 1ac66fc Compare October 21, 2026 14:16
@dowski
dowski changed the base branch from graphite-base/757 to dowski/remove-session-refresh-timestamp October 21, 2026 14:16
@dowski
dowski force-pushed the dowski/sid-in-jwt branch from 78fe425 to dfc4870 Compare October 21, 2026 14:23
@dowski dowski changed the title Store the session ID in a sid claim in the JWT Issued JWTs now carry the session ID in the sub claim Oct 21, 2026
@dowski dowski changed the title Issued JWTs now carry the session ID in the sub claim Move to stateful session auth Oct 21, 2026
@dowski
dowski requested a review from Nicolapps October 21, 2026 14:50

dowski commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the initial review, @Nicolapps ! Requesting another pass since this has changed in structure based on discussions yesterday.

@dowski
dowski force-pushed the dowski/remove-session-refresh-timestamp branch from 1ac66fc to 47f463e Compare October 21, 2026 19:50
@dowski
dowski force-pushed the dowski/sid-in-jwt branch from dfc4870 to a8fadc6 Compare October 21, 2026 19:50
@pkg-pr-new

pkg-pr-new Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
npm i https://pkg.pr.new/@convex-dev/auth@757

commit: 242704a

Comment thread packages/core/src/component/public.ts Outdated
* A session is live exactly as long as its row exists: sign-out, refresh-token
* reuse detection, and expired-token cleanup all delete the row.
*
* This is a component-internal function (callable by the app, not by end-user

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that “component-internal” here is misleading, it sounds like it’s an internal function defined in the component, or that it’s not meant to be called by the app. We probably need a better term (maybe something like “This is a function exposed by the component to the app, it’s not exposed to the end user”?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, touched up the comment (and a similar one above it).

The access token's `sub` claim is now the session id, not the user id. The
user is whoever the session belongs to, for as long as the session exists,
so a revoked session stops resolving to a user before its access token
expires.

- The core component gains `getSession`, which returns the `userId` of a
  live session, or `null` when the row is gone (or the string is not a
  session id).
- `getAuthUserId` from `@convex-dev/auth/server` looks the user up through
  that session. It still takes just `ctx`, reaching the core component
  under its default mount name, `auth`; an app that mounts it under another
  name passes the component as a second argument.
- `AuthCore` carries its `component`, so the sign-in methods pass it to
  `getAuthUserId` and keep working under a renamed mount; the passkey
  management functions get it through `UsernamePasskeyConfig`.
- `@convex-dev/auth/test` gains `sessionIdentity(tokens)`, the identity for
  `t.withIdentity` that Convex derives from a sign-in's access token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dowski
dowski force-pushed the dowski/sid-in-jwt branch from f77ca58 to 242704a Compare October 26, 2026 23:44
@dowski
dowski merged commit 56bf5c5 into reboot Oct 26, 2026
10 checks passed

This branch was successfully deployed

1 active deployment
Preview – auth-v2 — 242704a4 Deployed Oct 26, 2026 by vercel[bot]
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.

2 participants