UNPKG

@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
"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;