UNPKG

@wristband/nextjs-auth

Version:

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

148 lines (147 loc) 7.44 kB
"use strict"; 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.getLoginStateCookie = getLoginStateCookie; exports.clearLoginStateCookie = clearLoginStateCookie; const constants_1 = require("../constants"); const crypto_1 = require("../crypto"); function parseCookies(cookieHeader) { if (!cookieHeader) return {}; return Object.fromEntries(cookieHeader.split(';').map((cookie) => { const [name, ...rest] = cookie.trim().split('='); return [name, decodeURIComponent(rest.join('='))]; })); } function parseTenantSubdomain(request, parseTenantFromRootDomain) { const host = request.headers.get('host'); // 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 tenantNameParam = request.nextUrl.searchParams.getAll('tenant_name'); if (tenantNameParam.length > 1) { throw new TypeError('More than one [tenant_name] query parameter was encountered'); } return tenantNameParam[0] || ''; } function resolveTenantCustomDomainParam(request) { const tenantCustomDomainParam = request.nextUrl.searchParams.getAll('tenant_custom_domain'); if (tenantCustomDomainParam.length > 1) { throw new TypeError('More than one [tenant_custom_domain] query parameter was encountered'); } return tenantCustomDomainParam[0] || ''; } function createLoginState(request, redirectUri, config = {}) { const returnUrlParam = request.nextUrl.searchParams.getAll('return_url'); if (returnUrlParam.length > 1) { throw new TypeError('More than one [return_url] query parameter was encountered'); } const resolvedReturnUrlParam = returnUrlParam.length > 0 ? returnUrlParam[0] : ''; const returnUrl = config.returnUrl ?? resolvedReturnUrlParam; 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) { // Parse existing cookies from the request const cookies = parseCookies(request.headers.get('cookie')); // Filter for login state cookies const allLoginCookies = Object.entries(cookies) .filter(([name]) => { return name.startsWith(constants_1.LOGIN_STATE_COOKIE_PREFIX); }) .map(([name]) => { return { name, timestamp: parseInt(name.split(constants_1.LOGIN_STATE_COOKIE_SEPARATOR)[2], 10) }; }); // 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. if (allLoginCookies.length >= 3) { const oldestCookie = allLoginCookies.sort((a, b) => { return a.timestamp - b.timestamp; })[0]; // Delete the cookie response.headers.append('Set-Cookie', `${oldestCookie.name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${!dangerouslyDisableSecureCookies ? '; Secure' : ''}`); } // 1 hour expiration for new cookie const newCookieName = `${constants_1.LOGIN_STATE_COOKIE_PREFIX}${state}${constants_1.LOGIN_STATE_COOKIE_SEPARATOR}${Date.now().valueOf()}`; response.headers.append('Set-Cookie', `${newCookieName}=${encryptedLoginState}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600${!dangerouslyDisableSecureCookies ? '; Secure' : ''}`); } async function getAuthorizeUrl(request, config) { const loginHint = request.nextUrl.searchParams.getAll('login_hint'); if (loginHint.length > 1) { 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.length > 0 ? { login_hint: loginHint[0] } : {}), }); 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 getLoginStateCookie(request) { // Parse existing cookies from the request const cookies = parseCookies(request.headers.get('cookie')); const state = request.nextUrl.searchParams.get('state'); 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}`); }); if (matchingLoginCookieNames.length > 0) { const cookieName = matchingLoginCookieNames[0]; return { name: cookieName, value: cookies[cookieName] }; } return null; } function clearLoginStateCookie(response, cookieName, dangerouslyDisableSecureCookies) { // NOTE: Due to a bug in iron-session, we set both maxAge and Expires const cookieAttributes = [ `${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, !dangerouslyDisableSecureCookies ? 'Secure' : '', ].join('; '); response.headers.append('Set-Cookie', cookieAttributes); }