auth-vir - v1.3.1
    Preparing search index...

    auth-vir - v1.3.1

    auth-vir

    Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers. ESM and browser friendly.

    Install

    npm i auth-vir
    

    Usage

    • 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.

    1. Expose the csrfTokenHeaderName (or just 'csrf-token') header via CORS headers with either of the following options:
      1. Set customHeaders: [csrfTokenHeaderName] in implementService from @rest-vir/implement-service.
      2. Set the header Access-Control-Allow-Headers to (at least) csrfTokenHeaderName.
    2. Set the Access-Control-Allow-Origin header (it cannot be *) and properly implement CORS headers and responses.
    3. Generate JWT signing and encryption keys with one of the following:
      • Run npx auth-vir: the generated keys will be printed to your console.
      • Run await generateNewJwtKeys() (imported from this package) in your code.
    4. Securely store the generated keys in a secret place. Do not commit them. They should not be shared with anyone or any other host, client, or service.
    5. In your application code, load the string keys from step 1 into parseJwtKeys: await parseJwtKeys(stringKeys).
    6. Use the output of parseJwtKeys in all auth functionality:

    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.

    1. Send a login fetch request to your host / server / backend with {credentials: 'include'} set on the request.
    2. Pass the Response from step 1 into handleAuthResponse.
    3. In all subsequent fetch requests to the host / server / backend, set {credentials: 'include'} and include {headers: {[csrfTokenHeaderName]: getCurrentCsrfToken()}}.
    4. Upon user logout, call 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();
    }

    Requirements

    All of these configurations must be set for the auth exports in this package to function properly:

    • Expose the csrfTokenHeaderName (or just 'csrf-token') header via CORS headers with either of the following options:
      1. Set customHeaders: [csrfTokenHeaderName] in implementService from @rest-vir/implement-service.
      2. Set the header Access-Control-Allow-Headers to (at least) csrfTokenHeaderName.
    • Set credentials: include in all fetch requests on the client that need to use or set the auth cookie.
    • Server CORS should set Access-Control-Allow-Origin (it cannot be *).