[GH-ISSUE #4517] auth.api.getSession() returns null despite valid session token in database and proper cookie headers #9967

Closed
opened 2026-04-13 05:49:28 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @hamdi-islam on GitHub (Sep 8, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4517

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a user account:

    curl -X POST http://localhost:3000/api/v1/auth/signup \
      -H "Content-Type: application/json" \
      -d '{"email": "test@example.com", "password": "password123", "name": "Test User"}'
    
  2. Sign in to get session:

    curl -X POST http://localhost:3000/api/v1/auth/signin \
      -H "Content-Type: application/json" \
      -d '{"email": "test@example.com", "password": "password123"}' \
      -c cookies.txt
    
  3. Verify session exists in database:

    SELECT token, "userId", "expiresAt", "createdAt" FROM session 
    WHERE token = '<session_token_from_step_2>' 
    ORDER BY "createdAt" DESC LIMIT 1;
    

    Result: Valid session with future expiry date

  4. Call get session endpoint:

    curl -X GET http://localhost:3000/api/v1/auth/me \
      -H "Cookie: better-auth.session-token=<session_token>" \
      -v
    

Current vs. Expected behavior

Expected

  • auth.api.getSession() should return { user: {...}, session: {...} }
  • /api/v1/auth/me should return user data from session

Actual

  • auth.api.getSession() returns null
  • /api/v1/auth/me returns { success: false, message: "Session not found" }
  • No errors are thrown - the API call appears "successful"

What version of Better Auth are you using?

1.3.8

System info

- **BetterAuth Version**: 1.3.8
- **Database**: PostgreSQL with Drizzle ORM
- **Server**: Fastify 5.5.0 
- **Node.js**: Latest
- **OS**: macOS

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)


Additional context

BetterAuth Configuration (src/lib/auth.ts)

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../schema";
import { config } from "@/config";

export const auth = betterAuth({
    database: drizzleAdapter(db, {
        provider: "pg",
        schema,
    }),
    emailAndPassword: {
        enabled: true,
    },
    session: {
        expiresIn: 60 * 60 * 24 * 7, // 7 days
        updateAge: 60 * 60 * 24, // 1 day
        cookieCache: {
            enabled: true,
            maxAge: 60 * 5, // 5 minutes
        },
    },
    secret: config.auth.secret,
    cookies: {
        sessionToken: {
            options: {
                httpOnly: true,
                sameSite: "lax",
                secure: process.env.NODE_ENV === "production",
                path: "/",
            },
        },
    },
    logger: {
        disabled: false,
        level: "error",
        log: (level, message, ...args) => {
            console.log(`[${level}] ${message}`, ...args);
        }
    }
});

Auth Service (src/services/auth.service.ts)

async getSession(sessionToken: string) {
    try {
        const headers = new Headers()
        headers.append('cookie', `better-auth.session-token=${sessionToken}`)

        console.log({ headers })

        const result = await auth.api.getSession({
            headers
        });

        if (!result) {
            return {
                success: false,
                message: 'Session not found',
            };
        }

        return {
            success: true,
            message: 'Session retrieved successfully',
            user: {
                id: result.user.id,
                email: result.user.email,
                name: result.user.name,
            },
        };
    } catch (error) {
        return {
            success: false,
            message: error instanceof Error ? error.message : 'An unexpected error occurred',
        };
    }
}

Controller Implementation (src/controllers/auth.controller.ts)

getMe = async (
    request: FastifyRequest,
    reply: FastifyReply
) => {
    try {
        const sessionToken = request.headers.authorization?.replace('Bearer ', '') ||
            request.cookies['better-auth.session-token'];

        if (!sessionToken) {
            return reply.status(401).send({
                success: false,
                message: 'No session token provided',
            });
        }

        const result = await this.authService.getSession(sessionToken);
        return result;

    } catch (error) {
        return reply.status(500).send({
            success: false,
            message: 'Internal server error',
        });
    }
};

Fastify Server Setup (src/services/server.service.ts)

private async setupPlugins(): Promise<void> {
    await this.fastify.register(cors, {
        origin: true,
        credentials: true,
    });

    await this.fastify.register(cookie, {
        secret: config.auth.secret,
    });
}

Database Schema (src/schema/session.schema.ts)

import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { user } from "./user.schema";

export const session = pgTable("session", {
    id: text("id").primaryKey(),
    expiresAt: timestamp("expires_at").notNull(),
    token: text("token").notNull().unique(),
    createdAt: timestamp("created_at").notNull(),
    updatedAt: timestamp("updated_at").notNull(),
    ipAddress: text("ip_address"),
    userAgent: text("user_agent"),
    userId: text("user_id")
        .notNull()
        .references(() => user.id, { onDelete: "cascade" }),
});
Originally created by @hamdi-islam on GitHub (Sep 8, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4517 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. **Create a user account**: ```bash curl -X POST http://localhost:3000/api/v1/auth/signup \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com", "password": "password123", "name": "Test User"}' ``` 2. **Sign in to get session**: ```bash curl -X POST http://localhost:3000/api/v1/auth/signin \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com", "password": "password123"}' \ -c cookies.txt ``` 3. **Verify session exists in database**: ```sql SELECT token, "userId", "expiresAt", "createdAt" FROM session WHERE token = '<session_token_from_step_2>' ORDER BY "createdAt" DESC LIMIT 1; ``` **Result**: Valid session with future expiry date 4. **Call get session endpoint**: ```bash curl -X GET http://localhost:3000/api/v1/auth/me \ -H "Cookie: better-auth.session-token=<session_token>" \ -v ``` ### Current vs. Expected behavior ### Expected - `auth.api.getSession()` should return `{ user: {...}, session: {...} }` - `/api/v1/auth/me` should return user data from session ### Actual - `auth.api.getSession()` returns `null` - `/api/v1/auth/me` returns `{ success: false, message: "Session not found" }` - No errors are thrown - the API call appears "successful" ### What version of Better Auth are you using? 1.3.8 ### System info ```bash - **BetterAuth Version**: 1.3.8 - **Database**: PostgreSQL with Drizzle ORM - **Server**: Fastify 5.5.0 - **Node.js**: Latest - **OS**: macOS ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript ``` ### Additional context ### BetterAuth Configuration (`src/lib/auth.ts`) ```typescript import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "../db"; import * as schema from "../schema"; import { config } from "@/config"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema, }), emailAndPassword: { enabled: true, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day cookieCache: { enabled: true, maxAge: 60 * 5, // 5 minutes }, }, secret: config.auth.secret, cookies: { sessionToken: { options: { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", }, }, }, logger: { disabled: false, level: "error", log: (level, message, ...args) => { console.log(`[${level}] ${message}`, ...args); } } }); ``` ### Auth Service (`src/services/auth.service.ts`) ```typescript async getSession(sessionToken: string) { try { const headers = new Headers() headers.append('cookie', `better-auth.session-token=${sessionToken}`) console.log({ headers }) const result = await auth.api.getSession({ headers }); if (!result) { return { success: false, message: 'Session not found', }; } return { success: true, message: 'Session retrieved successfully', user: { id: result.user.id, email: result.user.email, name: result.user.name, }, }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : 'An unexpected error occurred', }; } } ``` ### Controller Implementation (`src/controllers/auth.controller.ts`) ```typescript getMe = async ( request: FastifyRequest, reply: FastifyReply ) => { try { const sessionToken = request.headers.authorization?.replace('Bearer ', '') || request.cookies['better-auth.session-token']; if (!sessionToken) { return reply.status(401).send({ success: false, message: 'No session token provided', }); } const result = await this.authService.getSession(sessionToken); return result; } catch (error) { return reply.status(500).send({ success: false, message: 'Internal server error', }); } }; ``` ### Fastify Server Setup (`src/services/server.service.ts`) ```typescript private async setupPlugins(): Promise<void> { await this.fastify.register(cors, { origin: true, credentials: true, }); await this.fastify.register(cookie, { secret: config.auth.secret, }); } ``` ### Database Schema (`src/schema/session.schema.ts`) ```typescript import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { user } from "./user.schema"; export const session = pgTable("session", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").notNull(), updatedAt: timestamp("updated_at").notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }); ```
GiteaMirror added the locked label 2026-04-13 05:49:28 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 8, 2025):

Hi @hamdi-islam! I'm Dosu and I’m helping the better-auth team.

This is a known issue with Better Auth when the session token cookie is not properly signed and encoded as expected by the framework. Manually setting the cookie header with a raw session token (as in your getSession implementation) will not work—Better Auth expects a signed and URL-encoded cookie value, not just the raw token. The recommended approach is to use authClient.getCookie() (or the equivalent server-side helper) to generate the correct cookie string for requests, ensuring proper signing and encoding. See this discussion for details.

Another possible cause is a bug in cookie cache handling: in Better Auth versions before a recent fix (PR #3283, merged 2025-07-26), getSession could return null if the cookie session data and its signature had mismatched expiresAt timestamps. This would sporadically clear the session data cookie even if the session was valid. Upgrading to the latest version will resolve this if you're affected by that bug (see PR #3283).

To fix your issue:

  • Do not manually construct the cookie header with the raw session token. Use the helper that produces a properly signed and encoded cookie string.
  • If you are using cookie cache, upgrade Better Auth to the latest version to get the fix for the expiresAt mismatch bug.
  • Double-check your cookie configuration: the cookie name, prefix, and security flags must match what Better Auth expects. If you are running in a non-secure (HTTP) environment, make sure the secure flag is set to false and the cookie name does not have the __Secure- prefix.

If you still see issues after these changes, enable debug logging and inspect the actual cookie values being sent and received to ensure they match what Better Auth expects.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3265894492 --> @dosubot[bot] commented on GitHub (Sep 8, 2025): <!-- Greeting --> Hi @hamdi-islam! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known issue with Better Auth when the session token cookie is not properly signed and encoded as expected by the framework. Manually setting the cookie header with a raw session token (as in your getSession implementation) will not work—Better Auth expects a signed and URL-encoded cookie value, not just the raw token. The recommended approach is to use `authClient.getCookie()` (or the equivalent server-side helper) to generate the correct cookie string for requests, ensuring proper signing and encoding. See [this discussion for details](https://github.com/better-auth/better-auth/issues/3892). Another possible cause is a bug in cookie cache handling: in Better Auth versions before a recent fix (PR #3283, merged 2025-07-26), `getSession` could return null if the cookie session data and its signature had mismatched `expiresAt` timestamps. This would sporadically clear the session data cookie even if the session was valid. Upgrading to the latest version will resolve this if you're affected by that bug ([see PR #3283](https://github.com/better-auth/better-auth/pull/3283)). To fix your issue: - Do not manually construct the cookie header with the raw session token. Use the helper that produces a properly signed and encoded cookie string. - If you are using cookie cache, upgrade Better Auth to the latest version to get the fix for the `expiresAt` mismatch bug. - Double-check your cookie configuration: the cookie name, prefix, and security flags must match what Better Auth expects. If you are running in a non-secure (HTTP) environment, make sure the `secure` flag is set to `false` and the cookie name does not have the `__Secure-` prefix. If you still see issues after these changes, enable debug logging and inspect the actual cookie values being sent and received to ensure they match what Better Auth expects. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/01c602ff-5fc8-4ff8-acb9-1a486d6780a4?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4517)
Author
Owner

@KhalilMeziane commented on GitHub (Sep 8, 2025):

+1

<!-- gh-comment-id:3266047748 --> @KhalilMeziane commented on GitHub (Sep 8, 2025): +1
Author
Owner

@Kinfe123 commented on GitHub (Sep 8, 2025):

Can you please provide me a minimal repro of this issue ?

<!-- gh-comment-id:3266571574 --> @Kinfe123 commented on GitHub (Sep 8, 2025): Can you please provide me a minimal repro of this issue ?
Author
Owner

@hamdi-islam commented on GitHub (Sep 8, 2025):

@Kinfe123 the problem was that i was using raw token that comes from the database. At first i was using auth.api.signInEmail and pass the body, i had to add asResponse: true so the response includes signed cookies in the header which i then used instead of raw token. the documentation doesn't include such a critical information

<!-- gh-comment-id:3267435309 --> @hamdi-islam commented on GitHub (Sep 8, 2025): @Kinfe123 the problem was that i was using raw token that comes from the database. At first i was using `auth.api.signInEmail` and pass the body, i had to add `asResponse: true` so the response includes signed cookies in the header which i then used instead of raw token. the documentation doesn't include such a critical information
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9967