auth-vir
Version:
Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.
141 lines (130 loc) • 4.24 kB
text/typescript
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;
}