Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers. ESM and browser friendly.
npm i auth-vir
Hash a user created password:
import {hashPassword} from 'auth-vir';
/** When a user creates or resets their password, hash it before storing it in your database. */
const hashedPassword = await hashPassword('user input password');
/** Store `hashedPassword` in your database. */
Compare a stored password hash for login checking:
import {doesPasswordMatchHash} from 'auth-vir';
if (
!(await doesPasswordMatchHash({
hash: 'hash from database',
password: 'user input password for login',
}))
) {
throw new Error('Login failure.');
}
Use this on your host / server / backend to authenticate client / frontend requests.
csrfTokenHeaderName
(or just 'csrf-token'
) header via CORS headers with either of the following options:
customHeaders: [csrfTokenHeaderName]
in implementService
from @rest-vir/implement-service
.Access-Control-Allow-Headers
to (at least) csrfTokenHeaderName
.Access-Control-Allow-Origin
header (it cannot be *
) and properly implement CORS headers and responses.npx auth-vir
: the generated keys will be printed to your console.await generateNewJwtKeys()
(imported from this package) in your code.parseJwtKeys
: await parseJwtKeys(stringKeys)
.parseJwtKeys
in all auth functionality:
generateSuccessfulLoginHeaders
: after a user successfully logs in, run this function and attach the output headers to the Response
object.extractUserIdFromRequestHeaders
: to verify an authenticated user Request
object (make sure to properly attach all auth in the client by following the below Client / frontend side guide).Here's a full example of how to use all host / server / backend side auth functionality:
import {type ClientRequest, type ServerResponse} from 'node:http';
import {
doesPasswordMatchHash,
extractUserIdFromRequestHeaders,
generateNewJwtKeys,
generateSuccessfulLoginHeaders,
hashPassword,
parseJwtKeys,
type CookieParams,
type CreateJwtParams,
} from 'auth-vir';
/**
* Use this for a /login endpoint.
*
* This verifies a user's login credentials and generate the auth cookie and CSRF token.
*/
export async function handleLogin(
userRequestData: Readonly<{username: string; password: string}>,
response: ServerResponse,
) {
const user = findUserInDatabaseByUsername(userRequestData.username);
if (
!(await doesPasswordMatchHash({
hash: user.hashedPassword,
password: userRequestData.password,
}))
) {
throw new Error('Credentials mismatch.');
}
const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams);
response.setHeaders(new Headers(authHeaders));
}
/**
* Use this for a /sign-up endpoint.
*
* This creates a new user, stores their securely hashed password in the database, and generates the
* auth cookie and CSRF token.
*/
export async function createUser(
userRequestData: Readonly<{username: string; password: string}>,
response: ServerResponse,
) {
const newUser = await createUserInDatabase(userRequestData);
const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams);
response.setHeaders(new Headers(authHeaders));
}
/**
* Use this all endpoints that require an authenticated user.
*
* This loads the current user from their auth cookie and CSRF token.
*/
export async function getAuthenticatedUser(request: ClientRequest) {
const userId = await extractUserIdFromRequestHeaders(request.getHeaders(), jwtParams);
const user = userId ? findUserInDatabaseById(userId) : undefined;
if (!userId || !user) {
throw new Error('Unauthorized.');
}
return user;
}
/**
* # ===========
*
* Helpers
*
* # ===========
*/
async function loadSecretJwtKeys() {
/**
* This should load your saved JWT keys from a non-committed config file or a secrets manager
* (like AWS Secrets Manager).
*/
return await generateNewJwtKeys();
}
const jwtParams: Readonly<CreateJwtParams> = {
audience: 'server context',
jwtDuration: {
hours: 2,
},
issuer: 'server login',
jwtKeys: await parseJwtKeys(await loadSecretJwtKeys()),
};
const cookieParams: CookieParams = {
cookieDuration: {
hours: 2,
},
hostOrigin: 'https://your-backend-origin.example.com',
jwtParams,
};
function findUserInDatabaseByUsername(username: string) {
/** This should connect to your database and find a user matching the given username. */
return {
/** This should be retrieved from your database. */
id: 'some id',
username,
/** This should be retrieved from your database. */
hashedPassword: 'hash retrieved from database',
};
}
function findUserInDatabaseById(userId: string): undefined | {id: string; username: string} {
/** This should connect to your database and find a user matching the given user id. */
return {
id: userId,
/** This should be retrieved from your database. */
username: 'some username',
};
}
async function createUserInDatabase(
userRequestData: Readonly<{username: string; password: string}>,
) {
const hashedPassword = await hashPassword(userRequestData.password);
if (!hashedPassword) {
throw new Error('Password too long.');
}
/**
* Store the new username and hashedPassword in your database and return the new user id.
*
* @example
*
* // using the Prisma ORM:
* return (
* await prismaClient.user.create({
* data: {
* username: userRequestData.username,
* hashedPassword,
* },
* select: {
* id: true,
* },
* })
* ).id;
*/
return {
id: 'some new id',
};
}
Use this on your client / frontend for storing and sending session authorization.
{credentials: 'include'}
set on the request.Response
from step 1 into handleAuthResponse
.{credentials: 'include'}
and include {headers: {[csrfTokenHeaderName]: getCurrentCsrfToken()}}
.wipeCurrentCsrfToken()
Here's a full example of how to use all the client / frontend side auth functionality:
import {HttpStatus} from '@augment-vir/common';
import {
csrfTokenHeaderName,
getCurrentCsrfToken,
handleAuthResponse,
wipeCurrentCsrfToken,
} from 'auth-vir';
/** Call this when the user logs in for the first time this session. */
export async function sendLoginRequest(
userLoginData: {username: string; password: string},
loginUrl: string,
) {
if (getCurrentCsrfToken()) {
throw new Error('Already logged in.');
}
const response = await fetch(loginUrl, {
method: 'post',
body: JSON.stringify(userLoginData),
credentials: 'include',
});
handleAuthResponse(response);
return response;
}
/** Call this when the user needs to send any authenticated request after already having logged in. */
export async function sendAuthenticatedRequest(
requestUrl: string,
requestInit: Omit<RequestInit, 'headers'> = {},
headers: Record<string, string> = {},
) {
const csrfToken = getCurrentCsrfToken();
if (!csrfToken) {
throw new Error('Not authenticated.');
}
const response = await fetch(requestUrl, {
...requestInit,
credentials: 'include',
headers: {
...headers,
[csrfTokenHeaderName]: csrfToken,
},
});
/**
* This indicates the user is no longer authorized and thus needs to login again. (This likely
* means that their session timed out or they clicked a "log out" button onr your website in
* another tab.)
*/
if (response.status === HttpStatus.Unauthorized) {
wipeCurrentCsrfToken();
throw new Error(`User no longer logged in.`);
} else {
return response;
}
}
/** Call this when the user explicitly clicks a "log out" button. */
export function logout() {
wipeCurrentCsrfToken();
}
All of these configurations must be set for the auth exports in this package to function properly:
csrfTokenHeaderName
(or just 'csrf-token'
) header via CORS headers with either of the following options:
customHeaders: [csrfTokenHeaderName]
in implementService
from @rest-vir/implement-service
.Access-Control-Allow-Headers
to (at least) csrfTokenHeaderName
.credentials: include
in all fetch requests on the client that need to use or set the auth cookie.Access-Control-Allow-Origin
(it cannot be *
).