UNPKG

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