@wristband/nextjs-auth
Version:
SDK for integrating your NextJS application with Wristband. Handles user authentication and token management.
124 lines (123 loc) • 6.04 kB
JavaScript
import retry from 'async-retry';
import { AppRouterAuthHandler } from './app-router/app-router-auth-handler';
import { PageRouterAuthHandler } from './page-router/page-router-auth-handler';
import { WristbandService } from '../services/wristband-service';
import { TENANT_DOMAIN_TOKEN } from '../utils/constants';
import { FetchError, WristbandError } from '../error';
/**
* WristbandAuth is a utility class providing methods for seamless interaction with the Wristband authentication service.
* @implements {WristbandAuth}
*/
export class WristbandAuthImpl {
/**
* Creates an instance of WristbandAuth.
*
* @param {AuthConfig} authConfig The configuration for Wristband authentication.
*/
constructor(authConfig) {
this.appRouter = {
login: (req, loginConfig) => {
return this.appRouterAuthHandler.login(req, loginConfig);
},
callback: (req) => {
return this.appRouterAuthHandler.callback(req);
},
logout: (req, logoutConfig) => {
return this.appRouterAuthHandler.logout(req, logoutConfig);
},
createCallbackResponse: (req, redirectUrl) => {
return this.appRouterAuthHandler.createCallbackResponse(req, redirectUrl);
},
};
this.pageRouter = {
login: (req, res, loginConfig) => {
return this.pageRouterAuthHandler.login(req, res, loginConfig);
},
callback: (req, res) => {
return this.pageRouterAuthHandler.callback(req, res);
},
logout: (req, res, logoutConfig) => {
return this.pageRouterAuthHandler.logout(req, res, logoutConfig);
},
};
if (!authConfig.clientId) {
throw new TypeError('The [clientId] config must have a value.');
}
if (!authConfig.clientSecret) {
throw new TypeError('The [clientSecret] config must have a value.');
}
if (!authConfig.loginStateSecret || authConfig.loginStateSecret.length < 32) {
throw new TypeError('The [loginStateSecret] config must have a value of at least 32 characters.');
}
if (!authConfig.loginUrl) {
throw new TypeError('The [loginUrl] config must have a value.');
}
if (!authConfig.redirectUri) {
throw new TypeError('The [redirectUri] config must have a value.');
}
if (!authConfig.wristbandApplicationDomain) {
throw new TypeError('The [wristbandApplicationDomain] config must have a value.');
}
if (authConfig.useTenantSubdomains) {
if (!authConfig.rootDomain) {
throw new TypeError('The [rootDomain] config must have a value when using tenant subdomains.');
}
if (!authConfig.loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [loginUrl] must contain the "{tenant_domain}" token when using tenant subdomains.');
}
if (!authConfig.redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [redirectUri] must contain the "{tenant_domain}" token when using tenant subdomains.');
}
}
else {
if (authConfig.loginUrl.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [loginUrl] must contain the "{tenant_domain}" token when using tenant subdomains.');
}
if (authConfig.redirectUri.includes(TENANT_DOMAIN_TOKEN)) {
throw new TypeError('The [redirectUri] must contain the "{tenant_domain}" token when using tenant subdomains.');
}
}
const wristbandServiceImpl = new WristbandService(authConfig.wristbandApplicationDomain, authConfig.clientId, authConfig.clientSecret);
this.wristbandService = wristbandServiceImpl;
this.appRouterAuthHandler = new AppRouterAuthHandler(authConfig, wristbandServiceImpl);
this.pageRouterAuthHandler = new PageRouterAuthHandler(authConfig, wristbandServiceImpl);
}
async refreshTokenIfExpired(refreshToken, expiresAt) {
// Safety checks
if (!refreshToken) {
throw new TypeError('Refresh token must be a valid string');
}
if (!expiresAt || expiresAt < 0) {
throw new TypeError('The expiresAt field must be an integer greater than 0');
}
if (Date.now().valueOf() <= expiresAt) {
return null;
}
// Try up to 3 times to perform a token refresh.
let tokenResponse = null;
await retry(async (bail) => {
try {
tokenResponse = await this.wristbandService.refreshToken(refreshToken);
}
catch (error) {
if (error instanceof FetchError &&
error.response &&
error.response.status >= 400 &&
error.response.status < 500) {
const errorDescription = error.body && error.body.error_description ? error.body.error_description : 'Invalid Refresh Token';
// Only 4xx errors should short-circuit the retry loop early.
bail(new WristbandError('invalid_refresh_token', errorDescription));
return;
}
// Retry any 5xx errors.
throw new WristbandError('unexpected_error', 'Unexpected Error');
}
}, { retries: 2, minTimeout: 100, maxTimeout: 100 });
if (tokenResponse) {
const { access_token: accessToken, id_token: idToken, expires_in: expiresIn, refresh_token: responseRefreshToken, } = tokenResponse;
return { accessToken, idToken, refreshToken: responseRefreshToken, expiresIn };
}
// This is merely a safety check, but this should never happen.
throw new WristbandError('unexpected_error', 'Unexpected Error');
}
}