UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

151 lines (149 loc) • 7.33 kB
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs"; import { deleteSessionCookie, parseCookies, setSessionCookie } from "../../cookies/index.mjs"; import { sessionMiddleware } from "../../api/routes/session.mjs"; import { APIError } from "../../api/index.mjs"; import { defineErrorCodes } from "@better-auth/core/utils"; import * as z from "zod"; import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api"; //#region src/plugins/multi-session/index.ts const ERROR_CODES = defineErrorCodes({ INVALID_SESSION_TOKEN: "Invalid session token" }); const setActiveSessionBodySchema = z.object({ sessionToken: z.string().meta({ description: "The session token to set as active" }) }); const revokeDeviceSessionBodySchema = z.object({ sessionToken: z.string().meta({ description: "The session token to revoke" }) }); const multiSession = (options) => { const opts = { maximumSessions: 5, ...options }; const isMultiSessionCookie = (key) => key.includes("_multi-"); return { id: "multi-session", endpoints: { listDeviceSessions: createAuthEndpoint("/multi-session/list-device-sessions", { method: "GET", requireHeaders: true }, async (ctx) => { const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return ctx.json([]); const cookies = Object.fromEntries(parseCookies(cookieHeader)); const sessionTokens = (await Promise.all(Object.entries(cookies).filter(([key]) => isMultiSessionCookie(key)).map(async ([key]) => await ctx.getSignedCookie(key, ctx.context.secret)))).filter((v) => typeof v === "string"); if (!sessionTokens.length) return ctx.json([]); const uniqueUserSessions = (await ctx.context.internalAdapter.findSessions(sessionTokens)).filter((session) => session && session.session.expiresAt > /* @__PURE__ */ new Date()).reduce((acc, session) => { if (!acc.find((s) => s.user.id === session.user.id)) acc.push(session); return acc; }, []); return ctx.json(uniqueUserSessions); }), setActiveSession: createAuthEndpoint("/multi-session/set-active", { method: "POST", body: setActiveSessionBodySchema, requireHeaders: true, use: [sessionMiddleware], metadata: { openapi: { description: "Set the active session", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { session: { $ref: "#/components/schemas/Session" } } } } } } } } } }, async (ctx) => { const sessionToken = ctx.body.sessionToken; const multiSessionCookieName = `${ctx.context.authCookies.sessionToken.name}_multi-${sessionToken.toLowerCase()}`; if (!await ctx.getSignedCookie(multiSessionCookieName, ctx.context.secret)) throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.INVALID_SESSION_TOKEN }); const session = await ctx.context.internalAdapter.findSession(sessionToken); if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) { ctx.setCookie(multiSessionCookieName, "", { ...ctx.context.authCookies.sessionToken.options, maxAge: 0 }); throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.INVALID_SESSION_TOKEN }); } await setSessionCookie(ctx, session); return ctx.json(session); }), revokeDeviceSession: createAuthEndpoint("/multi-session/revoke", { method: "POST", body: revokeDeviceSessionBodySchema, requireHeaders: true, use: [sessionMiddleware], metadata: { openapi: { description: "Revoke a device session", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" } } } } } } } } } }, async (ctx) => { const sessionToken = ctx.body.sessionToken; const multiSessionCookieName = `${ctx.context.authCookies.sessionToken.name}_multi-${sessionToken.toLowerCase()}`; if (!await ctx.getSignedCookie(multiSessionCookieName, ctx.context.secret)) throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.INVALID_SESSION_TOKEN }); await ctx.context.internalAdapter.deleteSession(sessionToken); ctx.setCookie(multiSessionCookieName, "", { ...ctx.context.authCookies.sessionToken.options, maxAge: 0 }); if (!(ctx.context.session?.session.token === sessionToken)) return ctx.json({ status: true }); const cookieHeader = ctx.headers?.get("cookie"); if (cookieHeader) { const cookies = Object.fromEntries(parseCookies(cookieHeader)); const sessionTokens = (await Promise.all(Object.entries(cookies).filter(([key]) => isMultiSessionCookie(key)).map(async ([key]) => await ctx.getSignedCookie(key, ctx.context.secret)))).filter((v) => typeof v === "string"); const internalAdapter = ctx.context.internalAdapter; if (sessionTokens.length > 0) { const validSessions = (await internalAdapter.findSessions(sessionTokens)).filter((session) => session && session.session.expiresAt > /* @__PURE__ */ new Date()); if (validSessions.length > 0) { const nextSession = validSessions[0]; await setSessionCookie(ctx, nextSession); } else deleteSessionCookie(ctx); } else deleteSessionCookie(ctx); } else deleteSessionCookie(ctx); return ctx.json({ status: true }); }) }, hooks: { after: [{ matcher: () => true, handler: createAuthMiddleware(async (ctx) => { const cookieString = ctx.context.responseHeaders?.get("set-cookie"); if (!cookieString) return; const setCookies = parseSetCookieHeader(cookieString); const sessionCookieConfig = ctx.context.authCookies.sessionToken; const sessionToken = ctx.context.newSession?.session.token; if (!sessionToken) return; const cookies = parseCookies(ctx.headers?.get("cookie") || ""); const cookieName = `${sessionCookieConfig.name}_multi-${sessionToken.toLowerCase()}`; if (setCookies.get(cookieName) || cookies.get(cookieName)) return; if (Object.keys(Object.fromEntries(cookies)).filter(isMultiSessionCookie).length + (cookieString.includes("session_token") ? 1 : 0) >= opts.maximumSessions) return; await ctx.setSignedCookie(cookieName, sessionToken, ctx.context.secret, sessionCookieConfig.options); }) }, { matcher: (context) => context.path === "/sign-out", handler: createAuthMiddleware(async (ctx) => { const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return; const cookies = Object.fromEntries(parseCookies(cookieHeader)); const multiSessionKeys = Object.keys(cookies).filter((key) => isMultiSessionCookie(key)); const verifiedTokens = (await Promise.all(multiSessionKeys.map(async (key) => { const verifiedToken = await ctx.getSignedCookie(key, ctx.context.secret); if (verifiedToken) { ctx.setCookie(key.toLowerCase().replace("__secure-", "__Secure-"), "", { ...ctx.context.authCookies.sessionToken.options, maxAge: 0 }); return verifiedToken; } return null; }))).filter((v) => typeof v === "string"); if (verifiedTokens.length > 0) await ctx.context.internalAdapter.deleteSessions(verifiedTokens); }) }] }, options, $ERROR_CODES: ERROR_CODES }; }; //#endregion export { multiSession }; //# sourceMappingURL=index.mjs.map