[PR #8101] [CLOSED] Fix Session ID Missing From Secondary Storage #15993

Closed
opened 2026-04-13 10:20:26 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/8101
Author: @barreeeiroo
Created: 2/22/2026
Status: Closed

Base: mainHead: fix/session-id-missing-in-secondary-storage


📝 Commits (1)

  • 4118deb Fix session id missing from secondary storage after creation

📊 Changes

2 files changed (+119 additions, -0 deletions)

View changed files

📝 packages/better-auth/src/db/internal-adapter.ts (+23 -0)
📝 packages/better-auth/src/db/secondary-storage.test.ts (+96 -0)

📄 Description

Summary

Sessions stored in secondary storage (e.g. Redis) are missing the id field, causing any code that reads session.id from a cached session to get undefined.

This breaks the @better-auth/oauth-provider plugin: the authorization code is stored without a sessionId, and the token exchange fails with {"error_description":"session no longer exists","error":"invalid_request"}.

This bug occurs whenever secondary storage is configured, even when using storeSessionInDatabase: true. As long as there is secondary storage, the Redis entry will be missing the id.

Root Cause

In createSession (packages/better-auth/src/db/internal-adapter.ts), the data object built for the new session never includes an id — that field is normally generated by the DB adapter's transformInput during the create call.

In createWithHooks (packages/better-auth/src/db/with-hooks.ts, lines 48–58), the custom fn that writes to secondary storage receives sessionData which lacks the id:

  • When storeSessionInDatabase is false (default): the DB adapter is never called. fn receives actualData directly — no id is ever generated.
  • When storeSessionInDatabase is true: the DB adapter runs first and returns a record with id, but fn still stores a session without id in secondary storage.

The result: the DB has the full session; secondary storage has a partial one.

Redis entry (note no id in session):

{
  "session": {
    "ipAddress": "...",
    "userAgent": "...",
    "expiresAt": "...",
    "userId": "usr_xxx",
    "token": "abc123",
    "createdAt": "...",
    "updatedAt": "..."
  },
  "user": { "id": "usr_xxx", "..." }
}

Fix

After createWithHooks returns, patch the secondary storage entry to include the id:

  • If the DB result already contains the id (i.e. storeSessionInDatabase: true), use that.
  • Otherwise (secondary-only mode), fall back to ctx.generateId({ model: "session" }).

Workaround

Until this is released, users can patch the Redis entry via a databaseHooks.session.create.after hook:

session: {
  create: {
    after: async (session) => {
      if (redisClient && session.id && session.token) {
        try {
          const cached = await redisClient.get(session.token);
          if (cached) {
            const parsed = JSON.parse(cached);
            if (parsed?.session && !parsed.session.id) {
              parsed.session.id = session.id;
              const ttl = await redisClient.ttl(session.token);
              await redisClient.set(
                session.token,
                JSON.stringify(parsed),
                ttl > 0 ? { EX: ttl } : undefined,
              );
            }
          }
        } catch (error) {
          console.error("Failed to patch session id in Redis:", error);
        }
      }
    },
  },
},

Summary by cubic

Fixes missing session.id in secondary storage by updating the cached session after creation. Restores OAuth token exchange by ensuring authorization codes include sessionId.

  • Bug Fixes
    • After createWithHooks, inject session.id into secondary storage; use DB id when available, otherwise generate one.
    • Added tests for secondary-only and database+secondary modes to verify id is stored.
    • Prevents OAuth token exchange failure ("session no longer exists").

Written for commit 4118debd9f. Summary will update 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/8101 **Author:** [@barreeeiroo](https://github.com/barreeeiroo) **Created:** 2/22/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/session-id-missing-in-secondary-storage` --- ### 📝 Commits (1) - [`4118deb`](https://github.com/better-auth/better-auth/commit/4118debd9fd9d60d860b1bed545818e19cbdfcad) Fix session id missing from secondary storage after creation ### 📊 Changes **2 files changed** (+119 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `packages/better-auth/src/db/internal-adapter.ts` (+23 -0) 📝 `packages/better-auth/src/db/secondary-storage.test.ts` (+96 -0) </details> ### 📄 Description ## Summary Sessions stored in secondary storage (e.g. Redis) are missing the `id` field, causing any code that reads `session.id` from a cached session to get `undefined`. This breaks the `@better-auth/oauth-provider` plugin: the authorization code is stored without a `sessionId`, and the token exchange fails with `{"error_description":"session no longer exists","error":"invalid_request"}`. This bug occurs whenever secondary storage is configured, even when using `storeSessionInDatabase: true`. As long as there is secondary storage, the Redis entry will be missing the `id`. ## Root Cause In `createSession` (`packages/better-auth/src/db/internal-adapter.ts`), the `data` object built for the new session never includes an `id` — that field is normally generated by the DB adapter's `transformInput` during the `create` call. In `createWithHooks` (`packages/better-auth/src/db/with-hooks.ts`, lines 48–58), the custom `fn` that writes to secondary storage receives `sessionData` which lacks the `id`: - When `storeSessionInDatabase` is **false** (default): the DB adapter is never called. `fn` receives `actualData` directly — no `id` is ever generated. - When `storeSessionInDatabase` is **true**: the DB adapter runs first and returns a record with `id`, but `fn` still stores a session without `id` in secondary storage. The result: the DB has the full session; secondary storage has a partial one. Redis entry (note no `id` in `session`): ```json { "session": { "ipAddress": "...", "userAgent": "...", "expiresAt": "...", "userId": "usr_xxx", "token": "abc123", "createdAt": "...", "updatedAt": "..." }, "user": { "id": "usr_xxx", "..." } } ``` ## Fix After `createWithHooks` returns, patch the secondary storage entry to include the `id`: - If the DB result already contains the `id` (i.e. `storeSessionInDatabase: true`), use that. - Otherwise (secondary-only mode), fall back to `ctx.generateId({ model: "session" })`. ## Workaround Until this is released, users can patch the Redis entry via a `databaseHooks.session.create.after` hook: ```typescript session: { create: { after: async (session) => { if (redisClient && session.id && session.token) { try { const cached = await redisClient.get(session.token); if (cached) { const parsed = JSON.parse(cached); if (parsed?.session && !parsed.session.id) { parsed.session.id = session.id; const ttl = await redisClient.ttl(session.token); await redisClient.set( session.token, JSON.stringify(parsed), ttl > 0 ? { EX: ttl } : undefined, ); } } } catch (error) { console.error("Failed to patch session id in Redis:", error); } } }, }, }, ``` <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes missing session.id in secondary storage by updating the cached session after creation. Restores OAuth token exchange by ensuring authorization codes include sessionId. - **Bug Fixes** - After createWithHooks, inject session.id into secondary storage; use DB id when available, otherwise generate one. - Added tests for secondary-only and database+secondary modes to verify id is stored. - Prevents OAuth token exchange failure ("session no longer exists"). <sup>Written for commit 4118debd9fd9d60d860b1bed545818e19cbdfcad. Summary will update 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-04-13 10:20:26 -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#15993