@wristband/nextjs-auth
Version:
SDK for integrating your Next.js application with Wristband. Handles user authentication, session management, and token management.
292 lines (291 loc) • 17.5 kB
TypeScript
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextRequest, NextResponse } from 'next/server';
import { SessionData, SessionOptions } from '@wristband/typescript-session';
import type { AuthMiddlewareConfig, CallbackResult, LoginConfig, LogoutConfig, NextJsCookieStore, ServerActionAuthResult, TokenData } from '../types';
/**
* WristbandAuth is a utility interface providing methods for seamless interaction with Wristband for authenticating
* application users. It can handle the following:
* - Initiate a login request by redirecting to Wristband.
* - Receive callback requests from Wristband to complete a login request.
* - Retrive all necessary JWT tokens and userinfo to start an application session.
* - Logout a user from the application by revoking refresh tokens and redirecting to Wristband.
* - Checking for expired access tokens and refreshing them automatically, if necessary.
*/
export interface WristbandAuth {
/**
* App Router authentication handlers for Next.js 13+ App Router.
* Provides login, callback, logout, and response creation methods that work with Next.js App Router APIs.
*/
appRouter: {
/**
* Initiates a login request by redirecting to Wristband. An authorization request is constructed
* for the user attempting to login in order to start the Authorization Code flow.
*
* Your request can contain Wristband-specific query parameters:
* - login_hint: A hint to Wristband about user's preferred login identifier. This can be appended as a query
* parameter in the redirect request to the Authorize URL.
* - return_url: The location of where to send users after authenticating.
* - tenant_custom_domain: The tenant custom domain for the tenant that the user belongs to, if applicable. Should be
* used as the domain of the authorize URL when present.
* - tenant_: The name of the tenant the user belongs to. Should be used in the tenant vanity domain of
* the authorize URL when not utilizing tenant subdomains nor tenant custom domains.
*
* @param {NextRequest} request The request object.
* @param {LoginConfig} [config] Additional configuration for creating an auth request to Wristband.
* @returns {Promise<NextResponse>} A Promise with the NextResponse that is peforming the URL redirect to Wristband.
*/
login: (request: NextRequest, loginConfig?: LoginConfig) => Promise<NextResponse>;
/**
* Receives incoming requests from Wristband with an authorization code. It will then proceed to exchange the auth
* code for an access token as well as fetch the userinfo for the user attempting to login.
*
* Your request can contain Wristband-specific query parameters:
* - code: The authorization code to use for exchanging for an access token.
* - error: An error code indicating that some an issue occurred during the login process.
* - error_description: A plaintext description giving more detail around the issue that occurred during the login
* process.
* - state: The state value that was originally sent to the Authorize URL.
* - tenant_custom_domain: If the tenant has a tenant custom domain defined, then this query parameter will be part
* of the incoming request to the Callback Endpoint. n the event a redirect to the Login Endpoint is required, then
* this should be appended as a query parameter when redirecting to the Login Endpoint.
* - tenant_name: The name of the tenant the user belongs to. In the event a redirect to the Login Endpoint
* is required and neither tenant subdomains nor tenant custom domains are not being utilized, then this should be
* appended as a query parameter when redirecting to the Login Endpoint.
*
* @param {Request} request The request object.
* @returns {Promise<CallbackResult>} A Promise containing the result of what happened during callback execution
* as well as any accompanying data.
* @throws {WristbandError} If an error occurs during the callback handling.
*/
callback: (request: NextRequest) => Promise<CallbackResult>;
/**
* Revokes the user's refresh token and redirects them to the Wristband logout endpoint to destroy
* their authenticated session in Wristband.
*
* @param {NextRequest} request The request object.
* @param {LogoutConfig} [config] Additional configuration for logging out the user.
* @returns {Promise<NextResponse>} A Promise with the NextResponse that is peforming the URL redirect to Wristband.
* @throws {Error} If an error occurs during the logout process.
*/
logout: (request: NextRequest, logoutConfig?: LogoutConfig) => Promise<NextResponse>;
/**
* Constructs the redirect response to your application and cleans up the login state.
*
* @param {NextRequest} request The request object.
* @param {string} redirectUrl The location for your application that you want to send users to.
* @returns {NextResponse} The NextResponse that is peforming the URL redirect to your desired application URL.
*/
createCallbackResponse: (request: NextRequest, redirectUrl: string) => Promise<NextResponse>;
/**
* Creates a configured Server Action authentication helper.
*
* Factory function that returns a reusable authentication helper for Server Actions.
* Configure once with session options, then use the returned function in multiple Server Actions
* to validate authentication, refresh tokens, and retrieve session data.
*
* **Note:** Server Actions have built-in CSRF protection via Origin/Host header comparison,
* so CSRF token validation is not performed.
*
* @param config - Configuration object
* @param config.sessionOptions - Session configuration options (secrets, cookie settings, etc.)
* @returns A function that checks authentication and returns a result
*
* @example
* ```typescript
* // Create a configured auth helper (once per app)
* const requireServerActionAuth = wristbandAuth.appRouter.createServerActionAuth({
* sessionOptions: {
* secrets: process.env.SESSION_SECRET!,
* cookieName: 'my-session',
* maxAge: 24 * 60 * 60
* }
* });
*
* // Use in Server Actions
* 'use server'
* import { cookies } from 'next/headers';
*
* export async function updateProfile(formData: FormData) {
* const cookieStore = await cookies();
* const { authenticated, session, reason } = await requireServerActionAuth(cookieStore);
*
* if (!authenticated) {
* return { message: 'Not authenticated', authError: true };
* }
*
* // Session is validated and tokens are refreshed automatically
* const userId = session.userId;
* // ... update profile
* }
* ```
*
* @see {@link ServerActionAuthResult} for the return type details
*/
createServerActionAuth: <T extends SessionData = SessionData>(config: {
sessionOptions: SessionOptions;
}) => (cookieStore: NextJsCookieStore) => Promise<ServerActionAuthResult<T>>;
};
/**
* Pages Router authentication handlers for Next.js Pages Router (API routes and getServerSideProps).
* Provides login, callback, and logout methods that work with Next.js Pages Router APIs.
*/
pagesRouter: {
/**
* Initiates a login request by redirecting to Wristband. An authorization request is constructed
* for the user attempting to login in order to start the Authorization Code flow.
*
* Your request can contain Wristband-specific query parameters:
* - login_hint: A hint to Wristband about user's preferred login identifier. This can be appended as a query
* parameter in the redirect request to the Authorize URL.
* - return_url: The location of where to send users after authenticating.
* - tenant_custom_domain: The tenant custom domain for the tenant that the user belongs to, if applicable. Should be
* used as the domain of the authorize URL when present.
* - tenant_name: The name of the tenant the user belongs to. Should be used in the tenant vanity domain of
* the authorize URL when not utilizing tenant subdomains nor tenant custom domains.
*
* @param {Request} request The request object.
* @param {Response} response The response object.
* @param {LoginConfig} [config] Additional configuration for creating an auth request to Wristband.
* @returns {Promise<string>} A Promise with the Wristband authorize URL that your app should redirect to.
*/
login: (request: NextApiRequest, response: NextApiResponse, loginConfig?: LoginConfig) => Promise<string>;
/**
* Receives incoming requests from Wristband with an authorization code. It will then proceed to exchange the auth
* code for an access token as well as fetch the userinfo for the user attempting to login.
*
* Your request can contain Wristband-specific query parameters:
* - code: The authorization code to use for exchanging for an access token.
* - error: An error code indicating that some an issue occurred during the login process.
* - error_description: A plaintext description giving more detail around the issue that occurred during the login
* process.
* - state: The state value that was originally sent to the Authorize URL.
* - tenant_custom_domain: If the tenant has a tenant custom domain defined, then this query parameter will be part
* of the incoming request to the Callback Endpoint. n the event a redirect to the Login Endpoint is required, then
* this should be appended as a query parameter when redirecting to the Login Endpoint.
* - tenant_name: The name of the tenant the user belongs to. In the event a redirect to the Login Endpoint
* is required and neither tenant subdomains nor tenant custom domains are not being utilized, then this should be
* appended as a query parameter when redirecting to the Login Endpoint.
*
* @param {Request} request The request object.
* @param {Response} response The response object.
* @param {CallbackConfig} [config] Additional configuration for handling auth callbacks from Wristband.
* @returns {Promise<CallbackResult>} A Promise containing the result of what happened during callback execution
* as well as any accompanying data.
* @throws {WristbandError} If an error occurs during the callback handling.
*/
callback: (request: NextApiRequest, response: NextApiResponse) => Promise<CallbackResult>;
/**
* Revokes the user's refresh token and returns a redirect URL to the Wristband logout endpoint, where
* their authenticated session in Wristband gets destroy.
*
* @param {Request} request The request object.
* @param {Response} response The response object.
* @param {LogoutConfig} [config] Additional configuration for logging out the user.
* @returns {Promise<string>} A Promise with the Wristband logout URL that your app should redirect to.
*/
logout: (request: NextApiRequest, response: NextApiResponse, logoutConfig?: LogoutConfig) => Promise<string>;
};
/**
* Checks if the user's access token is expired and refreshed the token, if necessary. Works for both
* Pages and App Router.
*
* @param {string} refreshToken The refresh token.
* @param {number} expiresAt Unix timestamp in milliseconds at which the token expires.
* @returns {Promise<TokenData | null>} A Promise with the data from the token endpoint if the token was refreshed.
* Otherwise, a Promise with null value is returned.
* @throws {Error} If an error occurs during the token refresh process.
*/
refreshTokenIfExpired: (refreshToken: string, expiresAt: number) => Promise<TokenData | null>;
/**
* Creates a Next.js middleware function that handles authentication and session management
* for protected routes in your application. Works for both Pages and App Router.
*
* This middleware:
* - Checks if routes require authentication based on your configuration
* - Validates user sessions for protected routes
* - Optionally enforces CSRF token protection for API routes (if enabled)
* - Automatically refreshes expired access tokens (if refresh tokens are in your session data)
* - Handles unauthenticated requests appropriately (401 for APIs, your own custom handler for pages)
* - Preserves headers and cookies from previous middleware when chained
*
* @template T - Session data type extending SessionData
* @param config - Configuration for the authentication middleware
* @param config.authStrategies - Authentication strategies to use (SESSION, JWT, or both)
* @param config.sessionConfig - Session strategy configuration (required if using SESSION)
* @param config.sessionConfig.sessionOptions - Session options including secrets and cookie settings
* @param config.sessionConfig.csrfTokenHeaderName - Header name for CSRF token (default: 'X-CSRF-TOKEN')
* @param config.sessionConfig.sessionEndpoint - Path to session endpoint (default: '/api/auth/session')
* @param config.sessionConfig.tokenEndpoint - Path to token endpoint (default: '/api/auth/token')
* @param config.jwtConfig - JWT strategy configuration (optional)
* @param config.jwtConfig.jwksCacheMaxSize - Maximum number of JWKs to cache (default: 20)
* @param config.jwtConfig.jwksCacheTtl - Cache TTL in milliseconds (default: 3600000)
* @param config.protectedPages - Array of page route patterns requiring authentication (supports regex)
* @param config.protectedApis - Array of API route patterns requiring authentication (default: empty array)
* @param config.onPageUnauthenticated - Callback to handle unauthenticated page requests (e.g., redirect to login)
* @returns A Next.js middleware function that accepts an optional response parameter for middleware chaining
*
* @example
* ```typescript
* // Basic usage - wristband.ts
* import { createWristbandAuth } from '@wristband/nextjs-auth';
*
* const wristbandAuth = createWristbandAuth({ ... });
*
* export const requireWristbandSession = wristbandAuth.createMiddlewareAuth({
* sessionOptions: { secrets: process.env.SESSION_SECRET!, enableCsrfProtection: true },
* protectedPages: ['/dashboard(.*)', '/settings(.*)'],
* protectedApis: ['/api/users/.*', '/api/admin/.*'],
* onPageUnauthenticated: (request) => {
* const loginUrl = new URL('/api/auth/login', request.url);
* loginUrl.searchParams.set('return_url', request.nextUrl.pathname);
* return NextResponse.redirect(loginUrl);
* }
* });
*
* // middleware.ts
* import { requireWristbandSession } from '@/wristband';
*
* export async function middleware(request: NextRequest) {
* return await requireWristbandSession(request);
* }
*
* export const config = {
* matcher: ['/((?!_next|fonts|[\\w-]+\\.\\w+).*)']
* };
* ```
*
* @example
* ```typescript
* // Chaining with other middleware - preserves headers/cookies
* import { requireWristbandSession } from '@/wristband';
* import { customMiddleware01, customMiddleware02 } from '@/lib/custom-middleware';
*
* export async function middleware(request: NextRequest) {
* // First middleware sets some headers
* const customResponse = await customMiddleware01(request);
*
* // Wristband middleware preserves those headers
* const wristbandResponse = await requireWristbandSession(request, customResponse);
*
* // You can pass the Wristband response to other middlewares that suppport chaining.
* return await customMiddleware02(request, wristbandResponse);
* }
* ```
*
* @example
* ```typescript
* // With custom session data type
* interface MySessionData extends SessionData {
* theme: string;
* roles: string[];
* }
*
* export const requireWristbandSession = wristbandAuth.createMiddlewareAuth<MySessionData>({
* sessionOptions: { secrets: process.env.SESSION_SECRET! },
* protectedPages: ['/dashboard(.*)'],
* onPageUnauthenticated: (request) => NextResponse.redirect(new URL('/login', request.url))
* });
* ```
*/
createMiddlewareAuth: <T extends SessionData = SessionData>(config: AuthMiddlewareConfig) => (request: NextRequest, previousResponse?: NextResponse) => Promise<NextResponse>;
}