[PR #6978] feat(revokeSession): implement the sessionId support in revoke session #7002

Open
opened 2026-03-13 13:20:52 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/6978
Author: @Ridhim-RR
Created: 12/24/2025
Status: 🔄 Open

Base: canaryHead: feat/sessionId-parameter-in-revoke-session


📝 Commits (8)

  • 5d4c3f7 feat(revokeSession): implement the sessionId support in revoke session
  • 18ff4ec Update packages/better-auth/src/api/routes/session-api.test.ts
  • 1bdf259 chore(sso): allow Buffer (#6979)
  • d46dd30 fix(testcase): fix issue in testcase and snapshots
  • 7273738 Merge branch 'feat/sessionId-parameter-in-revoke-session' of https://github.com/Ridhim-RR/better-auth into feat/sessionId-parameter-in-revoke-session
  • b082a2f Merge branch 'canary' into feat/sessionId-parameter-in-revoke-session
  • 17753bd choee(test): test issue resolved
  • 58b2162 Merge branch 'canary' into feat/sessionId-parameter-in-revoke-session

📊 Changes

10 files changed (+227 additions, -32 deletions)

View changed files

📝 demo/nextjs/data/user/revoke-session-mutation.ts (+2 -1)
📝 packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts (+1 -1)
📝 packages/better-auth/src/api/routes/session-api.test.ts (+47 -1)
📝 packages/better-auth/src/api/routes/session.ts (+18 -8)
📝 packages/better-auth/src/context/__snapshots__/create-context.test.ts.snap (+1 -0)
📝 packages/better-auth/src/db/internal-adapter.ts (+143 -9)
📝 packages/better-auth/src/db/secondary-storage.test.ts (+1 -1)
📝 packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap (+1 -3)
📝 packages/core/src/types/context.ts (+5 -0)
📝 pnpm-lock.yaml (+8 -8)

📄 Description

Closes #6940

Summary

This PR adds support for revoking sessions using sessionId in addition to the existing token parameter. This enables developers to revoke sessions when they only have access to the session ID (e.g., from listSessions() response), which is especially important for admin dashboards and session management UIs.

Problem

Previously, the revokeSession endpoint only accepted a session token:

await authClient.revokeSession({
    token: "session-token"
})

API Changes

The revokeSession endpoint now accepts either token or sessionId:

// Using token (existing behavior)
await authClient.revokeSession({
    token: "session-token"
})

// Using sessionId (new behavior)
await authClient.revokeSession({
    sessionId: "session-id"
})

At least one parameter must be provided, enforced via Zod schema refinement.

Technical Implementation

1. Session Route Updates (session.ts)

Updated the revokeSession endpoint schema to accept both parameters:

body: z.object({
    token: z.string().optional().meta({
        description: "The token to revoke",
    }),
    sessionId: z.string().optional().meta({
        description: "The session ID to revoke",
    }),
})
.refine((data: any) => data.token || data.sessionId, {
    message: "Either token or sessionId must be provided",
})

The endpoint now conditionally calls the appropriate finder method:

const session = token
    ? await ctx.context.internalAdapter.findSession(token)
    : await ctx.context.internalAdapter.findSessionBySessionId(sessionId!);

2. Internal Adapter: New findSessionBySessionId Method (internal-adapter.ts)

Added a new method to find sessions by ID, which works with both secondary storage and database:

For secondary storage:

  • Looks up the session-id-${sessionId} key to get the corresponding token
  • Uses the token to retrieve the full session data from token key
  • Falls back to database if session isn't found in cache

For database:

  • Directly queries the session table by id field
  • Joins with user table to return complete session data
findSessionBySessionId: async (sessionId: string) => {
    if (secondaryStorage) {
        // Get token from session-id mapping
        const tokenRaw = await secondaryStorage.get(`session-id-${sessionId}`);
        const token = typeof tokenRaw === "string" ? tokenRaw : undefined;
        
        // Retrieve session using token
        const sessionStringified = token
            ? await secondaryStorage.get(token)
            : undefined;
        
        // ... parse and return session
    }
    
    // Fallback to database query
    const result = await currentAdapter.findOne<Session & { user: User | null }>({
        model: "session",
        where: [{ field: "id", value: sessionId }],
        join: { user: true }
    });
}

3. Session ID Mapping in Secondary Storage (internal-adapter.ts)

The critical change: When creating a session with secondary storage, we now create a bidirectional mapping:

  1. Token → Session data (existing): Stores full session and user data
  2. SessionId → Token (new): Enables lookup by session ID
// In createSession function
const sessionWithId = {
    ...sessionData,
    id: sessionData.id || sessionIdGenerator({ model: "session" }),
};

// Store session data by token (existing)
await secondaryStorage.set(
    data.token,
    JSON.stringify({
        session: sessionWithId,
        user,
    }),
    sessionTTL,
);

// Create mapping from sessionId to token (NEW!)
await secondaryStorage.set(
    `session-id-${sessionWithId.id}`,
    data.token,
    sessionTTL, // Same TTL as the session
);

Why this mapping is necessary:

  • listSessions() returns session IDs (not tokens) for security
  • When using only secondary storage, session data is keyed by token
  • Without this mapping, there's no way to look up a session by its ID
  • The mapping allows findSessionBySessionId to work efficiently

4. Cleanup on Session Deletion

Updated deleteSession and deleteSessions to clean up the session-id mapping:

In deleteSession:

// Delete the main session data
await secondaryStorage.delete(token);

// Delete the sessionId → token mapping
if (sessionId) {
    await secondaryStorage.delete(`session-id-${sessionId}`);
}

In deleteSessions:

for (const session of sessions) {
    const sessionData = await secondaryStorage.get(session.token);
    if (sessionData) {
        const parsed = safeJSONParse<{ session: Session; user: User }>(sessionData);
        if (parsed?.session.id) {
            // Delete session-id mapping
            await secondaryStorage.delete(`session-id-${parsed.session.id}`);
        }
    }
    await secondaryStorage.delete(session.token);
}

This prevents orphaned keys in secondary storage and ensures proper cleanup.

5. Demo Updates (demo/nextjs/data/user/revoke-session-mutation.ts)

Updated the demo to reflect the new API signature:

interface RevokeSessionParams {
    token?: string;
    sessionId?: string;
}

Why This Architecture?

Secondary Storage Key Structure

Key: token → Value: { session: {...}, user: {...} }
Key: session-id-{sessionId} → Value: token
Key: active-sessions-{userId} → Value: [{ token, expiresAt }]

This structure allows:

  1. Fast token-based lookups (primary use case)
  2. Session ID lookups via the mapping (new capability)
  3. Listing all sessions for a user
  4. Automatic expiration via TTL

Session ID Generation Timing

The session ID is generated during createSession, not during database insert:

  • Ensures consistency between secondary storage and database
  • Allows the mapping to be created immediately
  • Works correctly when storeSessionInDatabase: false

Example Usage

// List all sessions
const allSessions = await authClient.listSessions();

// Revoke all sessions except current one
for (const session of allSessions) {
    if (session.id !== currentSession.session.id) {
        await authClient.revokeSession({
            sessionId: session.id
        });
    }
}

Summary by cubic

Add sessionId support to revokeSession so sessions can be revoked without a token (e.g., from listSessions). Adds a sessionId→token mapping in secondary storage and ensures proper cleanup.

  • New Features
    • revokeSession now accepts either token or sessionId; schema enforces at least one.
    • InternalAdapter: added findSessionBySessionId with cache-first lookup and DB fallback.
    • Secondary storage now stores session-id-{id} → token on createSession and deletes it on revoke/delete; tests, types, and demo updated.

Written for commit 58b2162684. Summary will update automatically on new commits.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/6978 **Author:** [@Ridhim-RR](https://github.com/Ridhim-RR) **Created:** 12/24/2025 **Status:** 🔄 Open **Base:** `canary` ← **Head:** `feat/sessionId-parameter-in-revoke-session` --- ### 📝 Commits (8) - [`5d4c3f7`](https://github.com/better-auth/better-auth/commit/5d4c3f7e708be15a09a0db3470c1e42bfcc78874) feat(revokeSession): implement the sessionId support in revoke session - [`18ff4ec`](https://github.com/better-auth/better-auth/commit/18ff4ec30b9d2bd6d0d309e18b09559ef95724fd) Update packages/better-auth/src/api/routes/session-api.test.ts - [`1bdf259`](https://github.com/better-auth/better-auth/commit/1bdf2599c5d035d2fc36ad5817aed396fc3869c5) chore(sso): allow `Buffer` (#6979) - [`d46dd30`](https://github.com/better-auth/better-auth/commit/d46dd30e342634fe4ad1184c851b541bbd1f9dc7) fix(testcase): fix issue in testcase and snapshots - [`7273738`](https://github.com/better-auth/better-auth/commit/7273738976756cb32354f73f80e77a870c8afd92) Merge branch 'feat/sessionId-parameter-in-revoke-session' of https://github.com/Ridhim-RR/better-auth into feat/sessionId-parameter-in-revoke-session - [`b082a2f`](https://github.com/better-auth/better-auth/commit/b082a2fe2037509af678e732ee829e8323b3b927) Merge branch 'canary' into feat/sessionId-parameter-in-revoke-session - [`17753bd`](https://github.com/better-auth/better-auth/commit/17753bd4cbdfa1ddf9e14452368ba4a7b99627e7) choee(test): test issue resolved - [`58b2162`](https://github.com/better-auth/better-auth/commit/58b2162684fe314432541f18c18c8c695d6accd5) Merge branch 'canary' into feat/sessionId-parameter-in-revoke-session ### 📊 Changes **10 files changed** (+227 additions, -32 deletions) <details> <summary>View changed files</summary> 📝 `demo/nextjs/data/user/revoke-session-mutation.ts` (+2 -1) 📝 `packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts` (+1 -1) 📝 `packages/better-auth/src/api/routes/session-api.test.ts` (+47 -1) 📝 `packages/better-auth/src/api/routes/session.ts` (+18 -8) 📝 `packages/better-auth/src/context/__snapshots__/create-context.test.ts.snap` (+1 -0) 📝 `packages/better-auth/src/db/internal-adapter.ts` (+143 -9) 📝 `packages/better-auth/src/db/secondary-storage.test.ts` (+1 -1) 📝 `packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap` (+1 -3) 📝 `packages/core/src/types/context.ts` (+5 -0) 📝 `pnpm-lock.yaml` (+8 -8) </details> ### 📄 Description Closes #6940 ## Summary This PR adds support for revoking sessions using `sessionId` in addition to the existing `token` parameter. This enables developers to revoke sessions when they only have access to the session ID (e.g., from `listSessions()` response), which is especially important for admin dashboards and session management UIs. ## Problem Previously, the `revokeSession` endpoint only accepted a session `token`: ```typescript await authClient.revokeSession({ token: "session-token" }) ``` ### API Changes The `revokeSession` endpoint now accepts **either** `token` or `sessionId`: ```typescript // Using token (existing behavior) await authClient.revokeSession({ token: "session-token" }) // Using sessionId (new behavior) await authClient.revokeSession({ sessionId: "session-id" }) ``` At least one parameter must be provided, enforced via Zod schema refinement. ## Technical Implementation ### 1. **Session Route Updates** ([session.ts](packages/better-auth/src/api/routes/session.ts)) Updated the `revokeSession` endpoint schema to accept both parameters: ```typescript body: z.object({ token: z.string().optional().meta({ description: "The token to revoke", }), sessionId: z.string().optional().meta({ description: "The session ID to revoke", }), }) .refine((data: any) => data.token || data.sessionId, { message: "Either token or sessionId must be provided", }) ``` The endpoint now conditionally calls the appropriate finder method: ```typescript const session = token ? await ctx.context.internalAdapter.findSession(token) : await ctx.context.internalAdapter.findSessionBySessionId(sessionId!); ``` ### 2. **Internal Adapter: New `findSessionBySessionId` Method** ([internal-adapter.ts](packages/better-auth/src/db/internal-adapter.ts)) Added a new method to find sessions by ID, which works with both secondary storage and database: **For secondary storage:** - Looks up the `session-id-${sessionId}` key to get the corresponding token - Uses the token to retrieve the full session data from `token` key - Falls back to database if session isn't found in cache **For database:** - Directly queries the session table by `id` field - Joins with user table to return complete session data ```typescript findSessionBySessionId: async (sessionId: string) => { if (secondaryStorage) { // Get token from session-id mapping const tokenRaw = await secondaryStorage.get(`session-id-${sessionId}`); const token = typeof tokenRaw === "string" ? tokenRaw : undefined; // Retrieve session using token const sessionStringified = token ? await secondaryStorage.get(token) : undefined; // ... parse and return session } // Fallback to database query const result = await currentAdapter.findOne<Session & { user: User | null }>({ model: "session", where: [{ field: "id", value: sessionId }], join: { user: true } }); } ``` ### 3. **Session ID Mapping in Secondary Storage** ([internal-adapter.ts](packages/better-auth/src/db/internal-adapter.ts)) The critical change: When creating a session with secondary storage, we now create a **bidirectional mapping**: 1. **Token → Session data** (existing): Stores full session and user data 2. **SessionId → Token** (new): Enables lookup by session ID ```typescript // In createSession function const sessionWithId = { ...sessionData, id: sessionData.id || sessionIdGenerator({ model: "session" }), }; // Store session data by token (existing) await secondaryStorage.set( data.token, JSON.stringify({ session: sessionWithId, user, }), sessionTTL, ); // Create mapping from sessionId to token (NEW!) await secondaryStorage.set( `session-id-${sessionWithId.id}`, data.token, sessionTTL, // Same TTL as the session ); ``` **Why this mapping is necessary:** - `listSessions()` returns session IDs (not tokens) for security - When using only secondary storage, session data is keyed by token - Without this mapping, there's no way to look up a session by its ID - The mapping allows `findSessionBySessionId` to work efficiently ### 4. **Cleanup on Session Deletion** Updated `deleteSession` and `deleteSessions` to clean up the session-id mapping: **In `deleteSession`:** ```typescript // Delete the main session data await secondaryStorage.delete(token); // Delete the sessionId → token mapping if (sessionId) { await secondaryStorage.delete(`session-id-${sessionId}`); } ``` **In `deleteSessions`:** ```typescript for (const session of sessions) { const sessionData = await secondaryStorage.get(session.token); if (sessionData) { const parsed = safeJSONParse<{ session: Session; user: User }>(sessionData); if (parsed?.session.id) { // Delete session-id mapping await secondaryStorage.delete(`session-id-${parsed.session.id}`); } } await secondaryStorage.delete(session.token); } ``` This prevents orphaned keys in secondary storage and ensures proper cleanup. ### 5. **Demo Updates** ([demo/nextjs/data/user/revoke-session-mutation.ts](demo/nextjs/data/user/revoke-session-mutation.ts)) Updated the demo to reflect the new API signature: ```typescript interface RevokeSessionParams { token?: string; sessionId?: string; } ``` ## Why This Architecture? ### Secondary Storage Key Structure ``` Key: token → Value: { session: {...}, user: {...} } Key: session-id-{sessionId} → Value: token Key: active-sessions-{userId} → Value: [{ token, expiresAt }] ``` This structure allows: 1. Fast token-based lookups (primary use case) 2. Session ID lookups via the mapping (new capability) 3. Listing all sessions for a user 4. Automatic expiration via TTL ### Session ID Generation Timing The session ID is generated during `createSession`, not during database insert: - Ensures consistency between secondary storage and database - Allows the mapping to be created immediately - Works correctly when `storeSessionInDatabase: false` ## Example Usage ``` // List all sessions const allSessions = await authClient.listSessions(); // Revoke all sessions except current one for (const session of allSessions) { if (session.id !== currentSession.session.id) { await authClient.revokeSession({ sessionId: session.id }); } } ``` <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add sessionId support to revokeSession so sessions can be revoked without a token (e.g., from listSessions). Adds a sessionId→token mapping in secondary storage and ensures proper cleanup. - **New Features** - revokeSession now accepts either token or sessionId; schema enforces at least one. - InternalAdapter: added findSessionBySessionId with cache-first lookup and DB fallback. - Secondary storage now stores session-id-{id} → token on createSession and deletes it on revoke/delete; tests, types, and demo updated. <sup>Written for commit 58b2162684fe314432541f18c18c8c695d6accd5. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-03-13 13:20:53 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#7002