auth-vir
Version:
Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.
132 lines (131 loc) • 4.64 kB
JavaScript
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;
}
}