BA
Better Authβ€’3w ago
Felix

Session cookie not working in production deployment

Hello everyone, Unfortunately, my authentication only works on localhost and not in production or the deployed development environment. We have multiple frontends. Central authentication runs via an Express API (api-ts.playin.gg). The test frontend for authentication is auth.playin.gg. Signing in via Google works; you are redirected to /protected and the cookie is set, but the frontend apparently can't retrieve the sessions. What could be the reason for this? Can anyone help me here? I'm also open to any suggestions for improvement πŸ™‚ auth.ts (Express API)
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
Frontend (auth.playin.gg/protected, NextJS)
import AccountList from "@/components/settings/account-list";
import LogoutButton from "@/components/auth/logout-button";
import MFASection from "@/components/settings/mfa-section";
import PasskeyList from "@/components/settings/passkey-list";
import SessionList from "@/components/settings/session-list";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

const ProtectedSSRPage = async () => {
const { data: session } = await auth.getSession({
fetchOptions: {
headers: await headers(),
},
});

if (!session) {
return redirect("/auth/login");
}

return(...)
}

export default ProtectedSSRPage;
import AccountList from "@/components/settings/account-list";
import LogoutButton from "@/components/auth/logout-button";
import MFASection from "@/components/settings/mfa-section";
import PasskeyList from "@/components/settings/passkey-list";
import SessionList from "@/components/settings/session-list";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

const ProtectedSSRPage = async () => {
const { data: session } = await auth.getSession({
fetchOptions: {
headers: await headers(),
},
});

if (!session) {
return redirect("/auth/login");
}

return(...)
}

export default ProtectedSSRPage;
No description
102 Replies
KiNFiSH
KiNFiSHβ€’3w ago
you mean session is returning null ? like can you check up on the cookie exists with the headers
Felix
FelixOPβ€’3w ago
Yes, if I log the headers, the cookie is present
const heads = await headers();
console.log("HEADERS", heads.get("cookie"));
const heads = await headers();
console.log("HEADERS", heads.get("cookie"));
OUTPUT: HEADERS __Secure-playingg_gguardian.session_token=xxx; ... SESSION null
KiNFiSH
KiNFiSHβ€’3w ago
const session = await auth.api.getSession({
headers: await headers(),
})
const session = await auth.api.getSession({
headers: await headers(),
})
can you do the fetching with the internal api like this
Felix
FelixOPβ€’3w ago
No, unfortunately not. I've separated the frontend and backend. The internal API is therefore in the Express API. I only have the client in NextJS. This prevents access to the internal API. Fetching with the headers or useSession worked on localhost.
import { passkeyClient, twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";

export const auth = createAuthClient({
baseURL: "https://api-ts.playin.gg",
plugins: [passkeyClient(), twoFactorClient()],
});
import { passkeyClient, twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";

export const auth = createAuthClient({
baseURL: "https://api-ts.playin.gg",
plugins: [passkeyClient(), twoFactorClient()],
});
KiNFiSH
KiNFiSHβ€’3w ago
Oh if u r using express Ig you should be fine Can you manually pass the cookie to headers options itself
Felix
FelixOPβ€’3w ago
Hmm, you mean read the specific cookie and then pass it in the fetchoptions as headers: { cookie: ... } I don't know if this is intended, and it's also a bit cumbersome. I don't really understand what's wrong with it πŸ˜… I also didn't really find any documentation deployment specific
KiNFiSH
KiNFiSHβ€’3w ago
can u pls share the root page and initialization of your experess app ?
Felix
FelixOPβ€’3w ago
Sure EXPRESS src/index.ts
import v2Routes from "@/routes/v2.routes";
import applyRelations from "@/utils/relations";
import { toNodeHandler } from "better-auth/node";
import bodyParser from "body-parser";
import compression from "compression";
import cookieParser from "cookie-parser";
import cors from "cors";
import dotenv from "dotenv";
import express, { Express, Request, Response } from "express";
import { auth } from "./auth";

dotenv.config({ path: "./.env" });

const app: Express = express();
const baseUrl = process.env.BASE_URL || "http://localhost";
const port = process.env.PORT || 3000;

// CORS
app.use(
cors({
origin: ["https://auth.playin.gg", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "http://localhost:3000"], // @WARN: http:localhost:3000 only for development purposes
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
})
);

// Better-Auth
app.all("/api/auth/*", toNodeHandler(auth));

// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Sequelize - Apply relations
applyRelations();

// Routes (v2)
app.use("/v2", v2Routes);

app.listen(port, () => {
console.log(`🟒 [PlayinGG API]: Running at ${baseUrl}:${port}`);
});
import v2Routes from "@/routes/v2.routes";
import applyRelations from "@/utils/relations";
import { toNodeHandler } from "better-auth/node";
import bodyParser from "body-parser";
import compression from "compression";
import cookieParser from "cookie-parser";
import cors from "cors";
import dotenv from "dotenv";
import express, { Express, Request, Response } from "express";
import { auth } from "./auth";

dotenv.config({ path: "./.env" });

const app: Express = express();
const baseUrl = process.env.BASE_URL || "http://localhost";
const port = process.env.PORT || 3000;

// CORS
app.use(
cors({
origin: ["https://auth.playin.gg", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "http://localhost:3000"], // @WARN: http:localhost:3000 only for development purposes
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
})
);

// Better-Auth
app.all("/api/auth/*", toNodeHandler(auth));

// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Sequelize - Apply relations
applyRelations();

// Routes (v2)
app.use("/v2", v2Routes);

app.listen(port, () => {
console.log(`🟒 [PlayinGG API]: Running at ${baseUrl}:${port}`);
});
EXPRESS src/auth.ts
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
router.get("/", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});

if (!session) {
res.status(401).json({ error: "Unauthorized" });
return;
}

res.json(session);
return;
});
router.get("/", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});

