@wristband/nextjs-auth
Version:
SDK for integrating your Next.js application with Wristband. Handles user authentication, session management, and token management.
152 lines (151 loc) • 7.56 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseTenantSubdomain = parseTenantSubdomain;
exports.resolveTenantName = resolveTenantName;
exports.resolveTenantCustomDomainParam = resolveTenantCustomDomainParam;
exports.createLoginState = createLoginState;
exports.createLoginStateCookie = createLoginStateCookie;
exports.getAuthorizeUrl = getAuthorizeUrl;
exports.getAndClearLoginStateCookie = getAndClearLoginStateCookie;
const constants_1 = require("../constants");
const crypto_1 = require("../crypto");
function parseTenantSubdomain(request, parseTenantFromRootDomain) {
const { host } = request.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 resolveTenantName(request, parseTenantFromRootDomain) {
if (parseTenantFromRootDomain) {
return parseTenantSubdomain(request, parseTenantFromRootDomain) || '';
}
const { tenant_name: tenantNameParam } = request.query;
if (!!tenantNameParam && typeof tenantNameParam !== 'string') {
throw new TypeError('More than one [tenant_name] query parameter was encountered');
}
return tenantNameParam || '';
}
function resolveTenantCustomDomainParam(request) {
const { tenant_custom_domain: tenantCustomDomainParam } = request.query;
if (!!tenantCustomDomainParam && typeof tenantCustomDomainParam !== 'string') {
throw new TypeError('More than one [tenant_custom_domain] query parameter was encountered');
}
return tenantCustomDomainParam || '';
}
function createLoginState(request, redirectUri, config = {}) {
const { return_url: returnUrlParam } = request.query;
if (!!returnUrlParam && typeof returnUrlParam !== 'string') {
throw new TypeError('More than one [return_url] query parameter was encountered');
}
const returnUrl = config.returnUrl ?? returnUrlParam;
return {
state: (0, crypto_1.generateRandomString)(32),
codeVerifier: (0, crypto_1.generateRandomString)(32),
redirectUri,
...(!!returnUrl && typeof returnUrl === 'string' ? { returnUrl } : {}),
...(!!config.customState && !!Object.keys(config.customState).length ? { customState: config.customState } : {}),
};
}
function createLoginStateCookie(request, response, state, encryptedLoginState, dangerouslyDisableSecureCookies) {
const { cookies } = request;
// 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 responseCookieArray = [];
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 3 cookies exist, then we delete the oldest one to make room for the new one.
if (!mostRecentTimestamps.includes(timestamp)) {
const staleCookieHeaderValue = [
`${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${!dangerouslyDisableSecureCookies ? '; Secure' : ''}`,
];
responseCookieArray.push(staleCookieHeaderValue);
}
});
}
// Now add the new login state cookie with a 1-hour expiration time.
// NOTE: If deploying your own app to production, do not disable secure cookies.
const newCookieName = `${constants_1.LOGIN_STATE_COOKIE_PREFIX}${state}${constants_1.LOGIN_STATE_COOKIE_SEPARATOR}${Date.now().valueOf()}`;
const newCookieHeaderValue = [
`${newCookieName}=${encryptedLoginState};`,
'HTTPOnly;',
'Max-Age=3600;',
'Path=/;',
'SameSite=lax',
].join(' ');
const resolvedCookieValue = `${newCookieHeaderValue}${dangerouslyDisableSecureCookies ? '' : '; Secure'}`;
responseCookieArray.push(resolvedCookieValue);
response.setHeader('Set-Cookie', responseCookieArray);
}
async function getAuthorizeUrl(request, config) {
const { login_hint: loginHint } = request.query;
if (!!loginHint && typeof loginHint !== 'string') {
throw new TypeError('More than one [login_hint] query parameter was encountered');
}
const digest = await (0, crypto_1.sha256Base64)(config.codeVerifier);
const queryParams = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
response_type: 'code',
state: config.state,
scope: config.scopes.join(' '),
code_challenge: (0, crypto_1.base64ToURLSafe)(digest),
code_challenge_method: 'S256',
nonce: (0, crypto_1.generateRandomString)(32),
...(!!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 getAndClearLoginStateCookie(request, response, dangerouslyDisableSecureCookies) {
const { cookies, query } = request;
const { state } = query;
const paramState = state ? state.toString() : '';
// 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];
// Delete the login state cookie.
response.setHeader('Set-Cookie', [
`${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${!dangerouslyDisableSecureCookies ? '; Secure' : ''}`,
]);
}
return loginStateCookie;
}