Skip to content

Session revocation support - #759

Merged
dowski merged 2 commits into
rebootfrom
dowski/revoke-session-handle
Oct 27, 2026
Merged

dowski merged 2 commits into
rebootfrom
dowski/revoke-session-handle

Conversation

@dowski

@dowski dowski commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Auth mutations now have access to a ctx.convexAuth.revokeOtherSessions function. It will revoke every session for the user except the current one.

It's built from new listSessions and revokeSessions component functions.

The session revocation functions have documentation noting that they should be used with care (e.g. use along with proof that a user possesses credentials).

@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 27, 2026 2:52pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
convex-auth-docs Ignored Ignored Oct 27, 2026 2:52pm 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.

@dowski
dowski force-pushed the dowski/revoke-session-handle branch from fb4748f to ebf9d14 Compare October 20, 2026 15:33
@dowski
dowski force-pushed the dowski/remove-session-refresh-timestamp branch from 0704c76 to 2fd44f3 Compare October 20, 2026 15:33
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from ebf9d14 to 86e0b19 Compare October 20, 2026 15:46
@dowski dowski changed the title WiP on revoking other signed in sessions for a user. Internal support for revoking user's other sessions Oct 20, 2026
@dowski
dowski marked this pull request as ready for review October 20, 2026 16:23

@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 7 files

This PR changes authentication, authorization, or input validation. Ultrareviews find 2.4x more serious bugs than standard reviews. Comment @cubic-dev-ai ultrareview to run one.

Re-trigger cubic

@dowski
dowski requested a review from Nicolapps October 20, 2026 17:46
@dowski
dowski changed the base branch from dowski/remove-session-refresh-timestamp to graphite-base/759 October 21, 2026 14:16
@dowski
dowski force-pushed the graphite-base/759 branch from 2fd44f3 to 78fe425 Compare October 21, 2026 14:16
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from 86e0b19 to 1053d64 Compare October 21, 2026 14:16
@dowski
dowski changed the base branch from graphite-base/759 to dowski/sid-in-jwt 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 force-pushed the dowski/revoke-session-handle branch from 1053d64 to 3f34310 Compare October 21, 2026 14:23
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from 3f34310 to ff30c70 Compare October 21, 2026 16:39
@dowski
dowski force-pushed the dowski/sid-in-jwt branch from dfc4870 to a8fadc6 Compare October 21, 2026 19:50
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from ff30c70 to 2f66a76 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@759

commit: adf014c

@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.

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/lib/types.ts">

<violation number="1" location="packages/core/src/lib/types.ts:369">
P3: The doc says this returns 0 only when not authenticated, but it also returns 0 without revoking anything when the current session has already been revoked. Document that case so callers can tell it apart from having no other sessions.</violation>
</file>

<file name="packages/core/src/component/public.ts">

<violation number="1" location="packages/core/src/component/public.ts:805">
P2: This collects every session and queries its refresh tokens one session at a time; repeated sign-ins can grow this work without bound because session creation has no per-user cap. Cap sessions when issuing them or batch/paginate this listing and revocation.</violation>

<violation number="2" location="packages/core/src/component/public.ts:810">
P2: This treats the newest issued token's expiry as the session expiry, but refresh races can leave several issued tokens valid at once. After `refreshTokenTtlSeconds` changes, an older token can expire later, so `listSessions` reports the session as expired and `revokeOtherSessions` undercounts active sessions; use the maximum expiry across issued tokens.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

