UNPKG

@wristband/express-auth

Version:

SDK for integrating your ExpressJS application with Wristband. Handles user authentication, session management, and token management.

215 lines (214 loc) 10.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.encodeBase64 = encodeBase64; exports.parseTenantSubdomain = parseTenantSubdomain; exports.generateRandomString = generateRandomString; exports.base64URLEncode = base64URLEncode; exports.encryptLoginState = encryptLoginState; exports.decryptLoginState = decryptLoginState; exports.getAndClearLoginStateCookie = getAndClearLoginStateCookie; exports.resolveTenantName = resolveTenantName; exports.resolveTenantCustomDomainParam = resolveTenantCustomDomainParam; exports.createLoginState = createLoginState; exports.clearOldestLoginStateCookie = clearOldestLoginStateCookie; exports.createLoginStateCookie = createLoginStateCookie; exports.getOAuthAuthorizeUrl = getOAuthAuthorizeUrl; exports.isExpired = isExpired; const crypto_1 = require("crypto"); const iron_webcrypto_1 = require("iron-webcrypto"); const crypto = __importStar(require("uncrypto")); const constants_1 = require("./constants"); const cookies_1 = require("./cookies"); function encodeBase64(input) { const encoder = new TextEncoder(); const data = encoder.encode(input); let binary = ''; data.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary); } function parseTenantSubdomain(req, parseTenantFromRootDomain) { const { host } = req.headers; // Should never happen (defensive measure) if (!host) { return ''; } // Strip off the port if it exists const hostname = host.split(':')[0]; return hostname.substring(hostname.indexOf('.') + 1) === parseTenantFromRootDomain ? hostname.substring(0, hostname.indexOf('.')) : ''; } function generateRandomString(length) { return (0, crypto_1.randomBytes)(length).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } function base64URLEncode(str) { return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } async function encryptLoginState(loginState, loginStateSecret) { // @ts-ignore const encryptedLoginState = await (0, iron_webcrypto_1.seal)(crypto, loginState, loginStateSecret, iron_webcrypto_1.defaults); if (encryptedLoginState.length > 4096) { throw new TypeError('Login state cookie exceeds 4kB in size. Ensure your [customState] and [returnUrl] values are a reasonable size.'); } return encryptedLoginState; } async function decryptLoginState(loginStateCookie, loginStateSecret) { // @ts-ignore const loginState = await (0, iron_webcrypto_1.unseal)(crypto, loginStateCookie, loginStateSecret, iron_webcrypto_1.defaults); return loginState; } function getAndClearLoginStateCookie(req, res, dangerouslyDisableSecureCookies) { const { state } = req.query; const paramState = state ? state.toString() : ''; const cookies = (0, cookies_1.parseCookies)(req); // This should always resolve to a single cookie with this prefix, or possibly no cookie at all // if it got cleared or expired before the callback was triggered. const matchingLoginCookieNames = Object.keys(cookies).filter((cookieName) => { return cookieName.startsWith(`${constants_1.LOGIN_STATE_COOKIE_PREFIX}${paramState}${constants_1.LOGIN_STATE_COOKIE_SEPARATOR}`); }); let loginStateCookie = ''; if (matchingLoginCookieNames.length > 0) { const cookieName = matchingLoginCookieNames[0]; loginStateCookie = cookies[cookieName]; (0, cookies_1.clearCookie)(res, cookieName, dangerouslyDisableSecureCookies); } return loginStateCookie; } function resolveTenantName(req, parseTenantFromRootDomain) { if (parseTenantFromRootDomain) { return parseTenantSubdomain(req, parseTenantFromRootDomain) || ''; } const { tenant_name: tenantNameParam } = req.query; if (!!tenantNameParam && typeof tenantNameParam !== 'string') { throw new TypeError('More than one [tenant_name] query parameter was encountered'); } return tenantNameParam || ''; } function resolveTenantCustomDomainParam(req) { const { tenant_custom_domain: tenantCustomDomainParam } = req.query; if (!!tenantCustomDomainParam && typeof tenantCustomDomainParam !== 'string') { throw new TypeError('More than one [tenant_custom_domain] query parameter was encountered'); } return tenantCustomDomainParam || ''; } function createLoginState(req, redirectUri, config = {}) { const { return_url: returnUrlParam } = req.query; if (!!returnUrlParam && typeof returnUrlParam !== 'string') { throw new TypeError('More than one [return_url] query parameter was encountered'); } const returnUrl = config.returnUrl ?? returnUrlParam; const loginStateData = { state: generateRandomString(32), codeVerifier: generateRandomString(32), redirectUri, ...(!!returnUrl && typeof returnUrl === 'string' ? { returnUrl } : {}), ...(!!config.customState && !!Object.keys(config.customState).length ? { customState: config.customState } : {}), }; return config.customState ? { ...loginStateData, customState: config.customState } : loginStateData; } function clearOldestLoginStateCookie(req, res, dangerouslyDisableSecureCookies) { const cookies = (0, cookies_1.parseCookies)(req); // The max amount of concurrent login state cookies we allow is 3. If there are already 3 cookies, // then we clear the one with the oldest creation timestamp to make room for the new one. const allLoginCookieNames = Object.keys(cookies).filter((cookieName) => { return cookieName.startsWith(`${constants_1.LOGIN_STATE_COOKIE_PREFIX}`); }); // Retain only the 2 cookies with the most recent timestamps. if (allLoginCookieNames.length >= 3) { const mostRecentTimestamps = allLoginCookieNames .map((cookieName) => { return cookieName.split(constants_1.LOGIN_STATE_COOKIE_SEPARATOR)[2]; }) .sort() .reverse() .slice(0, 2); allLoginCookieNames.forEach((cookieName) => { const timestamp = cookieName.split(constants_1.LOGIN_STATE_COOKIE_SEPARATOR)[2]; if (!mostRecentTimestamps.includes(timestamp)) { (0, cookies_1.clearCookie)(res, cookieName, dangerouslyDisableSecureCookies); } }); } } function createLoginStateCookie(res, state, encryptedLoginState, dangerouslyDisableSecureCookies) { // Add the new login state cookie (1 hour max age). const cookieName = `${constants_1.LOGIN_STATE_COOKIE_PREFIX}${state}${constants_1.LOGIN_STATE_COOKIE_SEPARATOR}${Date.now().valueOf()}`; (0, cookies_1.setCookie)(res, cookieName, encryptedLoginState, { maxAge: 3600, dangerouslyDisableSecureCookies }); } function getOAuthAuthorizeUrl(req, config) { const { idp_hint: idpHint, login_hint: loginHint } = req.query; if (!!idpHint && typeof idpHint !== 'string') { throw new TypeError('More than one [idp_hint] query parameter was encountered'); } if (!!loginHint && typeof loginHint !== 'string') { throw new TypeError('More than one [login_hint] query parameter was encountered'); } const queryParams = new URLSearchParams({ client_id: config.clientId, redirect_uri: config.redirectUri, response_type: 'code', state: config.state, scope: config.scopes.join(' '), code_challenge: base64URLEncode((0, crypto_1.createHash)('sha256').update(config.codeVerifier).digest('base64')), code_challenge_method: 'S256', nonce: generateRandomString(32), ...(!!idpHint && typeof idpHint === 'string' ? { idp_hint: idpHint } : {}), ...(!!loginHint && typeof loginHint === 'string' ? { login_hint: loginHint } : {}), }); const separator = config.isApplicationCustomDomainActive ? '.' : '-'; // Domain priority order resolution: // 1) tenant_custom_domain query param // 2a) tenant subdomain // 2b) tenant_name query param // 3) defaultTenantCustomDomain login config // 4) defaultTenantName login config if (config.tenantCustomDomain) { return `https://${config.tenantCustomDomain}/api/v1/oauth2/authorize?${queryParams.toString()}`; } if (config.tenantName) { return `https://${config.tenantName}${separator}${config.wristbandApplicationVanityDomain}/api/v1/oauth2/authorize?${queryParams.toString()}`; } if (config.defaultTenantCustomDomain) { return `https://${config.defaultTenantCustomDomain}/api/v1/oauth2/authorize?${queryParams.toString()}`; } return `https://${config.defaultTenantName}${separator}${config.wristbandApplicationVanityDomain}/api/v1/oauth2/authorize?${queryParams.toString()}`; } function isExpired(expiresAt) { const currentTime = Date.now().valueOf(); return currentTime >= expiresAt; }