UNPKG

auth-vir

Version:

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

153 lines (152 loc) 5.09 kB
import { check } from '@augment-vir/assert'; import { escapeStringForRegExp, safeMatch, } from '@augment-vir/common'; import { convertDuration } from 'date-vir'; import { parseUrl } from 'url-vir'; import { createUserJwt, parseUserJwt } from './jwt/user-jwt.js'; /** * Cookie header names supported by default. * * @category Internal */ export var AuthCookie; (function (AuthCookie) { /** Used for a full user login auth. */ AuthCookie["Auth"] = "auth"; /** Use for a temporary "just signed up" auth. */ AuthCookie["SignUp"] = "sign-up"; /** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */ AuthCookie["Csrf"] = "auth-vir-csrf"; })(AuthCookie || (AuthCookie = {})); /** * Resolves a cookie name by appending a suffix when provided. When `cookieNameSuffix` is * `undefined`, the base name is returned unchanged. * * @category Internal */ export function resolveCookieName(baseCookieName, cookieNameSuffix) { return [ baseCookieName, cookieNameSuffix, ] .filter(check.isTruthy) .join('-'); } function generateSetCookie({ name, value, httpOnly, cookieConfig, }) { return generateCookie({ [name]: value, Domain: parseUrl(cookieConfig.hostOrigin).hostname, HttpOnly: httpOnly, Path: '/', SameSite: 'Strict', 'MAX-AGE': cookieConfig.cookieDuration ? convertDuration(cookieConfig.cookieDuration, { seconds: true, }).seconds : 0, Secure: !cookieConfig.isDev, }); } /** * Generate a secure cookie that stores the user JWT data. Used in host (backend) code. * * @category Internal */ export async function generateAuthCookie(userJwtData, cookieConfig) { return generateSetCookie({ name: resolveCookieName(cookieConfig.authCookie || AuthCookie.Auth, cookieConfig.cookieNameSuffix), value: await createUserJwt(userJwtData, cookieConfig.jwtParams), httpOnly: true, cookieConfig, }); } /** * Generate a CSRF token cookie. This cookie is intentionally not `HttpOnly` so that frontend * JavaScript can read it and inject the value as a request header for double-submit verification. * * The CSRF cookie uses a fixed 400-day MAX-AGE rather than matching the auth cookie duration. 400 * days is the cross-browser safe maximum (Chrome caps cookie lifetimes at 400 days; other browsers * accept it as-is). The CSRF token is only meaningful when paired with a valid JWT, so it doesn't * need its own expiration management. It gets regenerated on every fresh login. * * @category Internal */ export function generateCsrfCookie(csrfToken, cookieConfig) { return generateSetCookie({ name: resolveCookieName(AuthCookie.Csrf, cookieConfig.cookieNameSuffix), value: csrfToken, httpOnly: false, cookieConfig: { ...cookieConfig, cookieDuration: { days: 400, }, }, }); } /** * Generate a cookie value that will clear the previous auth cookie. Use this when signing out. * * @category Internal */ export function clearAuthCookie(cookieConfig) { return generateSetCookie({ name: resolveCookieName(cookieConfig.authCookie || AuthCookie.Auth, cookieConfig.cookieNameSuffix), value: 'redacted', httpOnly: true, cookieConfig, }); } /** * Generate a cookie value that will clear the CSRF token cookie. Use this when signing out. * * @category Internal */ export function clearCsrfCookie(cookieConfig) { return generateSetCookie({ name: resolveCookieName(AuthCookie.Csrf, cookieConfig.cookieNameSuffix), value: 'redacted', httpOnly: false, cookieConfig, }); } /** * Generate a cookie string from a raw set of parameters. * * @category Internal */ export function generateCookie(params) { return Object.entries(params) .map(([key, value,]) => { 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, jwtParams, cookieName, cookieNameSuffix, }) { const resolvedName = resolveCookieName(cookieName, cookieNameSuffix); const cookieRegExp = new RegExp(`${escapeStringForRegExp(resolvedName)}=[^;]+(?:;|$)`); const [cookieValue] = safeMatch(rawCookie, cookieRegExp); if (!cookieValue) { return undefined; } const rawJwt = cookieValue.replace(`${resolvedName}=`, '').replace(';', ''); const jwt = await parseUserJwt(rawJwt, jwtParams); return jwt; }