UNPKG

auth-vir

Version:

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

132 lines (131 loc) 4.64 kB
import { HttpStatus, } from '@augment-vir/common'; import { listenToActivity } from 'detect-activity'; import { getCurrentCsrfToken, resolveCsrfHeaderName, } from '../csrf-token.js'; import { AuthHeaderName } from '../headers.js'; /** * An auth client for sending and validating client requests to a backend. This should only be used * in a frontend environment as it accesses native browser APIs. * * @category Auth : Client * @category Clients */ export class FrontendAuthClient { config; userCheckInterval; /** Used to clean up the activity listener on `.destroy()`. */ removeActivityListener; constructor(config) { this.config = config; if (config.checkUser) { this.removeActivityListener = listenToActivity({ listener: async () => { const response = await config.checkUser?.performCheck(); if (response) { await this.verifyResponseAuth({ status: response.status, }); } }, debounce: config.checkUser.debounce || { minutes: 1, }, fireImmediately: false, }); } } /** * Destroys the client and performs all necessary cleanup (like clearing the user check * interval). */ destroy() { this.userCheckInterval?.clearInterval(); this.removeActivityListener?.(); } /** * Assume the given user. Pass `undefined` to wipe the currently assumed user. * * @returns Whether the assumed user setting or clearing succeeded or not. */ async assumeUser(assumedUserParams) { const localStorage = this.config.overrides?.localStorage || globalThis.localStorage; const storageKey = this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser; if (!assumedUserParams) { localStorage.removeItem(storageKey); return true; } else if (!(await this.config.canAssumeUser?.())) { return false; } localStorage.setItem(storageKey, JSON.stringify(assumedUserParams)); return true; } /** Gets the assumed user params stored in local storage, if any. */ getAssumedUser() { const rawValue = (this.config.overrides?.localStorage || globalThis.localStorage).getItem(this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser); if (!rawValue) { return undefined; } try { return JSON.parse(rawValue); } catch { return undefined; } } /** * Creates a `RequestInit` object for the `fetch` API. If you have other request init options, * use [`mergeDeep` from * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to * combine them with these. */ createAuthenticatedRequestInit() { const csrfToken = getCurrentCsrfToken(this.config.cookieNameSuffix); const assumedUser = this.getAssumedUser(); const headers = { ...(csrfToken ? { [resolveCsrfHeaderName(this.config.csrf)]: csrfToken, } : {}), ...(assumedUser ? { [this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser]: JSON.stringify(assumedUser), } : {}), }; return { headers, credentials: 'include', }; } /** Wipes the current user auth. */ async logout() { await this.config.authClearedCallback?.(); } /** * Use to handle a login response. The CSRF token cookie is automatically stored by the browser * from the `Set-Cookie` response header. * * @throws Error if the login response failed. */ async handleLoginResponse(response) { if (!response.ok) { await this.logout(); throw new Error('Login response failed.'); } } /** * Use to verify _all_ responses received from the backend. Immediately logs the user out once * an unauthorized response is detected. * * @returns `true` if the auth is okay, `false` otherwise. */ async verifyResponseAuth(response) { if (response.status === HttpStatus.Unauthorized && !response.headers?.get(AuthHeaderName.IsSignUpAuth)) { await this.logout(); return false; } return true; } }