@wristband/nextjs-auth
Version:
SDK for integrating your Next.js application with Wristband. Handles user authentication, session management, and token management.
270 lines (269 loc) • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WristbandAuthImpl = void 0;
const server_1 = require("next/server");
const typescript_jwt_1 = require("@wristband/typescript-jwt");
const config_resolver_1 = require("../config-resolver");
const app_router_auth_handler_1 = require("./handlers/app-router-auth-handler");
const pages_router_auth_handler_1 = require("./handlers/pages-router-auth-handler");
const session_1 = require("../session");
const common_utils_1 = require("../utils/auth/common-utils");
const middleware_1 = require("../utils/middleware");
const wristband_service_1 = require("../wristband-service");
/**
* WristbandAuth is a utility class providing methods for seamless interaction with the Wristband authentication service.
* @implements {WristbandAuth}
*/
class WristbandAuthImpl {
/**
* Creates an instance of WristbandAuth.
*
* @param {AuthConfig} authConfig The configuration for Wristband authentication.
*/
constructor(authConfig) {
/**
* @see {@link WristbandAuth.appRouter}
*/
this.appRouter = {
/**
* @see {@link WristbandAuth.appRouter} for full documentation
*/
login: (request, loginConfig) => {
return this.appRouterAuthHandler.login(request, loginConfig);
},
/**
* @see {@link WristbandAuth.appRouter} for full documentation
*/
callback: (request) => {
return this.appRouterAuthHandler.callback(request);
},
/**
* @see {@link WristbandAuth.appRouter} for full documentation
*/
logout: (request, logoutConfig) => {
return this.appRouterAuthHandler.logout(request, logoutConfig);
},
/**
* @see {@link WristbandAuth.appRouter} for full documentation
*/
createCallbackResponse: (request, redirectUrl) => {
return this.appRouterAuthHandler.createCallbackResponse(request, redirectUrl);
},
/**
* @see {@link WristbandAuth.appRouter} for full documentation
*/
createServerActionAuth: (config) => {
if (!config || !config.sessionOptions) {
throw new TypeError('Session options are a required configuration.');
}
return async (cookieStore) => {
return this.appRouterAuthHandler.createServerActionAuth(cookieStore, config.sessionOptions);
};
},
};
/**
* @see {@link WristbandAuth.pagesRouter}
*/
this.pagesRouter = {
/**
* @see {@link WristbandAuth.pagesRouter} for full documentation
*/
login: (request, response, loginConfig) => {
return this.pagesRouterAuthHandler.login(request, response, loginConfig);
},
/**
* @see {@link WristbandAuth.pagesRouter} for full documentation
*/
callback: (request, response) => {
return this.pagesRouterAuthHandler.callback(request, response);
},
/**
* @see {@link WristbandAuth.pagesRouter} for full documentation
*/
logout: (request, response, logoutConfig) => {
return this.pagesRouterAuthHandler.logout(request, response, logoutConfig);
},
};
this.configResolver = new config_resolver_1.ConfigResolver(authConfig);
this.wristbandService = new wristband_service_1.WristbandService(this.configResolver.getWristbandApplicationVanityDomain(), this.configResolver.getClientId(), this.configResolver.getClientSecret());
this.appRouterAuthHandler = new app_router_auth_handler_1.AppRouterAuthHandler(this.configResolver, this.wristbandService);
this.pagesRouterAuthHandler = new pages_router_auth_handler_1.PagesRouterAuthHandler(this.configResolver, this.wristbandService);
}
/**
* @see {@link WristbandAuth.refreshTokenIfExpired}
*/
async refreshTokenIfExpired(refreshToken, expiresAt) {
// Fetch SDK Configs
const tokenExpirationBuffer = this.configResolver.getTokenExpirationBuffer();
return (0, common_utils_1.refreshExpiredToken)(refreshToken, expiresAt, this.wristbandService, tokenExpirationBuffer);
}
/**
* @see {@link WristbandAuth.createMiddlewareAuth}
*/
createMiddlewareAuth(config) {
const normalizedConfig = (0, middleware_1.normalizeMiddlewareConfig)(config);
return async (request, previousResponse) => {
// Check if this route needs protection
const isProtectedApiRoute = (0, middleware_1.isProtectedApi)(request.nextUrl.pathname, normalizedConfig);
const isProtectedPageRoute = (0, middleware_1.isProtectedPage)(request, normalizedConfig);
// If not protected, don't copy anything -- just continue
if (!(isProtectedApiRoute || isProtectedPageRoute)) {
return previousResponse || server_1.NextResponse.next();
}
// Check if this is a session/token endpoint - force SESSION strategy only
const isSessionEndpoint = request.nextUrl.pathname === normalizedConfig.sessionConfig.sessionEndpoint;
const isTokenEndpoint = request.nextUrl.pathname === normalizedConfig.sessionConfig.tokenEndpoint;
const isWristbandAuthEndpoint = isSessionEndpoint || isTokenEndpoint;
// For session/token endpoints, ONLY use SESSION strategy (prevents JWT from breaking them)
const strategiesToTry = isWristbandAuthEndpoint ? ['SESSION'] : normalizedConfig.authStrategies;
// Try all auth strategies in the sequential order in which they were provided.
let result = { authenticated: false, reason: 'not_authenticated' };
for (let i = 0; i < strategiesToTry.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
result = await this.tryAuthStrategy(request, strategiesToTry[i], normalizedConfig, isProtectedApiRoute);
if (result.authenticated) {
break;
}
}
// If no strategy succeeded, handle the auth faiure accordingly.
if (!result.authenticated) {
const failureResponse = await this.getAuthFailureResponse(request, result.reason, isProtectedApiRoute, normalizedConfig);
return previousResponse ? (0, middleware_1.copyResponseHeaders)(previousResponse, failureResponse) : failureResponse;
}
const finalResponse = previousResponse || server_1.NextResponse.next();
// Save session/CSRF cookie headers only if we used SESSION strategy
if (result.usedStrategy === 'SESSION' && result.session) {
const sessionResponse = await result.session.saveToResponse(new Response());
return (0, middleware_1.copyResponseHeaders)(sessionResponse, finalResponse);
}
return finalResponse;
};
}
/**
* Lazily initializes and returns the JWT validator instance.
* Only creates the validator on first use if JWT strategy is configured.
*/
getJwtValidator(jwtConfig) {
if (!this.jwtValidator) {
const wristbandApplicationVanityDomain = this.configResolver.getWristbandApplicationVanityDomain();
this.jwtValidator = (0, typescript_jwt_1.createWristbandJwtValidator)({
wristbandApplicationVanityDomain,
jwksCacheMaxSize: jwtConfig?.jwksCacheMaxSize,
jwksCacheTtl: jwtConfig?.jwksCacheTtl,
});
}
return this.jwtValidator;
}
/**
* Attempts to authenticate a request using a single configured auth strategy.
*
* This evaluates the provided strategy in isolation and reports whether it
* succeeded or failed with a specific reason. Normal authentication failures
* are returned as structured results rather than thrown, allowing the caller
* to orchestrate fallback strategies and proper HTTP error responses.
*
* @template T - Session data type extending SessionData
* @param request - The incoming Next.js request to authenticate.
* @param strategy - The auth strategy to apply for this attempt.
* @param normalizedConfig - The fully normalized middleware configuration.
* @param isProtectedApiRoute - Indicates whether the current path is a protected API route.
* @returns A structured result describing authentication outcome, session (if successful), strategy used, and failure reason (if failed).
*/
async tryAuthStrategy(request, strategy, normalizedConfig, isProtectedApiRoute) {
if (strategy === 'SESSION') {
const { csrfTokenHeaderName, sessionOptions } = normalizedConfig.sessionConfig;
try {
const session = await (0, session_1.getSessionFromRequest)(request, sessionOptions);
if (!session.isAuthenticated) {
return { authenticated: false, reason: 'not_authenticated' };
}
// CSRF validation (only for API routes)
if (isProtectedApiRoute && sessionOptions?.enableCsrfProtection) {
const csrfValid = (0, middleware_1.isValidCsrf)(request, session.csrfToken, csrfTokenHeaderName);
if (!csrfValid) {
return { authenticated: false, reason: 'csrf_failed' };
}
}
// Try to refresh token if expired
if (session.refreshToken && session.expiresAt !== undefined) {
try {
const newTokenData = await this.refreshTokenIfExpired(session.refreshToken, session.expiresAt);
if (newTokenData) {
session.accessToken = newTokenData.accessToken;
session.refreshToken = newTokenData.refreshToken;
session.expiresAt = newTokenData.expiresAt;
}
}
catch (error) {
return { authenticated: false, reason: 'token_refresh_failed' };
}
}
return { authenticated: true, session, usedStrategy: 'SESSION' };
}
catch (error) {
return { authenticated: false, reason: 'unexpected_error' };
}
}
if (strategy === 'JWT') {
try {
const jwtValidator = this.getJwtValidator(normalizedConfig.jwtConfig);
const authHeader = request.headers.get('authorization');
if (!authHeader) {
return { authenticated: false, reason: 'not_authenticated' };
}
const bearerToken = jwtValidator.extractBearerToken(authHeader);
if (!bearerToken) {
return { authenticated: false, reason: 'not_authenticated' };
}
const validationResult = await jwtValidator.validate(bearerToken);
if (!validationResult.isValid) {
return { authenticated: false, reason: 'not_authenticated' };
}
return { authenticated: true, usedStrategy: 'JWT' };
}
catch (error) {
return { authenticated: false, reason: 'unexpected_error' };
}
}
// Should never reach here
return { authenticated: false, reason: 'unexpected_error' };
}
/**
* Creates the appropriate failure response based on the authentication failure reason
* and whether the request is for an API route or page route.
*
* @param request - The incoming request
* @param reason - Why authentication failed
* @param isProtectedApiRoute - Whether this is a protected API route
* @param normalizedConfig - The normalized middleware configuration
* @returns NextResponse with appropriate status code or redirect
*/
async getAuthFailureResponse(request, reason, isProtectedApiRoute, normalizedConfig) {
if (isProtectedApiRoute) {
// Return appropriate HTTP status based on failure reason
let status;
let errorMessage;
switch (reason) {
case 'unexpected_error':
status = 500;
errorMessage = 'Internal Server Error';
break;
case 'csrf_failed':
status = 403;
errorMessage = 'Forbidden';
break;
case 'not_authenticated':
case 'token_refresh_failed':
default:
status = 401;
errorMessage = 'Unauthorized';
}
return server_1.NextResponse.json({ error: errorMessage }, { status });
}
// Protected page route - invoke the unauthenticated handler
const loginUrl = await this.configResolver.getLoginUrl();
const onPageUnauthenticatedHandler = (0, middleware_1.resolveOnPageUnauthenticated)(normalizedConfig, loginUrl);
return onPageUnauthenticatedHandler(request, reason);
}
}
exports.WristbandAuthImpl = WristbandAuthImpl;