UNPKG

@withstudiocms/auth-kit

Version:

Utilities for managing authentication

153 lines (152 loc) 5.21 kB
import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding"; import { Effect } from "@withstudiocms/effect"; import { SessionError, useSessionError, useSessionErrorPromise } from "../errors.js"; import { defaultSessionConfig, makeExpirationDate, makeSessionId } from "../utils/session.js"; const Session = (config) => Effect.gen(function* () { const { expTime, cookieName, sessionTools } = { ...defaultSessionConfig, ...config }; if (!sessionTools) { return yield* Effect.fail( new SessionError({ cause: "Session tools must be provided in the configuration" }) ); } if (!Number.isFinite(expTime) || expTime <= 0) { return yield* Effect.fail( new SessionError({ cause: "Invalid session config: expTime must be a positive number (ms)" }) ); } if (typeof cookieName !== "string" || cookieName.trim().length === 0) { return yield* Effect.fail( new SessionError({ cause: "Invalid session config: cookieName must be a non-empty string" }) ); } const expTimeHalf = expTime / 2; const defaultSecure = process.env.NODE_ENV === "production"; const generateSessionToken = Effect.fn( "@withstudiocms/AuthKit/modules/session.generateSessionToken" )( () => useSessionError(() => { const data = new Uint8Array(20); const random = crypto.getRandomValues(data); const returnable = encodeBase32LowerCaseNoPadding(random); return returnable; }) ); const createSession = Effect.fn("@withstudiocms/AuthKit/modules/session.createSession")( function* (token, userId) { const [sessionId, expirationDate] = yield* Effect.all([ makeSessionId(token), makeExpirationDate(expTime) ]); return yield* useSessionErrorPromise( () => sessionTools.createSession({ expiresAt: expirationDate, id: sessionId, userId }) ); } ); const validateSessionToken = Effect.fn( "@withstudiocms/AuthKit/modules/session.validateSessionToken" )(function* (token) { const sessionId = yield* makeSessionId(token); const nullSession = { session: null, user: null }; const result = yield* useSessionErrorPromise( () => sessionTools.sessionAndUserData(sessionId) ); if (!result.length) { return nullSession; } const userSession = result[0]; if (!userSession) { return nullSession; } const { user, session } = userSession; const now = Date.now(); const expiresAtMs = session.expiresAt instanceof Date ? session.expiresAt.getTime() : new Date(session.expiresAt).getTime(); if (!Number.isFinite(expiresAtMs)) { yield* useSessionErrorPromise(() => sessionTools.deleteSession(session.id)); return nullSession; } if (now >= expiresAtMs) { yield* useSessionErrorPromise(() => sessionTools.deleteSession(session.id)); return nullSession; } let sessionOut = session; if (now >= expiresAtMs - expTimeHalf) { const expiresAt = new Date(now + expTime); yield* useSessionErrorPromise( () => sessionTools.updateSession(session.id, { ...session, expiresAt }) ); sessionOut = { ...session, expiresAt }; } return { session: sessionOut, user }; }); const invalidateSession = Effect.fn("@withstudiocms/AuthKit/modules/session.invalidateSession")( (sessionId) => useSessionErrorPromise(() => sessionTools.deleteSession(sessionId)) ); const setSessionTokenCookie = Effect.fn( "@withstudiocms/AuthKit/modules/session.setSessionTokenCookie" )( (context, token, expiresAt, secure) => useSessionError( () => context.cookies.set(cookieName, token, { httpOnly: true, sameSite: "lax", secure: secure ?? defaultSecure, expires: expiresAt, path: "/" }) ) ); const deleteSessionTokenCookie = Effect.fn( "@withstudiocms/AuthKit/modules/session.deleteSessionTokenCookie" )( (context, secure) => useSessionError( () => context.cookies.set(cookieName, "", { httpOnly: true, sameSite: "lax", secure: secure ?? defaultSecure, maxAge: 0, path: "/" }) ) ); const setOAuthSessionTokenCookie = Effect.fn( "@withstudiocms/AuthKit/modules/session.setOAuthSessionTokenCookie" )( (context, key, value, secure) => useSessionError( () => context.cookies.set(key, value, { httpOnly: true, sameSite: "lax", secure: secure ?? defaultSecure, maxAge: 60 * 10, path: "/" }) ) ); const createUserSession = Effect.fn("@withstudiocms/AuthKit/modules/session.createUserSession")( function* (userId, context, secure) { const sessionToken = yield* generateSessionToken(); const expirationDate = yield* makeExpirationDate(expTime); yield* createSession(sessionToken, userId); yield* setSessionTokenCookie(context, sessionToken, expirationDate, secure); } ); return { generateSessionToken, createSession, validateSessionToken, invalidateSession, setSessionTokenCookie, deleteSessionTokenCookie, setOAuthSessionTokenCookie, createUserSession }; }); export { Session };