@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
JavaScript
;
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;
}