for (const session of sessions) {
// Issued rows have no `retiredAt`, so the index orders them by
// `_creationTime`: the last one is the newest.
const newestIssued = await ctx.db

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This treats the newest issued token's expiry as the session expiry, but refresh races can leave several issued tokens valid at once. After refreshTokenTtlSeconds changes, an older token can expire later, so listSessions reports the session as expired and revokeOtherSessions undercounts active sessions; use the maximum expiry across issued tokens.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/component/public.ts, line 810:

<comment>This treats the newest issued token's expiry as the session expiry, but refresh races can leave several issued tokens valid at once. After `refreshTokenTtlSeconds` changes, an older token can expire later, so `listSessions` reports the session as expired and `revokeOtherSessions` undercounts active sessions; use the maximum expiry across issued tokens.</comment>

<file context>
@@ -755,3 +759,112 @@ export const signOut = mutation({
+    for (const session of sessions) {
+      // Issued rows have no `retiredAt`, so the index orders them by
+      // `_creationTime`: the last one is the newest.
+      const newestIssued = await ctx.db
+        .query("refreshTokens")
+        .withIndex("by_session_state", (q) =>
</file context>

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.

I'll address this separately.

.query("sessions")
.withIndex("by_user", (q) => q.eq("userId", userId))
.order("desc")
.collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This collects every session and queries its refresh tokens one session at a time; repeated sign-ins can grow this work without bound because session creation has no per-user cap. Cap sessions when issuing them or batch/paginate this listing and revocation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/component/public.ts, line 805:

<comment>This collects every session and queries its refresh tokens one session at a time; repeated sign-ins can grow this work without bound because session creation has no per-user cap. Cap sessions when issuing them or batch/paginate this listing and revocation.</comment>

<file context>
@@ -755,3 +759,112 @@ export const signOut = mutation({
+      .query("sessions")
+      .withIndex("by_user", (q) => q.eq("userId", userId))
+      .order("desc")
+      .collect();
+    const result = [];
+    for (const session of sessions) {
</file context>

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.

Will prune sessions at issuance (see TODO comment above).

/**
* Revokes every session of the signed-in user other than the current one
* (the JWT's subject). Returns the number of active (non-expired)
* sessions revoked; returns 0 when not authenticated. Expired sessions are

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The doc says this returns 0 only when not authenticated, but it also returns 0 without revoking anything when the current session has already been revoked. Document that case so callers can tell it apart from having no other sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/lib/types.ts, line 369:

<comment>The doc says this returns 0 only when not authenticated, but it also returns 0 without revoking anything when the current session has already been revoked. Document that case so callers can tell it apart from having no other sessions.</comment>

<file context>
@@ -363,6 +363,18 @@ export type BoundAuthHelpers<Profile> = {
+  /**
+   * Revokes every session of the signed-in user other than the current one
+   * (the JWT's subject). Returns the number of active (non-expired)
+   * sessions revoked; returns 0 when not authenticated. Expired sessions are
+   * deleted as well, but not counted.
+   *
</file context>
Suggested change
* sessions revoked; returns 0 when not authenticated. Expired sessions are
* sessions revoked. Returns 0 when not authenticated, or when the current
* session no longer exists (then nothing is revoked). Expired sessions are

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.

It already says returns the number of active sessions revoked. That could be 0.

Comment on lines +372 to +375
* NOTE: Callers should only invoke this function after verifying that the
* logged in user truly owns the session, via confirming an authentication
* method. Otherwise, the request might be originating from a stolen session,
* which shouldn't be able to revoke other sessions.

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.

What is this comment for, since this method receives no parameter?

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.

The implementation looks up the user via the current session. So even though there's no argument, the caution here is to confirm that the session user can authenticate as proof that they're the real user.

*/
resolveUserId(providerAccountId: string): Promise<string | null>;
/**
* Revokes every session of the signed-in user other than the current one

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.

It sounds good to add this method to the method authoring APIs, it allows us to improve the implementation later if necessary

sessionIds: others.map(({ sessionId }) => sessionId),
}),
);
// Expired sessions are deleted too, but only the active ones count.

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.

Shouldn't we automatically exclude expired sessions from listSessions? Logically they don’t exist, whether an expired session has been deleted by the batch worker or not should be an implementation detail of the component that is not exposed to the app

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.

In order to know whether a session is expired, you need the current time. I didn't want to put a Date.now() call in the listSessions query since that's an anti-pattern.

Once we have the batch worker (actually pending in a draft up stack) the window for including expired sessions will be small, but possible. So I think it makes sense to handle them here.

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.

Makes sense, thanks

): Promise<TokenBundle> {
const now = Date.now();
// TODO: cap the sessions a user may have (e.g. 100), deleting the oldest
// when a new one would exceed it. That bounds `listSessions`, which reads

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.

Maybe as an alternative we could consider paginating listSessions?

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 don’t know how feasible it would be to have methods revoke sessions asynchronously (e.g. through a self-scheduling mutation), so that it supports revoking sessions for a user that has lots of them.

Maybe it would make sense to keep the revokeOtherSessions() method @internal (even after we expose BoundAuthHelpers for custom method authors) if we consider that this API is a little bit footgunny

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.

Yeah I'd like to explore/discuss what a more reactive/scheduled deletion approach might look like.

I'm fine marking the function as @internal for now.

Comment thread packages/core/src/component/public.ts Outdated
continue;
}
result.push({
sessionId: session._id as string,

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.

nit: Is this cast necessary? Id<> is string & { … } so I would expect this cast to look more dangerous than what it is

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.

Good point.

@dowski
dowski force-pushed the dowski/sid-in-jwt branch from 1c5ba2c to f77ca58 Compare October 26, 2026 23:27
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from fcde1bf to 201b3bd Compare October 26, 2026 23:27
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from 201b3bd to 957d13b Compare October 26, 2026 23:44
@dowski
dowski force-pushed the dowski/sid-in-jwt branch from f77ca58 to 242704a Compare October 26, 2026 23:44
@dowski
dowski changed the base branch from dowski/sid-in-jwt to graphite-base/759 October 26, 2026 23:50
@dowski
dowski force-pushed the graphite-base/759 branch from 242704a to 56bf5c5 Compare October 26, 2026 23:50
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from 957d13b to 520e0ba Compare October 26, 2026 23:50
@cursor
cursor Bot changed the base branch from graphite-base/759 to reboot October 26, 2026 23:50
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from 520e0ba to 2a03112 Compare October 26, 2026 23:50
Replace the component's single-session revokeSession with revokeSessions,
which takes a list of session ids and returns the ids it revoked. The
app-side revokeOtherSessions now lists the user's sessions, filters out the
current one, and revokes the rest in a single component call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dowski
dowski force-pushed the dowski/revoke-session-handle branch from 2a03112 to adf014c Compare October 27, 2026 14:51
@dowski
dowski merged commit 09915cc into reboot Oct 27, 2026
13 checks passed

This branch was successfully deployed

1 active deployment
Preview – auth-v2 — adf014cc Deployed Oct 27, 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