@wristband/nextjs-auth
Version:
SDK for integrating your NextJS application with Wristband. Handles user authentication and token management.
266 lines (265 loc) • 12.7 kB
JavaScript
import { WristbandError } from './error';
import { TENANT_DOMAIN_TOKEN } from './utils/constants';
import { WristbandService } from './wristband-service';
const DEFAULT_SCOPES = ['openid', 'offline_access', 'email'];
const DEFAULT_TOKEN_EXPIRATION_BUFFER = 60; // 60 seconds
const MAX_FETCH_ATTEMPTS = 3;
const ATTEMPT_DELAY_MS = 100; // 100 milliseconds
export class ConfigResolver {
constructor(authConfig) {
this.sdkConfigCache = null;
this.configPromise = null;
this.authConfig = authConfig;
// Always validate the following:
// - ClientId
// - ClientSecret
// - LoginStateSecret
// - WristbandApplicationVanityDomain
this.validateRequiredAuthConfigs();
if (!this.getAutoConfigureEnabled()) {
// Validate the following if auto-configure is disabled:
// - loginUrl
// - redirectUri
// - parseTenantFromRootDomain
this.validateStrictUrlAuthConfigs();
}
else {
// Only validate manually provided values when auto-configure is enabled
this.validatePartialUrlAuthConfigs();
}
this.wristbandService = new WristbandService(authConfig.wristbandApplicationVanityDomain, authConfig.clientId, authConfig.clientSecret);
}
async preloadSdkConfig() {
await this.loadSdkConfig();
}
async loadSdkConfig() {
// Return cached config if available
if (this.sdkConfigCache) {
return this.sdkConfigCache;
}
// Return existing promise if already fetching
if (this.configPromise) {
const result = await this.configPromise;
return result;
}
try {
this.configPromise = this.fetchSdkConfiguration();
this.sdkConfigCache = await this.configPromise;
this.validateAllDynamicConfigs(this.sdkConfigCache);
return this.sdkConfigCache;
}
catch (error) {
// Reset promise on error so retry is possible
this.configPromise = null;
throw error;
}
}
async fetchSdkConfiguration() {
let lastError;
for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt += 1) {
try {
// eslint-disable-next-line no-await-in-loop
const config = await this.wristbandService.getSdkConfiguration();
return config;
}
catch (error) {
lastError = error;
// Final attempt failed, throw the error
if (attempt === MAX_FETCH_ATTEMPTS) {
break;
}
// Wait before retrying
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, ATTEMPT_DELAY_MS);
});
}
}
throw new WristbandError(`Failed to fetch SDK configuration after ${MAX_FETCH_ATTEMPTS} attempts: ${lastError?.message || 'Unknown error'}`);
}
validateRequiredAuthConfigs() {
if (!this.authConfig.clientId || !this.authConfig.clientId.trim()) {
throw new TypeError('The [clientId] config must have a value.');
}
if (!this.authConfig.clientSecret || !this.authConfig.clientSecret.trim()) {
throw new TypeError('The [clientSecret] config must have a value.');
}
if (this.authConfig.loginStateSecret && this.authConfig.loginStateSecret.length < 32) {
throw new TypeError('The [loginStateSecret] config must have a value of at least 32 characters.');
}
if (!this.authConfig.wristbandApplicationVanityDomain || !this.authConfig.wristbandApplicationVanityDomain.trim()) {
throw new TypeError('The [wristbandApplicationVanityDomain] config must have a value.');
}
if (this.authConfig.tokenExpirationBuffer && this.authConfig.tokenExpirationBuffer < 0) {
throw new TypeError('The [tokenExpirationBuffer] config must be greater than or equal to 0.');
}
}
validateStrictUrlAuthConfigs() {
if (!this.authConfig.loginUrl || !this.authConfig.loginUrl.trim()) {
throw new TypeError('The [loginUrl] config must have a value when auto-configure is disabled.');
}
if (!this.authConfig.redirectUri || !this.authConfig.redirectUri.trim()) {
throw new TypeError('The [redirectUri] config must have a value when auto-configure is disabled.');
}
if (this.authConfig.parseTenantFromRootDomain) {
if (!this.authConfig.loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [loginUrl] must contain the "{tenant_domain}" token when using the [parseTenantFromRootDomain] config.');
}
if (!this.authConfig.redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [redirectUri] must contain the "{tenant_domain}" token when using the [parseTenantFromRootDomain] config.');
}
}
else {
if (this.authConfig.loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [loginUrl] cannot contain the "{tenant_domain}" token when the [parseTenantFromRootDomain] is absent.');
}
if (this.authConfig.redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [redirectUri] cannot contain the "{tenant_domain}" token when the [parseTenantFromRootDomain] is absent.');
}
}
}
validatePartialUrlAuthConfigs() {
if (this.authConfig.loginUrl) {
if (this.authConfig.parseTenantFromRootDomain && !this.authConfig.loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [loginUrl] must contain the "{tenant_domain}" token when using the [parseTenantFromRootDomain] config.');
}
if (!this.authConfig.parseTenantFromRootDomain && this.authConfig.loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [loginUrl] cannot contain the "{tenant_domain}" token when the [parseTenantFromRootDomain] is absent.');
}
}
if (this.authConfig.redirectUri) {
if (this.authConfig.parseTenantFromRootDomain && !this.authConfig.redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [redirectUri] must contain the "{tenant_domain}" token when using the [parseTenantFromRootDomain] config.');
}
if (!this.authConfig.parseTenantFromRootDomain && this.authConfig.redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [redirectUri] cannot contain the "{tenant_domain}" token when the [parseTenantFromRootDomain] is absent.');
}
}
}
// Method to preload and validate all configurations
validateAllDynamicConfigs(sdkConfiguration) {
// Validate that required fields are present in the SDK config response
if (!sdkConfiguration.loginUrl) {
throw new WristbandError('SDK configuration response missing required field: loginUrl');
}
if (!sdkConfiguration.redirectUri) {
throw new WristbandError('SDK configuration response missing required field: redirectUri');
}
// Use manual config values if provided, otherwise use SDK config values
const loginUrl = this.authConfig.loginUrl || sdkConfiguration.loginUrl;
const redirectUri = this.authConfig.redirectUri || sdkConfiguration.redirectUri;
const parseTenantFromRootDomain = this.authConfig.parseTenantFromRootDomain || sdkConfiguration.loginUrlTenantDomainSuffix || '';
// Validate the tenant domain token logic with final resolved values
if (parseTenantFromRootDomain) {
if (!loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new WristbandError('The resolved [loginUrl] must contain the "{tenant_domain}" token when using [parseTenantFromRootDomain].');
}
if (!redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new WristbandError('The resolved [redirectUri] must contain the "{tenant_domain}" token when using [parseTenantFromRootDomain].');
}
}
else {
if (loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new WristbandError('The resolved [loginUrl] cannot contain the "{tenant_domain}" token when [parseTenantFromRootDomain] is absent.');
}
if (redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new WristbandError('The resolved [redirectUri] cannot contain the "{tenant_domain}" token when [parseTenantFromRootDomain] is absent.');
}
}
}
// ////////////////////////////////////
// STATIC CONFIGURATIONS
// ////////////////////////////////////
getClientId() {
return this.authConfig.clientId;
}
getClientSecret() {
return this.authConfig.clientSecret;
}
getLoginStateSecret() {
return this.authConfig.loginStateSecret || this.authConfig.clientSecret;
}
getWristbandApplicationVanityDomain() {
return this.authConfig.wristbandApplicationVanityDomain;
}
getDangerouslyDisableSecureCookies() {
return this.authConfig.dangerouslyDisableSecureCookies ?? false;
}
getScopes() {
return this.authConfig.scopes?.length ? this.authConfig.scopes : DEFAULT_SCOPES;
}
getAutoConfigureEnabled() {
return this.authConfig.autoConfigureEnabled !== false;
}
getTokenExpirationBuffer() {
return this.authConfig.tokenExpirationBuffer ?? DEFAULT_TOKEN_EXPIRATION_BUFFER;
}
// ////////////////////////////////////
// DYNAMIC CONFIGURATIONS
// ////////////////////////////////////
async getCustomApplicationLoginPageUrl() {
// 1. Check if manually provided in authConfig
if (this.authConfig.customApplicationLoginPageUrl) {
return this.authConfig.customApplicationLoginPageUrl;
}
// 2. If auto-configure is enabled, get from SDK config
if (this.getAutoConfigureEnabled()) {
const sdkConfig = await this.loadSdkConfig();
return sdkConfig.customApplicationLoginPageUrl || '';
}
// 3. Default fallback
return '';
}
async getIsApplicationCustomDomainActive() {
// 1. Check if manually provided in authConfig
if (this.authConfig.isApplicationCustomDomainActive !== undefined) {
return this.authConfig.isApplicationCustomDomainActive;
}
// 2. If auto-configure is enabled, get from SDK config
if (this.getAutoConfigureEnabled()) {
const sdkConfig = await this.loadSdkConfig();
return sdkConfig.isApplicationCustomDomainActive ?? false;
}
// 3. Default fallback
return false;
}
async getLoginUrl() {
// 1. Check if manually provided in authConfig
if (this.authConfig.loginUrl) {
return this.authConfig.loginUrl;
}
// 2. If auto-configure is enabled, get from SDK config cache
if (this.getAutoConfigureEnabled()) {
const sdkConfig = await this.loadSdkConfig();
return sdkConfig.loginUrl;
}
// 3. This should not happen if validation is done properly
throw new TypeError('The [loginUrl] config must have a value');
}
async getParseTenantFromRootDomain() {
// 1. Check if manually provided in authConfig
if (this.authConfig.parseTenantFromRootDomain) {
return this.authConfig.parseTenantFromRootDomain;
}
// 2. If auto-configure is enabled, get from SDK config
if (this.getAutoConfigureEnabled()) {
const sdkConfig = await this.loadSdkConfig();
return sdkConfig.loginUrlTenantDomainSuffix || '';
}
// 3. Default fallback
return '';
}
async getRedirectUri() {
// 1. Check if manually provided in authConfig
if (this.authConfig.redirectUri) {
return this.authConfig.redirectUri;
}
// 2. If auto-configure is enabled, get from SDK config cache
if (this.getAutoConfigureEnabled()) {
const sdkConfig = await this.loadSdkConfig();
return sdkConfig.redirectUri;
}
// 3. This should not happen if validation is done properly
throw new TypeError('The [redirectUri] config must have a value');
}
}