auth-vir
Version:
Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.
153 lines (152 loc) • 5.09 kB
JavaScript
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;
}