if (!session) {
res.status(401).json({ error: "Unauthorized" });
return;
}

res.json(session);
return;
});
I created a route in the Express API as a test. Authentication with the cookie set trough the frontend sign in works. It seems that the problem is only in the frontend in NextJS, where the cookie isn't recognized via the useSession or getSession hook, even though it exists in the await headers.
KiNFiSH
KiNFiSHβ€’3w ago
What is your nextjs version ?
Felix
FelixOPβ€’3w ago
Running latest packages "next": "15.3.0", "react": "^19.1.0",
Felix
FelixOPβ€’3w ago
Also passkeys are not working in production for me, localhost works fine 🫠
onClick={async () => {
await auth.signIn.passkey({
fetchOptions: {
onSuccess: () => {
router.push("/protected");
},
onError: (error) => {
console.log(error);
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
});
}}
onClick={async () => {
await auth.signIn.passkey({
fetchOptions: {
onSuccess: () => {
router.push("/protected");
},
onError: (error) => {
console.log(error);
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
});
}}
# SERVER_ERROR: MissingWebCrypto: An instance of the Crypto API could not be located
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:35:23
7|playingg-api-ts | at new Promise (<anonymous>)
7|playingg-api-ts | at getWebCrypto (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:21:23)
7|playingg-api-ts | at Object.getRandomValues (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getRandomValues.js:11:64)
7|playingg-api-ts | at generateChallenge (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/generateChallenge.js:19:32)
7|playingg-api-ts | at Object.generateAuthenticationOptions (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/authentication/generateAuthenticationOptions.js:19:94)
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/plugins/passkey/index.cjs:369:41
7|playingg-api-ts | at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
7|playingg-api-ts | at async internalHandler (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-call/dist/index.cjs:606:22)
7|playingg-api-ts | at async api.<computed> (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/api/index.cjs:484:22)
# SERVER_ERROR: MissingWebCrypto: An instance of the Crypto API could not be located
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:35:23
7|playingg-api-ts | at new Promise (<anonymous>)
7|playingg-api-ts | at getWebCrypto (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:21:23)
7|playingg-api-ts | at Object.getRandomValues (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getRandomValues.js:11:64)
7|playingg-api-ts | at generateChallenge (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/generateChallenge.js:19:32)
7|playingg-api-ts | at Object.generateAuthenticationOptions (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/authentication/generateAuthenticationOptions.js:19:94)
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/plugins/passkey/index.cjs:369:41
7|playingg-api-ts | at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
7|playingg-api-ts | at async internalHandler (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-call/dist/index.cjs:606:22)
7|playingg-api-ts | at async api.<computed> (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/api/index.cjs:484:22)
No description
bekacru
bekacruβ€’3w ago
if you have crosssubdomain cookie setup remove defaultCookieAttributes
Felix
FelixOPβ€’3w ago
I tried it. Unfortunately, it doesn't change the existing problems with retrieving the session or passkey sign in
bekacru
bekacruβ€’3w ago
after the user authenticated on auth.playing.gg where do they get redirected to?
Felix
FelixOPβ€’3w ago
At the moment, I'm using auth.playin.gg as a pure development environment. It redirects you to /protected, which works, both on localhost and deployed. But in "production" it redirects you back to /auth/login, because it doesn't recognize the session, as described above. Headers are present.
<Button
onClick={() =>
auth.signIn.social({
provider: "google",
callbackURL: redirectURL ? `${window.location.origin}${redirectURL}` : `${window.location.origin}/protected`,
fetchOptions: {
onError: (error) => {
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
})
}
className="flex w-full flex-row items-center justify-center space-x-2 rounded-full border border-playingg-gray-700 bg-transparent px-5 py-2 text-base text-white"
>
<Image src="/images/providers/GOOGLE_LOGO.svg" width={20} height={20} alt="Google Icon" />
<span>Sign in with Google</span>
</Button>
<Button
onClick={() =>
auth.signIn.social({
provider: "google",
callbackURL: redirectURL ? `${window.location.origin}${redirectURL}` : `${window.location.origin}/protected`,
fetchOptions: {
onError: (error) => {
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
})
}
className="flex w-full flex-row items-center justify-center space-x-2 rounded-full border border-playingg-gray-700 bg-transparent px-5 py-2 text-base text-white"
>
<Image src="/images/providers/GOOGLE_LOGO.svg" width={20} height={20} alt="Google Icon" />
<span>Sign in with Google</span>
</Button>
We will have several frontends later, as well as an Expo App
bekacru
bekacruβ€’3w ago
could you check if the cookie is being properly set in the browser and if it's sent with the request in the middleware?
Felix
FelixOPβ€’3w ago
We actually don't use auth middleware. And we would like to migrate from Next-Auth. Since only a few pages are fully protected, we would query and redirect the session accordingly in the layouts/pages. Or we would only check whether the user is logged in when certain features are clicked. So far, we've done this with Next-Auth's useSession/getServerSideSession. But it would be the same process as implemented above on the ProtectedSSRPage. As far as I understand, the cookies are definitely present, but not recognized by useSession / getSession on the authClient. I only have the client, since the auth.ts is on Express. Here I had logged the cookies
bekacru
bekacruβ€’3w ago
just to be sure remove the cookie prefix as well and on prod inforce useSecureCookies by setting it to true in the config
Felix
FelixOPβ€’3w ago
Hmm, I tried that. It's also deployed in auth.playin.gg. But it still doesn't work. I don't understand it somehow
appName: "PlayinGG",
advanced: {
// cookiePrefix: "playingg",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
/* defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
}, */
useSecureCookies: true,
},
appName: "PlayinGG",
advanced: {
// cookiePrefix: "playingg",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
/* defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
}, */
useSecureCookies: true,
},
daanish
daanishβ€’3w ago
faced the same problem in production it sets the cookie in browser after login but after redirect, it clear the the cookie in browser which redirect back to login page i found the problem is in middleware because it uses fetch this is the error i get in console
Session verification error: Error: fetch failed
client:start: at context.fetch (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/context.js:303:38)
client:start: at tv (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:39895)
client:start: at async tm (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:41964)
client:start: at async tP (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:42978)
client:start: at async handler (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:43639)
client:start: at async /home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:32351
client:start: at async e4 (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:29211)
client:start: at async /home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:122:26
client:start: at async runWithTaggedErrors (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:119:9)
client:start: at async NextNodeServer.runMiddleware (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/next-server.js:1008:24) {
client:start:
client:start: }
server:start: Listening at http://localhost:4000
server:start: Redis Client Connected
media:start: Listening at http://localhost:5000
Session verification error: Error: fetch failed
client:start: at context.fetch (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/context.js:303:38)
client:start: at tv (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:39895)
client:start: at async tm (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:41964)
client:start: at async tP (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:42978)
client:start: at async handler (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:43639)
client:start: at async /home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:32351
client:start: at async e4 (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:29211)
client:start: at async /home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:122:26
client:start: at async runWithTaggedErrors (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:119:9)
client:start: at async NextNodeServer.runMiddleware (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/next-server.js:1008:24) {
client:start:
client:start: }
server:start: Listening at http://localhost:4000
server:start: Redis Client Connected
media:start: Listening at http://localhost:5000
i got this error when i run in prod in local machine i have a backend in expressjs and nextjs as frontend @bekacru have any solution i have detected is problem is useSecureCookie in prod i deployed in aws with nginix it doesn't work
Felix
FelixOPβ€’3w ago
Hmm, I'm also using NGINX as a reverse proxy. I was wondering if that could be the problem, but according to the logs, the cookies are present on the backend. I've already played around with the configuration a bit, but so far, I've never had any problems with cookies... but the better-auth session hooks in my deployed frontend still don't want to recognize the session, even though the cookie is present 😦 I still haven't found a solution.
Fall
Fallβ€’3w ago
Same issue, the cookie is set when sign-in but get-session call remove it
Felix
FelixOPβ€’3w ago
It's actually not even removed for me. I still have the cookie, but it's just not recognized.
Fall
Fallβ€’3w ago
the response of get-session call, why return 3 null cookies?
No description
bekacru
bekacruβ€’3w ago
what is your baseURL or BETTER_AUTH_URL set to? it's trying to unset cookies
Fall
Fallβ€’3w ago
yes, the request does send a valid cookie. I suspect the issue might be related to the session database schema
bekacru
bekacruβ€’3w ago
is this on prod or local?
Fall
Fallβ€’3w ago
prod
bekacru
bekacruβ€’3w ago
what is your base url set to?
Fall
Fallβ€’3w ago
my API runs on a subdomain, api.osler, while my Next.js app runs on the base domain import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import * as schema from '@/db/schema';
import { db } from '@/db'; import { env } from '@/env'; export const auth = betterAuth({ secret: env.BETTER_AUTH_SECRET, baseUrl: env.BETTER_AUTH_URL, database: drizzleAdapter(db, { provider: 'pg', schema: schema, }), user: { modelName: 'doctor', }, account: { fields: { userId: 'doctorId', } }, session: { fields: { userId: 'doctorId', }, storeSessionInDatabase: true, }, emailAndPassword: { enabled: true, async sendResetPassword(data, request) { console.log(data, request); }, }, advanced: { crossSubDomainCookies: { enabled: true, domain: '.osler.app', }, useSecureCookies: true, }, appName: 'Osler', trustedOrigins: ['http://localhost:3001', 'https://osler.app'], socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, } } });
bekacru
bekacruβ€’3w ago
first baseUrl should be baseURL
Fall
Fallβ€’3w ago
oh 😳 I miss type safety right now
bekacru
bekacruβ€’3w ago
will be added on the next release. Extra values aren't being validated by ts cause of generics 🫑
Felix
FelixOPβ€’3w ago
Mine is BETTER_AUTH_URL=https://api-ts.playin.gg Frontend: https://auth.playin.gg But @Fall don't seem to have exact the same problem
PlayinGG | Authentication
PlayinGG Auth Service
bekacru
bekacruβ€’3w ago
if you're not incase enforcing useSecureCookies on prod do that when it's behind reverse proxy, it may not enable it by default
Felix
FelixOPβ€’3w ago
I had already tried this on your suggestion, unfortunately it didn't change anything
bekacru
bekacruβ€’3w ago
so the issue is after sign in and properly seting cookies getSession returns null, right?
Fall
Fallβ€’3w ago
The cookie clearing during the get-session call continues, and the baseURL mismatch wasn’t the problem. It’s the same issue as @daanish
daanish
daanishβ€’3w ago
yes
Felix
FelixOPβ€’3w ago
Yeah, exactly. The cookie is set correctly for me, but getSession and useSession only return null in PROD. On localhost, everything works perfectly. After signing in, you're redirected to /protected, but then, of course, you're redirected directly back to signin because the session is null.
No description
daanish
daanishβ€’3w ago
same after redirect cookie is being cleared
bekacru
bekacruβ€’3w ago
just to make sure, are you all in the latest version of the library?
Fall
Fallβ€’3w ago
i'm using: "better-auth": "^1.2.5",
daanish
daanishβ€’3w ago
im using 1.2.2
Felix
FelixOPβ€’3w ago
Express Backend "better-auth": "^1.2.6", NextJS Frontend "better-auth": "^1.2.6", "next": "15.3.0", "react": "^19.1.0", Upgraded this morning hoping it would fix something, no change
daanish
daanishβ€’3w ago
no change same problem after upgrade
daanish
daanishβ€’3w ago
this is my prod url you would check it https://roro-ai.com/auth/login
Welcome to client
Generated by create-nx-workspace
Felix
FelixOPβ€’3w ago
Felix on Notion
Session returns null in production | Notion
This scenario works on localhost. However, it doesn't work on the deployment https://auth.playin.gg (Backend: https://api-ts.playin.gg).
bekacru
bekacruβ€’3w ago
looking into it. will report back curios, have you tried not making a call from next server environment and just directly from the client to your auth server?
Felix
FelixOPβ€’3w ago
Mmm, you mean from my localhost to the prod API? The cookie is set on .playin.gg, so that wouldn't work.
daanish
daanishβ€’3w ago
problem is because of useSessionCookie at prod is true
bekacru
bekacruβ€’3w ago
no from your prod client/frontend to your prod api in the notion doc, it looks like your making a call from a server component do you mean useSecureCookies?
daanish
daanishβ€’3w ago
yes
bekacru
bekacruβ€’3w ago
is it working now?
daanish
daanishβ€’3w ago
no i tried in local developement with useSecureCookie to true but i doesn't work
bekacru
bekacruβ€’3w ago
in local host secure cookie doesn't work secure cookies only work under https protocol
daanish
daanishβ€’3w ago
yes, its just clears the cookie after login to redirect.this prod link https://roro-ai.com/auth/login
Welcome to client
Generated by create-nx-workspace
Felix
FelixOPβ€’3w ago
Ahh, I see, so it doesn't work on both the server and client side with getSession/useSession. I have a /protected-client page where the session is also null in prod. I added it in Notion.
bekacru
bekacruβ€’3w ago
in your client code
if (!session.data) {
router.push("/auth/login");
return null;
}
if (!session.data) {
router.push("/auth/login");
return null;
}
session is initally always null so this will always redirect regardless. You need to check if session.isPending is also false
Felix
FelixOPβ€’3w ago
Makes sense, thanks. It actually works client-side now, thanks πŸ™‚ It's strange that I've never encountered this issue with localhost before. I'm still having the same issue server-side, though. One more note: I'm also using the same backend with the Expo plugin. The session works there. It's probably a slightly different architecture, though.
bekacru
bekacruβ€’3w ago
can you check on the server if it returns both error and data null?
daanish
daanishβ€’3w ago
for me problem is solved i added crossSubDomain
Felix
FelixOPβ€’3w ago
HEADERS _ga=GA1.1.1653196102.1744302730; _ga_X5724CWZ7F=GS1.1.1744552980.4.1.1744556733.0.0.0; __Secure-playingg.session_token=xxx SESSION DATA null SESSION ERROR { status: 0, statusText: '' }
Fall
Fallβ€’3w ago
@daanish like this? advanced: { crossSubDomainCookies: { enabled: true, domain: '.osler.app', }, useSecureCookies: true, }
daanish
daanishβ€’3w ago
yes my frontend is roro-ai.com and backend is at backend.roro-ai.com but it created four cookie in browser client
Fall
Fallβ€’3w ago
same, my frontend is osler.app and backend is api.osler.app but the problem continue here
daanish
daanishβ€’3w ago
in prod or localhost
Fall
Fallβ€’3w ago
just prod localhost works fine
daanish
daanishβ€’3w ago
is it https enabled
Fall
Fallβ€’3w ago
yes
daanish
daanishβ€’3w ago
did you try auth.api.getSession()
Felix
FelixOPβ€’3w ago
Unfortunately I don't have access to the internal API because I don't use NextJS as a backend, but only as a frontend. My backend is on express
daanish
daanishβ€’3w ago
ok but you are using async in page.tsx try using async in layout.tsx and pass it as props to child don't use in page.tsx and try it
daanish
daanishβ€’3w ago
NEXTJS_NO_ASYNC_PAGE
Ensures that the exported Next.js page component and its transitive dependencies are not asynchronous, as that blocks the rendering of the page.
daanish
daanishβ€’3w ago
also you are using header as await header() but from nextjs 15 header are async to use header try this fetchOptions: (await header())
Felix
FelixOPβ€’3w ago
Hmm, I see your point about centrally fetching the session async in the layout. However, I also do a lot of fetching on the individual pages server-side, which is why I need async there anyway. Your suggestion is best practice, but it doesn't change my problem that the session is null. It doesn't really matter which call I make from the authclient, whether it's retrieving sessions, accounts, etc. everything that is serverside doesn't work with nextjs 😦 fetchOptions: (await header()) => This doesn't work.
daanish
daanishβ€’3w ago
Do you remove async in page.tsx
rinshad
rinshadβ€’2w ago
I am facing similar issue with oauth , i am using google as social provider , getting getsession is null after login using google ,but session data successfully saved in database , email and password works fine , only problem with google , @bekacru any update on this issue
Felix
FelixOPβ€’2w ago
Unfortunately not. To this day, I'm still facing the same problem: the session can only be accessed client-side. I still haven't had any success with NextJS on the server-side 😦
KiNFiSH
KiNFiSHβ€’2w ago
How are you using it on server side for getting the session
Felix
FelixOPβ€’2w ago
Here is my summarized code
rinshad
rinshadβ€’2w ago
GitHub
loginEndpoint for /sign-in/google bypass the sign-in page without r...
Is this suited for github? Yes, this is suited for github To Reproduce Main issue is that i am able to login via email and password but then, when i try google login it just bypass without getting ...
rinshad
rinshadβ€’2w ago
any update ? @bekacru
Note
Noteβ€’7d ago
i am also facing same issue
bekacru
bekacruβ€’7d ago
can you give a better way to implement hooks, tried all the different ways from the documentation.
is this the issue? cookie not working in prod?
Note
Noteβ€’7d ago
I believe it is, when i signin the user and session data are fetched, session token is stored But it redirects me instantly back to the signin, if i try to refresh the page the cookie is deleted
rinshad
rinshadβ€’7d ago
In my case, it's a bit different. Google Sign-In works fine in the local (development) environment. However, in the production environment, it's not working as expected. Google successfully signs in, generates the session on the server, and stores it correctly in the database β€” but it's not returning the session to the client (frontend). As a result, there's no session data on the client side, which triggers a redirect back to the login page.
bekacru
bekacruβ€’7d ago
does it work locally? is your backend and frontend separated?
Note
Noteβ€’7d ago
Yep it does I was hosting on vercel first but didnt work So i switched to netlify and it still doesnt work It used to give a warning in the headers section that: This attempt to set a cookie via a Set-Cookie header was blocked because its Domain attribute was invalid with regards to the current host uri. But sometime it doesnt
bekacru
bekacruβ€’7d ago
can you share your auth config
Note
Noteβ€’7d ago
No description
No description
No description
No description
bekacru
bekacruβ€’7d ago
is your be and fe hosted in subdomain
Note
Noteβ€’7d ago
TaskFlow
TaskFlow is a task management app. It helps you organize your tasks and get things done.
Xirynx
Xirynxβ€’7d ago
We had basically the exact same issue, it worked on Localhost, but did not work on production (except client side). Passing in headers AND cookies fixed the issue. Let me know if that fixes it for you too
No description
Felix
FelixOPβ€’7d ago
Hi, I was hoping, but unfortunately it still doesn't work for me this way. It works on the client side. It doesn't work on the server side. The cookie is present, session is null.
Lapis
Lapisβ€’7d ago
what you can do is on succesfull login in Expo you can call a function like refresh session : const refreshSession = async () => { try { const session = await authClient.getSession(); const sessionToken = session?.data?.session?.token; if (!sessionToken) { await SecureStore.deleteItemAsync('myapp-jwt'); setUser(null); setIsAuthenticated(false); return; } const response = await fetch(${BACKEND}/api/auth/token, { headers: { Authorization: Bearer ${sessionToken}, }, }); const data = await response.json(); if (data.token) { await SecureStore.setItemAsync('myapp-jwt', data.token); } else { await SecureStore.deleteItemAsync('myapp-jwt'); } You can now include this JWT token in your calls to backend. Now NextJs backend can authorize , get payload of user data using the token For ex. you can include token in a call from expo to backend : const token = await SecureStore.getItemAsync('myapp-jwt'); const cookies = authClient.getCookie(); const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json', 'x-client-type': 'mobile', ...(token ? { Authorization: Bearer ${token} } : {}), Cookie: cookies || '', }, credentials: 'include', ...(method === 'POST' && { body: JSON.stringify({ action, params }) }), }; const res = await fetch(url, opts); if (!res) throw new Error('Request failed'); return res.json(); }
Lapis
Lapisβ€’7d ago
now in NextJs middleware using these you can do something like this and create a new header with details of the user : ive used Jose for JWT verification. import { createRemoteJWKSet, jwtVerify } from "jose";
No description
Lapis
Lapisβ€’7d ago
Now you can use these details of session wherever you want in Next js by simply creating a function like this. call this function to get details of the user etc
No description
Xirynx
Xirynxβ€’7d ago
I can see from your code snippet that you changed the default cookie prefix, is that maybe affecting the ability of the auth client to pick up the cookie?
rinshad
rinshadβ€’7d ago
yes front end = next js backend = elysia js ,drizzle,psql
Felix
FelixOPβ€’7d ago
I've already tested that. That's not the problem. It seems more like the hook isn't sending the cookie when it's executed server-side. Although the token is included when I log the headers.
rinshad
rinshadβ€’2d ago
@bekacru any updates on this ?

Did you find this page helpful?