auth-vir
Version:
Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.
137 lines (136 loc) • 4.71 kB
JavaScript
import { clearAuthCookie, extractCookieJwt, generateAuthCookie, } from './cookie.js';
import { csrfTokenHeaderName, generateCsrfToken } from './csrf-token.js';
function readHeader(headers, headerName) {
if (headers instanceof Headers) {
return headers.get(headerName) || undefined;
}
else {
const value = headers[headerName];
if (value == undefined) {
return undefined;
}
else if (Array.isArray(value)) {
return value[0];
}
else {
return String(value);
}
}
}
/**
* Extract the user id from a request by checking both the request cookie and CSRF token. This is
* used by host (backend) code to help verify a request. After extracting the user id using this,
* you should compare it to users stored in your database.
*
* @category Auth : Host
* @returns The extracted user id or `undefined` if no valid auth headers exist.
*/
export async function extractUserIdFromRequestHeaders(headers, jwtParams, cookieName) {
try {
const csrfToken = readHeader(headers, csrfTokenHeaderName);
const cookie = readHeader(headers, 'cookie');
if (!cookie || !csrfToken) {
return undefined;
}
const jwt = await extractCookieJwt(cookie, jwtParams, cookieName);
if (!jwt || jwt.csrfToken !== csrfToken) {
return undefined;
}
return jwt.userId;
}
catch {
return undefined;
}
}
/**
* Extract a user id from just the cookie, without CSRF token validation. This is _less secure_ than
* {@link extractUserIdFromRequestHeaders} as a result. This should only be used in rare
* circumstances where you cannot rely on client-side JavaScript to insert the CSRF token.
*
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
*/
export async function extractUserIdFromCookieAlone(headers, jwtParams, cookieName) {
try {
const cookie = readHeader(headers, 'cookie');
if (!cookie) {
return undefined;
}
const jwt = await extractCookieJwt(cookie, jwtParams, cookieName);
if (!jwt) {
return undefined;
}
return jwt.userId;
}
catch {
return undefined;
}
}
/**
* Used by host (backend) code to set headers on a response object.
*
* @category Auth : Host
*/
export async function generateSuccessfulLoginHeaders(
/** The id from your database of the user you're authenticating. */
userId, cookieConfig) {
const csrfToken = generateCsrfToken();
return {
'set-cookie': await generateAuthCookie({
csrfToken,
userId,
}, cookieConfig),
[csrfTokenHeaderName]: csrfToken,
};
}
/**
* Used by host (backend) code to set headers on a response object when the user has logged out or
* failed to authorize.
*
* @category Auth : Host
*/
export function generateLogoutHeaders(...params) {
return {
'set-cookie': clearAuthCookie(...params),
[csrfTokenHeaderName]: 'redacted',
};
}
/**
* Store auth data on a client (frontend) after receiving an auth response from the host (backend).
* Specifically, this stores the CSRF token into local storage (which doesn't need to be a secret).
* Alternatively, if the given response failed, this will wipe the existing (if anyone) stored CSRF
* token.
*
* @category Auth : Client
* @throws Error if no CSRF token header is found.
*/
export function handleAuthResponse(response, overrides = {}) {
if (!response.ok) {
wipeCurrentCsrfToken(overrides);
return;
}
const headerName = overrides.csrfHeaderName || csrfTokenHeaderName;
const csrfToken = response.headers.get(headerName);
if (!csrfToken) {
wipeCurrentCsrfToken(overrides);
throw new Error('Did not receive any CSRF token.');
}
(overrides.localStorage || globalThis.localStorage).setItem(headerName, csrfToken);
}
/**
* Used in client (frontend) code to retrieve the current CSRF token in order to send it with
* requests to the host (backend).
*
* @category Auth : Client
*/
export function getCurrentCsrfToken(overrides = {}) {
return ((overrides.localStorage || globalThis.localStorage).getItem(overrides.csrfHeaderName || csrfTokenHeaderName) || undefined);
}
/**
* Wipes the current stored CSRF token. This should be used by client (frontend) code to logout a
* user or react to a session timeout.
*
* @category Auth : Client
*/
export function wipeCurrentCsrfToken(overrides = {}) {
return (overrides.localStorage || globalThis.localStorage).removeItem(overrides.csrfHeaderName || csrfTokenHeaderName);
}