UNPKG

auth-vir

Version:

Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.

141 lines (130 loc) 4.24 kB
import {check} from '@augment-vir/assert'; import {safeMatch, type PartialWithUndefined} from '@augment-vir/common'; import {convertDuration, type AnyDuration} from 'date-vir'; import {type Primitive} from 'type-fest'; import {parseUrl} from 'url-vir'; import {type CreateJwtParams, type ParseJwtParams} from './jwt.js'; import {createUserJwt, parseUserJwt, type UserJwtData} from './user-jwt.js'; /** * Parameters for {@link generateAuthCookie}. * * @category Internal */ export type CookieParams = { /** * The origin of the host (backend) service that cookies will be included in all requests to. * This should be restricted to just your host (backend) origin for security purposes. * * @example 'https://www.example.com' */ hostOrigin: string; /** * The max duration of this cookie. Or, in other words, the max user session duration before * they're logged out. */ cookieDuration: AnyDuration; /** * All JWT parameters required for generating the encrypted JWT that will be embedded in the * Cookie. Note that all JWT keys contained herein should never shared with any frontend, * client, etc. */ jwtParams: Readonly<CreateJwtParams>; cookieName?: string; } & PartialWithUndefined<{ /** * Is set to `true` (which should only be done in development environments), the cookie will be * allowed in insecure requests (non HTTPS requests). * * @default false */ isDev: boolean; }>; /** * Generate a secure cookie that stores the user JWT data. Used in host (backend) code. * * @category Internal */ export async function generateAuthCookie( userJwtData: Readonly<UserJwtData>, cookieConfig: Readonly<CookieParams>, ): Promise<string> { return generateCookie({ [cookieConfig.cookieName || 'auth']: await createUserJwt( userJwtData, cookieConfig.jwtParams, ), Domain: parseUrl(cookieConfig.hostOrigin).hostname, HttpOnly: true, Path: '/', SameSite: 'Strict', 'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {seconds: true}).seconds, Secure: !cookieConfig.isDev, }); } /** * Generate a cookie value that will clear the previous auth cookie. Use this when signing out. * * @category Internal */ export function clearAuthCookie( cookieConfig: Readonly<Pick<CookieParams, 'cookieName' | 'hostOrigin' | 'isDev'>>, ) { return generateCookie({ [cookieConfig.cookieName || 'auth']: 'redacted', Domain: parseUrl(cookieConfig.hostOrigin).hostname, HttpOnly: true, Path: '/', SameSite: 'Strict', 'MAX-AGE': 0, Secure: !cookieConfig.isDev, }); } /** * Generate a cookie string from a raw set of parameters. * * @category Internal */ export function generateCookie( params: Readonly<Record<string, Exclude<Primitive, symbol>>>, ): string { return Object.entries(params) .map( ([ key, value, ]): string | undefined => { if (value == undefined || value === false) { return undefined; } else if (value === '' || value === true) { return key; } else { return [ key, value, ].join('='); } }, ) .filter(check.isTruthy) .join('; '); } /** * Extract an auth cookie from a cookie string. Used in host (backend) code. * * @category Internal * @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found. */ export async function extractCookieJwt( rawCookie: string, jwtParams: Readonly<ParseJwtParams>, cookieName: string = 'auth', ): Promise<undefined | UserJwtData> { const cookieRegExp = new RegExp(`${cookieName}=[^;]+(?:;|$)`); const [auth] = safeMatch(rawCookie, cookieRegExp); if (!auth) { return undefined; } const rawJwt = auth.replace(`${cookieName}=`, '').replace(';', ''); const jwt = await parseUserJwt(rawJwt, jwtParams); return jwt; }