auth-vir
Version:
Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.
143 lines (142 loc) • 4.72 kB
JavaScript
import { clearAuthCookie, clearCsrfCookie, extractCookieJwt, generateAuthCookie, generateCsrfCookie, } from './cookie.js';
import { generateCsrfToken, resolveCsrfHeaderName } 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);
}
}
}
function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
return readHeader(headers, resolveCsrfHeaderName(csrfHeaderNameOption));
}
/**
* 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, csrfHeaderNameOption, cookieName, cookieNameSuffix, }) {
try {
const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
const cookie = readHeader(headers, 'cookie');
if (!cookie || !csrfToken) {
return undefined;
}
const jwt = await extractCookieJwt({
rawCookie: cookie,
jwtParams,
cookieName,
cookieNameSuffix,
});
if (!jwt || jwt.data.csrfToken !== csrfToken) {
return undefined;
}
return {
userId: jwt.data.userId,
jwtExpiration: jwt.jwtExpiration,
jwtIssuedAt: jwt.jwtIssuedAt,
cookieName,
csrfToken: jwt.data.csrfToken,
sessionStartedAt: jwt.data.sessionStartedAt,
};
}
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.
* @category Auth : Host
*/
export async function insecureExtractUserIdFromCookieAlone({ headers, jwtParams, cookieName, cookieNameSuffix, }) {
try {
const cookie = readHeader(headers, 'cookie');
if (!cookie) {
return undefined;
}
const jwt = await extractCookieJwt({
rawCookie: cookie,
jwtParams,
cookieName,
cookieNameSuffix,
});
if (!jwt) {
return undefined;
}
return {
userId: jwt.data.userId,
jwtExpiration: jwt.jwtExpiration,
jwtIssuedAt: jwt.jwtIssuedAt,
cookieName,
csrfToken: jwt.data.csrfToken,
sessionStartedAt: jwt.data.sessionStartedAt,
};
}
catch {
return undefined;
}
}
/**
* Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
* and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
* it and inject the value as a request header.
*
* @category Auth : Host
*/
export async function generateSuccessfulLoginHeaders(
/** The id from your database of the user you're authenticating. */
userId, cookieConfig,
/**
* The timestamp (in seconds) when the session originally started. If not provided, the current
* time will be used (for new sessions).
*/
sessionStartedAt) {
const csrfToken = generateCsrfToken();
const authCookie = await generateAuthCookie({
csrfToken,
userId,
sessionStartedAt: sessionStartedAt ?? Date.now(),
}, cookieConfig);
const csrfCookie = generateCsrfCookie(csrfToken, cookieConfig);
return {
'set-cookie': [
authCookie,
csrfCookie,
],
};
}
/**
* 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(cookieConfig, options) {
return {
'set-cookie': [
clearAuthCookie(cookieConfig),
...(options?.preserveCsrf
? []
: [
clearCsrfCookie(cookieConfig),
]),
],
};
}