better-auth
Version:
The most comprehensive authentication framework for TypeScript.
424 lines (422 loc) • 15.9 kB
JavaScript
import { getDate } from "../../utils/date.mjs";
import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
import "../../db/index.mjs";
import { symmetricDecodeJWT, verifyJWT } from "../../crypto/jwt.mjs";
import "../../crypto/index.mjs";
import { getChunkedCookie, getSessionQuerySchema } from "../../cookies/session-store.mjs";
import { deleteSessionCookie, setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import * as z from "zod";
import { APIError } from "better-call";
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
import { base64Url } from "@better-auth/utils/base64";
import { binary } from "@better-auth/utils/binary";
import { createHMAC } from "@better-auth/utils/hmac";
//#region src/api/routes/session.ts
const getSession = () => createAuthEndpoint("/get-session", {
method: "GET",
operationId: "getSession",
query: getSessionQuerySchema,
requireHeaders: true,
metadata: { openapi: {
operationId: "getSession",
description: "Get the current session",
responses: { "200": {
description: "Success",
content: { "application/json": { schema: {
type: "object",
nullable: true,
properties: {
session: { $ref: "#/components/schemas/Session" },
user: { $ref: "#/components/schemas/User" }
},
required: ["session", "user"]
} } }
} }
} }
}, async (ctx) => {
try {
const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
if (!sessionCookieToken) return null;
const sessionDataCookie = getChunkedCookie(ctx, ctx.context.authCookies.sessionData.name);
let sessionDataPayload = null;
if (sessionDataCookie) {
const strategy = ctx.context.options.session?.cookieCache?.strategy || "compact";
if (strategy === "jwe") {
const payload = await symmetricDecodeJWT(sessionDataCookie, ctx.context.secret, "better-auth-session");
if (payload && payload.session && payload.user) sessionDataPayload = {
session: {
session: payload.session,
user: payload.user,
updatedAt: payload.updatedAt,
version: payload.version
},
expiresAt: payload.exp ? payload.exp * 1e3 : Date.now()
};
else {
const dataCookie = ctx.context.authCookies.sessionData.name;
ctx.setCookie(dataCookie, "", { maxAge: 0 });
return ctx.json(null);
}
} else if (strategy === "jwt") {
const payload = await verifyJWT(sessionDataCookie, ctx.context.secret);
if (payload && payload.session && payload.user) sessionDataPayload = {
session: {
session: payload.session,
user: payload.user,
updatedAt: payload.updatedAt,
version: payload.version
},
expiresAt: payload.exp ? payload.exp * 1e3 : Date.now()
};
else {
const dataCookie = ctx.context.authCookies.sessionData.name;
ctx.setCookie(dataCookie, "", { maxAge: 0 });
return ctx.json(null);
}
} else {
const parsed = safeJSONParse(binary.decode(base64Url.decode(sessionDataCookie)));
if (parsed) if (await createHMAC("SHA-256", "base64urlnopad").verify(ctx.context.secret, JSON.stringify({
...parsed.session,
expiresAt: parsed.expiresAt
}), parsed.signature)) sessionDataPayload = parsed;
else {
const dataCookie = ctx.context.authCookies.sessionData.name;
ctx.setCookie(dataCookie, "", { maxAge: 0 });
return ctx.json(null);
}
}
}
const dontRememberMe = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
/**
* If session data is present in the cookie, check if it should be used or refreshed
*/
if (sessionDataPayload?.session && ctx.context.options.session?.cookieCache?.enabled && !ctx.query?.disableCookieCache) {
const session$1 = sessionDataPayload.session;
const versionConfig = ctx.context.options.session?.cookieCache?.version;
let expectedVersion = "1";
if (versionConfig) {
if (typeof versionConfig === "string") expectedVersion = versionConfig;
else if (typeof versionConfig === "function") {
const result = versionConfig(session$1.session, session$1.user);
expectedVersion = result instanceof Promise ? await result : result;
}
}
if ((session$1.version || "1") !== expectedVersion) {
const dataCookie = ctx.context.authCookies.sessionData.name;
ctx.setCookie(dataCookie, "", { maxAge: 0 });
} else {
const cachedSessionExpiresAt = new Date(session$1.session.expiresAt);
if (sessionDataPayload.expiresAt < Date.now() || cachedSessionExpiresAt < /* @__PURE__ */ new Date()) {
const dataCookie = ctx.context.authCookies.sessionData.name;
ctx.setCookie(dataCookie, "", { maxAge: 0 });
} else {
const cookieRefreshCache = ctx.context.sessionConfig.cookieRefreshCache;
if (cookieRefreshCache === false) {
ctx.context.session = session$1;
return ctx.json({
session: session$1.session,
user: session$1.user
});
}
if (sessionDataPayload.expiresAt - Date.now() < cookieRefreshCache.updateAge * 1e3) {
const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec");
const refreshedSession = {
session: {
...session$1.session,
expiresAt: newExpiresAt
},
user: session$1.user,
updatedAt: Date.now()
};
await setCookieCache(ctx, refreshedSession, false);
const parsedRefreshedSession = parseSessionOutput(ctx.context.options, {
...refreshedSession.session,
expiresAt: new Date(refreshedSession.session.expiresAt),
createdAt: new Date(refreshedSession.session.createdAt),
updatedAt: new Date(refreshedSession.session.updatedAt)
});
const parsedRefreshedUser = parseUserOutput(ctx.context.options, {
...refreshedSession.user,
createdAt: new Date(refreshedSession.user.createdAt),
updatedAt: new Date(refreshedSession.user.updatedAt)
});
ctx.context.session = {
session: parsedRefreshedSession,
user: parsedRefreshedUser
};
return ctx.json({
session: parsedRefreshedSession,
user: parsedRefreshedUser
});
}
const parsedSession = parseSessionOutput(ctx.context.options, {
...session$1.session,
expiresAt: new Date(session$1.session.expiresAt),
createdAt: new Date(session$1.session.createdAt),
updatedAt: new Date(session$1.session.updatedAt)
});
const parsedUser = parseUserOutput(ctx.context.options, {
...session$1.user,
createdAt: new Date(session$1.user.createdAt),
updatedAt: new Date(session$1.user.updatedAt)
});
ctx.context.session = {
session: parsedSession,
user: parsedUser
};
return ctx.json({
session: parsedSession,
user: parsedUser
});
}
}
}
const session = await ctx.context.internalAdapter.findSession(sessionCookieToken);
ctx.context.session = session;
if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) {
deleteSessionCookie(ctx);
if (session)
/**
* if session expired clean up the session
*/
await ctx.context.internalAdapter.deleteSession(session.session.token);
return ctx.json(null);
}
/**
* We don't need to update the session if the user doesn't want to be remembered
* or if the session refresh is disabled
*/
if (dontRememberMe || ctx.query?.disableRefresh) {
const parsedSession = parseSessionOutput(ctx.context.options, session.session);
const parsedUser = parseUserOutput(ctx.context.options, session.user);
return ctx.json({
session: parsedSession,
user: parsedUser
});
}
const expiresIn = ctx.context.sessionConfig.expiresIn;
const updateAge = ctx.context.sessionConfig.updateAge;
if (session.session.expiresAt.valueOf() - expiresIn * 1e3 + updateAge * 1e3 <= Date.now() && (!ctx.query?.disableRefresh || !ctx.context.options.session?.disableSessionRefresh)) {
const updatedSession = await ctx.context.internalAdapter.updateSession(session.session.token, {
expiresAt: getDate(ctx.context.sessionConfig.expiresIn, "sec"),
updatedAt: /* @__PURE__ */ new Date()
});
if (!updatedSession) {
/**
* Handle case where session update fails (e.g., concurrent deletion)
*/
deleteSessionCookie(ctx);
return ctx.json(null, { status: 401 });
}
const maxAge = (updatedSession.expiresAt.valueOf() - Date.now()) / 1e3;
await setSessionCookie(ctx, {
session: updatedSession,
user: session.user
}, false, { maxAge });
const parsedUpdatedSession = parseSessionOutput(ctx.context.options, updatedSession);
const parsedUser = parseUserOutput(ctx.context.options, session.user);
return ctx.json({
session: parsedUpdatedSession,
user: parsedUser
});
}
await setCookieCache(ctx, session, !!dontRememberMe);
return ctx.json(session);
} catch (error) {
ctx.context.logger.error("INTERNAL_SERVER_ERROR", error);
throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION });
}
});
const getSessionFromCtx = async (ctx, config) => {
if (ctx.context.session) return ctx.context.session;
const session = await getSession()({
...ctx,
asResponse: false,
headers: ctx.headers,
returnHeaders: false,
returnStatus: false,
query: {
...config,
...ctx.query
}
}).catch((e) => {
return null;
});
ctx.context.session = session;
return session;
};
/**
* The middleware forces the endpoint to require a valid session.
*/
const sessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) throw new APIError("UNAUTHORIZED");
return { session };
});
/**
* This middleware forces the endpoint to require a valid session and ignores cookie cache.
* This should be used for sensitive operations like password changes, account deletion, etc.
* to ensure that revoked sessions cannot be used even if they're still cached in cookies.
*/
const sensitiveSessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
if (!session?.session) throw new APIError("UNAUTHORIZED");
return { session };
});
/**
* This middleware allows you to call the endpoint on the client if session is valid.
* However, if called on the server, no session is required.
*/
const requestOnlySessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED");
return { session };
});
/**
* This middleware forces the endpoint to require a valid session,
* as well as making sure the session is fresh before proceeding.
*
* Session freshness check will be skipped if the session config's freshAge
* is set to 0
*/
const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) throw new APIError("UNAUTHORIZED");
if (ctx.context.sessionConfig.freshAge === 0) return { session };
const freshAge = ctx.context.sessionConfig.freshAge;
const lastUpdated = new Date(session.session.updatedAt || session.session.createdAt).getTime();
if (!(Date.now() - lastUpdated < freshAge * 1e3)) throw new APIError("FORBIDDEN", { message: "Session is not fresh" });
return { session };
});
/**
* user active sessions list
*/
const listSessions = () => createAuthEndpoint("/list-sessions", {
method: "GET",
operationId: "listUserSessions",
use: [sessionMiddleware],
requireHeaders: true,
metadata: { openapi: {
operationId: "listUserSessions",
description: "List all active sessions for the user",
responses: { "200": {
description: "Success",
content: { "application/json": { schema: {
type: "array",
items: { $ref: "#/components/schemas/Session" }
} } }
} }
} }
}, async (ctx) => {
try {
const activeSessions = (await ctx.context.internalAdapter.listSessions(ctx.context.session.user.id)).filter((session) => {
return session.expiresAt > /* @__PURE__ */ new Date();
});
return ctx.json(activeSessions);
} catch (e) {
ctx.context.logger.error(e);
throw ctx.error("INTERNAL_SERVER_ERROR");
}
});
/**
* revoke a single session
*/
const revokeSession = createAuthEndpoint("/revoke-session", {
method: "POST",
body: z.object({ token: z.string().meta({ description: "The token to revoke" }) }),
use: [sensitiveSessionMiddleware],
requireHeaders: true,
metadata: { openapi: {
description: "Revoke a single session",
requestBody: { content: { "application/json": { schema: {
type: "object",
properties: { token: {
type: "string",
description: "The token to revoke"
} },
required: ["token"]
} } } },
responses: { "200": {
description: "Success",
content: { "application/json": { schema: {
type: "object",
properties: { status: {
type: "boolean",
description: "Indicates if the session was revoked successfully"
} },
required: ["status"]
} } }
} }
} }
}, async (ctx) => {
const token = ctx.body.token;
if ((await ctx.context.internalAdapter.findSession(token))?.session.userId === ctx.context.session.user.id) try {
await ctx.context.internalAdapter.deleteSession(token);
} catch (error) {
ctx.context.logger.error(error && typeof error === "object" && "name" in error ? error.name : "", error);
throw new APIError("INTERNAL_SERVER_ERROR");
}
return ctx.json({ status: true });
});
/**
* revoke all user sessions
*/
const revokeSessions = createAuthEndpoint("/revoke-sessions", {
method: "POST",
use: [sensitiveSessionMiddleware],
requireHeaders: true,
metadata: { openapi: {
description: "Revoke all sessions for the user",
responses: { "200": {
description: "Success",
content: { "application/json": { schema: {
type: "object",
properties: { status: {
type: "boolean",
description: "Indicates if all sessions were revoked successfully"
} },
required: ["status"]
} } }
} }
} }
}, async (ctx) => {
try {
await ctx.context.internalAdapter.deleteSessions(ctx.context.session.user.id);
} catch (error) {
ctx.context.logger.error(error && typeof error === "object" && "name" in error ? error.name : "", error);
throw new APIError("INTERNAL_SERVER_ERROR");
}
return ctx.json({ status: true });
});
const revokeOtherSessions = createAuthEndpoint("/revoke-other-sessions", {
method: "POST",
requireHeaders: true,
use: [sensitiveSessionMiddleware],
metadata: { openapi: {
description: "Revoke all other sessions for the user except the current one",
responses: { "200": {
description: "Success",
content: { "application/json": { schema: {
type: "object",
properties: { status: {
type: "boolean",
description: "Indicates if all other sessions were revoked successfully"
} },
required: ["status"]
} } }
} }
} }
}, async (ctx) => {
const session = ctx.context.session;
if (!session.user) throw new APIError("UNAUTHORIZED");
const otherSessions = (await ctx.context.internalAdapter.listSessions(session.user.id)).filter((session$1) => {
return session$1.expiresAt > /* @__PURE__ */ new Date();
}).filter((session$1) => session$1.token !== ctx.context.session.session.token);
await Promise.all(otherSessions.map((session$1) => ctx.context.internalAdapter.deleteSession(session$1.token)));
return ctx.json({ status: true });
});
//#endregion
export { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
//# sourceMappingURL=session.mjs.map