UNPKG

@wristband/nextjs-auth

Version:

SDK for integrating your Next.js application with Wristband. Handles user authentication, session management, and token management.

1,171 lines (886 loc) β€’ 140 kB
<div align="center"> <a href="https://wristband.dev"> <picture> <img src="https://assets.wristband.dev/images/email_branding_logo_v1.png" alt="Github" width="297" height="64"> </picture> </a> <p align="center"> Enterprise-ready auth that is secure by default, truly multi-tenant, and ungated for small businesses. </p> <p align="center"> <b> <a href="https://wristband.dev">Website</a> β€’ <a href="https://docs.wristband.dev/">Documentation</a> </b> </p> </div> <br/> --- <br/> # Wristband Multi-Tenant Authentication SDK for Next.js [![npm package](https://img.shields.io/badge/npm%20i-nextjs--auth-brightgreen)](https://www.npmjs.com/package/@wristband/nextjs-auth) [![version number](https://img.shields.io/github/v/release/wristband-dev/nextjs-auth?color=green&label=version)](https://github.com/wristband-dev/nextjs-auth/releases) [![License](https://img.shields.io/github/license/wristband-dev/nextjs-auth)](https://github.com/wristband-dev/nextjs-auth/blob/main/LICENSE.md) [![Actions Status](https://github.com/wristband-dev/nextjs-auth/workflows/Test/badge.svg)](https://github.com/wristband-dev/nextjs-auth/actions) Enterprise-ready authentication for multi-tenant [Next.js applications](https://nextjs.org/) using OAuth 2.1 and OpenID Connect standards. It works for both the Next.js App Router and Pages Router. <br> ## Overview This SDK provides complete authentication integration with Wristband, including: - **Login flow** - Redirect to Wristband and handle OAuth callbacks - **Session management** - Encrypted cookie-based sessions with optional CSRF token protection - **Token handling** - Automatic access token refresh and validation - **Logout flow** - Token revocation and session cleanup - **Multi-tenancy** - Support for tenant subdomains and custom domains Learn more about Wristband's authentication patterns: - [Backend Server Integration Pattern](https://docs.wristband.dev/docs/backend-server-integration) - [Login Workflow In Depth](https://docs.wristband.dev/docs/login-workflow) > **πŸ’‘ Learn by Example** > > Want to see the SDK in action? Check out our [Next.js demo applications](#wristband-multi-tenant-nextjs-demo-apps). The demos showcase real-world authentication patterns and best practices. <br> --- <br> ## Table of Contents - [Migrating From Older SDK Versions](#migrating-from-older-sdk-versions) - [Prerequisites](#prerequisites) - [Installation](#installation) - [Usage](#usage) - [1) Initialize the Auth SDK](#1-initialize-the-auth-sdk) - [2) Set Up Session Management](#2-set-up-session-management) - [3) Add Auth Endpoints](#3-add-auth-endpoints) - [Login Endpoint](#login-endpoint) - [Callback Endpoint](#callback-endpoint) - [Logout Endpoint](#logout-endpoint) - [Session Endpoint](#session-endpoint) - [Token Endpoint (Optional)](#token-endpoint-optional) - [4) Protect Your Pages, Actions, and APIs](#4-protect-your-pages-actions-and-apis) - [Set Up Authentication Middleware](#set-up-authentication-middleware) - [Protect Server Actions (App Router Only)](#protect-server-actions-app-router-only) - [Manual Session Access (Optional)](#manual-session-access-optional) - [5) Pass Your Access Token to Downstream APIs](#5-pass-your-access-token-to-downstream-apis) - [Wristband Auth Configuration Options](#wristband-auth-configuration-options) - [Auth Config Options](#auth-config-options) - [createWristbandAuth()](#createwristbandauth) - [Auth API](#auth-api) - [login()](#login) - [callback()](#callback) - [createCallbackResponse() (App Router)](#createcallbackresponse-app-router) - [logout()](#logout) - [refreshTokenIfExpired()](#refreshtokenifexpired) - [Session Management](#session-management) - [Session Configuration](#session-configuration) - [The Session Object](#the-session-object) - [Session Helper Functions](#session-helper-functions) - [getSessionFromRequest()](#getsessionfromrequest) - [getPagesRouterSession()](#getpagesroutersession) - [getReadOnlySessionFromCookies()](#getreadonlysessionfromcookies) - [getMutableSessionFromCookies()](#getmutablesessionfromcookies) - [saveSessionWithCookies()](#savesessionwithcookies) - [destroySessionWithCookies()](#destroysessionwithcookies) - [Session Access Patterns](#session-access-patterns) - [Session API](#session-api) - [session.fromCallback()](#sessionfromcallbackcallbackdata-customfields) - [session.save()](#sessionsave) - [session.saveToResponse()](#sessionsavetoresponseresponse) - [session.destroy()](#sessiondestroy) - [session.destroyToResponse()](#sessiondestroytoresponseresponse) - [session.getSessionResponse()](#sessiongetsessionresponse) - [session.getTokenResponse()](#sessiongettokenresponse) - [CSRF Protection](#csrf-protection) - [Authentication Middleware](#authentication-middleware) - [createMiddlewareAuth()](#createmiddlewareauth) - [SESSION Strategy](#session-strategy) - [JWT Strategy](#jwt-strategy) - [Middleware Chaining](#middleware-chaining) - [createServerActionAuth()](#createserveractionauth) - [Related Wristband SDKs](#related-wristband-sdks) - [Wristband Multi-Tenant Next.js Demo Apps](#wristband-multi-tenant-nextjs-demo-apps) - [Questions](#questions) <br> ## Migrating From Older SDK Versions On an older version of our SDK? Check out our migration guide: - [Instructions for migrating to Version 4.x](migration/v4/README.md) - [Instructions for migrating to Version 3.x](migration/v3/README.md) - [Instructions for migrating to Version 2.x](migration/v2/README.md) <br> ## Prerequisites > **⚑ Try Our Next.js Quickstart!** > > For the fastest way to get started with Next.js authentication, follow our [Quick Start Guide](https://docs.wristband.dev/docs/auth-quick-start). It walks you through setting up a working Next.js app with Wristband authentication in minutes. Refer back to this README for comprehensive documentation and advanced usage patterns. Before installing, ensure you have: - [Node.js](https://nodejs.org/en) >= 20.0.0 - [Next.js](https://nextjs.org/) >= 14.0.0 - Your preferred package manager (npm >= 9.6.0, yarn, pnpm, etc.) <br> ## Installation ```bash # With npm npm install @wristband/nextjs-auth # Or with yarn yarn add @wristband/nextjs-auth # Or with pnpm pnpm add @wristband/nextjs-auth ``` <br> ## Usage ### 1) Initialize the Auth SDK First, create an instance of `WristbandAuth` in your Next.js directory structure in any location of your choice (i.e. `src/wristband.ts`). Then, you can export this instance and use it across your project. When creating an instance, you provide all necessary configurations for your application to correlate with how you've set it up in the Wristband Dashboard. ```typescript // src/wristband.ts import { createWristbandAuth } from '@wristband/nextjs-auth'; /** * Wristband authentication instance for handling login, callback, and logout flows. */ export const wristbandAuth = createWristbandAuth({ clientId: "replace-me-with-your-client-id", clientSecret: "replace-me-with-your-client-secret", wristbandApplicationVanityDomain: "replace-me-with-your-vanity-domain", }); ``` > **πŸ’‘ Disabling Secure Cookies in Local Development** > > By default, `WristbandAuth` creates secure cookies (for tracking login state), meaning they are only sent over HTTPS connections. Most browsers make an exception for localhost and allow secure cookies to be sent over HTTP (e.g., http://localhost). However, some browsers, such as Safari, enforce stricter rules and never send secure cookies over HTTP, even for localhost. If you need to disable the secure cookies for local development, set `dangerouslyDisableSecureCookies: true`. However, be sure to **re-enable secure cookies before deploying to production**. <br> ### 2) Set Up Session Management Wristband provides encrypted cookie-based session management built directly into this SDK, powered by [@wristband/typescript-session](https://github.com/wristband-dev/typescript-session). Add basic session configuration to enable the auth endpoints (Login, Callback, etc.) in the next steps. #### App Router ```typescript // src/wristband.ts (continued - add to existing file) import { NextRequest } from 'next/server'; import { getSessionFromRequest, SessionOptions } from '@wristband/nextjs-auth'; // ... /** * Session configuration for authentication. * * IMPORTANT: Use a strong 32+ character secret in production and set secure: true */ const sessionOptions: SessionOptions = { secrets: 'dummyval-b5c1-463a-812c-0d8db87c0ec5', // 32+ character secret maxAge: 3600, // 1 hour in seconds secure: process.env.NODE_ENV === 'production', // Must be true in Production }; /** * Retrieves the session from a NextRequest. * * Use in: * - App Router API Route Handlers * - Middleware/proxy functions */ export function getRequestSession(request: NextRequest) { return getSessionFromRequest(request, sessionOptions); } ``` #### Pages Router ```typescript // src/wristband.ts (continued - add to existing file) import * as http from 'http'; import { getPagesRouterSession, SessionOptions } from '@wristband/nextjs-auth'; // ... /** * Session configuration for authentication. * * IMPORTANT: Use a strong 32+ character secret in production and set secure: true */ const sessionOptions: SessionOptions = { secrets: 'dummyval-b5c1-463a-812c-0d8db87c0ec5', // 32+ character secret maxAge: 3600, // 1 hour in seconds secure: process.env.NODE_ENV === 'production', // Must be true in Production }; /** * Retrieves session from Pages Router API routes and SSR functions. * * Use in: * - Pages Router API Route Handlers * - getServerSideProps() */ export function getSession(req: http.IncomingMessage, res: http.ServerResponse) { return getPagesRouterSession(req, res, sessionOptions); } ``` <br> ### 3) Add Auth Endpoints There are **four core API endpoints** your Next.js server should expose to facilitate authentication workflows in Wristband: - Login Endpoint - Callback Endpoint - Logout Endpoint - Session Endpoint You'll need to add these endpoints to your Next.js API routes. There's also one additional endpoint you can implement depending on your authentication needs: - Token Endpoint (optional) <br> #### Login Endpoint The goal of the Login Endpoint is to initiate an auth request by redirecting to the [Wristband Authorization Endpoint](https://docs.wristband.dev/reference/authorizev1). It will store any state tied to the auth request in a Login State Cookie, which will later be used by the Callback Endpoint. The frontend of your application should redirect to this endpoint when users need to log in to your application. ##### App Router ```typescript // src/app/api/auth/login/route.ts import type { NextRequest } from 'next/server'; import { wristbandAuth } from '../../../../wristband'; // Login Endpoint at "/api/auth/login" (route can be wherever you prefer) export async function GET(req: NextRequest) { return await wristbandAuth.appRouter.login(req); } ``` ##### Pages Router ```typescript // src/pages/api/auth/login.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { wristbandAuth } from '../../../wristband'; // Login Endpoint at "/api/auth/login" (route can be wherever you prefer) export default async function loginEndpoint(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { res.status(405).end(); return; } const authorizeUrl = await wristbandAuth.pagesRouter.login(req, res); res.redirect(authorizeUrl); } ``` <br> #### Callback Endpoint The goal of the Callback Endpoint is to receive incoming calls from Wristband after the user has authenticated and ensure that the Login State cookie contains all auth request state in order to complete the Login Workflow. From there, it will call the [Wristband Token Endpoint](https://docs.wristband.dev/reference/tokenv1) to fetch necessary JWTs, call the [Wristband Userinfo Endpoint](https://docs.wristband.dev/reference/userinfov1) to get the user's data, and create a session for the application containing the JWTs and user data. ##### App Router ```typescript // src/app/api/auth/callback/route.ts import { NextRequest } from 'next/server'; import { getRequestSession, wristbandAuth } from '../../../../wristband'; // Callback Endpoint at "/api/auth/callback" (route can be wherever you prefer) export async function GET(req: NextRequest) { const callbackResult = await wristbandAuth.appRouter.callback(req); const { callbackData, redirectUrl, type } = callbackResult; if (type === 'redirect_required') { return await wristbandAuth.appRouter.createCallbackResponse(req, redirectUrl); } // Set authentication data into the session const session = await getRequestSession(req); session.fromCallback(callbackData); // Create the response that will send the user back to your application. const appUrl = callbackData.returnUrl || `<your_app_home_url>`; const callbackResponse = await wristbandAuth.appRouter.createCallbackResponse(req, appUrl); // Save session headers to the response; then redirect to your app. return await session.saveToResponse(callbackResponse); } ``` ##### Pages Router ```typescript // src/pages/api/auth/callback.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { getSession, wristbandAuth } from '../../../wristband'; // Callback Endpoint at "/api/auth/callback" (route can be wherever you prefer) export default async function callbackEndpoint(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { res.status(405).end(); return; } const callbackResult = await wristbandAuth.pagesRouter.callback(req, res); const { callbackData, redirectUrl, type } = callbackResult; if (type === 'redirect_required') { res.redirect(redirectUrl); return; } // Save authentication data in the session const session = await getSession(req, res); session.fromCallback(callbackData); await session.save(); // Send the user back to the application. res.redirect(callbackData.returnUrl || `<your_app_home_url>`); } ``` <br> #### Logout Endpoint The goal of the Logout Endpoint is to destroy the application's session that was established during the Callback Endpoint execution. If refresh tokens were requested during the Login Workflow, then a call to the [Wristband Revoke Token Endpoint](https://docs.wristband.dev/reference/revokev1) will occur. It then will redirect to the [Wristband Logout Endpoint](https://docs.wristband.dev/reference/logoutv1) in order to destroy the user's authentication session within the Wristband platform. From there, Wristband will send the user to the Tenant-Level Login Page (unless configured otherwise). ##### App Router ```typescript // src/app/api/auth/logout/route.ts import type { NextRequest } from 'next/server'; import { getRequestSession, wristbandAuth } from '../../../../wristband'; // Logout Endpoint at "/api/auth/logout" (route can be wherever you prefer) export async function GET(req: NextRequest) { const session = await getRequestSession(req); // Create the logout redirect response const logoutResponse = await wristbandAuth.appRouter.logout(req, { refreshToken: session.refreshToken, tenantCustomDomain: session.tenantCustomDomain, tenantName: session.tenantName, }); // Always destroy session before redirecting. return await session.destroyToResponse(logoutResponse); }); ``` ##### Pages Router ```typescript // src/pages/api/auth/logout.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { getSession, wristbandAuth } from '../../../wristband'; // Logout Endpoint at "/api/auth/logout" (route can be wherever you prefer) export default async function logoutEndpoint(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { res.status(405).end(); return; } // Create the logout redirect URL const session = await getSession(req, res); const logoutUrl = await wristbandAuth.pagesRouter.logout(req, res, { refreshToken: session.refreshToken, tenantCustomDomain: session.tenantCustomDomain, tenantName: session.tenantName, }); // Always destroy session before redirecting. session.destroy(); res.redirect(logoutUrl); }); ``` <br> #### Session Endpoint > [!NOTE] > This endpoint is required for Wristband frontend SDKs to function. For more details, see the [Wristband Session Management documentation](https://docs.wristband.dev/docs/session-management-backend-server). Wristband frontend SDKs require a Session Endpoint in your backend to verify authentication status and retrieve session metadata. Create a protected session endpoint that uses `session.getSessionResponse()` to return the session response format expected by Wristband's frontend SDKs. The response type will always have a `userId` and a `tenantId` in it. You can include any additional data for your frontend by customizing the `metadata` parameter (optional), which requires JSON-serializable values. **The response must not be cached**. > **⚠️ Important:** > This endpoint must be protected with authentication middleware, which is shown in [Section 4](#4-protect-your-pages-actions-and-apis). ##### App Router ```typescript // src/app/api/auth/session/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getRequestSession } from '../../../../wristband'; // Session Endpoint at "/api/auth/session" (route can be wherever you prefer) export async function GET(req: NextRequest) { const session = await getRequestSession(req); const sessionResponse = session.getSessionResponse({ foo: 'bar' }); return NextResponse.json(sessionResponse, { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' }, }); }); ``` ##### Page Router ```typescript // src/pages/api/auth/session.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { getSession } from '../../../wristband'; // Session Endpoint at "/api/auth/session" (route can be wherever you prefer) export default async function sessionEndpoint(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { res.status(405).end(); return; } const session = await getSession(req, res); const sessionResponse = session.getSessionResponse({ foo: 'bar' }); res.setHeader('Cache-Control', 'no-store'); res.setHeader('Pragma', 'no-cache'); res.status(200).json(sessionResponse); } ``` ##### Response Type The Session Endpoint returns the `SessionResponse` type to your frontend: ```json { "tenantId": "tenant_abc123", "userId": "user_xyz789", "metadata": { "foo": "bar", // Any other optional data you provide... } } ``` <br> #### Token Endpoint (Optional) > [!NOTE] > This endpoint is required when your frontend needs to make authenticated API requests directly to Wristband or other protected services. For more details, see the [Wristband documentation on using access tokens from the frontend](https://docs.wristband.dev/docs/authenticating-api-requests-with-bearer-tokens#using-access-tokens-from-the-frontend). > > If your application doesn't need frontend access to tokens (e.g., all API calls go through your backend), you can skip this endpoint. Some applications require the frontend to make direct API calls to Wristband or other protected services using the user's access token. The Token Endpoint provides a secure way for your frontend to retrieve the current access token and its expiration time without exposing it in the session cookie or in browser storage. Create a protected token endpoint that uses `session.getTokenResponse()` to return the token data expected by Wristband's frontend SDKs. **The response must not be cached**. > **⚠️ Important:** > This endpoint must be protected with authentication middleware, which is shown in [Section 4](#4-protect-your-pages-actions-and-apis). ##### App Router ```typescript // src/app/api/auth/token/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getRequestSession } from '../../../../wristband'; // Token Endpoint at "/api/auth/token" (route can be wherever you prefer) export async function GET(req: NextRequest) { const session = await getRequestSession(req); const tokenResponse = session.getTokenResponse(); return NextResponse.json(tokenResponse, { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' }, }); }); ``` ##### Page Router ```typescript // src/pages/api/auth/token.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { getSession } from '../../../wristband'; // Token Endpoint at "/api/auth/token" (route can be wherever you prefer) export default async function tokenEndpoint(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { res.status(405).end(); return; } const session = await getSession(req, res); const tokenResponse = session.getTokenResponse(); res.setHeader('Cache-Control', 'no-store'); res.setHeader('Pragma', 'no-cache'); res.status(200).json(tokenResponse); } ``` ##### Response Type The Token Endpoint returns the `TokenResponse` type to your frontend: ```json { "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "expiresAt": 1735689600000 } ``` Your frontend can then use the `accessToken` in the Authorization header when making API requests: ```typescript const tokenResponse = await fetch('/api/auth/token'); const { accessToken } = await tokenResponse.json(); // Use token to call Wristband API const userResponse = await fetch('https://<your-wristband-app-vanity-domain>/api/v1/users/123', { headers: { 'Authorization': `Bearer ${accessToken}` } }); ``` <br> ### 4) Protect Your Pages, Actions, and APIs Once your auth endpoints are set up, protect your application routes with authentication middleware and add session helpers for accessing session data in different contexts. #### Set Up Authentication Middleware In Next.js, middleware (or proxy in Next.js 16+) is the ideal place to centralize authentication checks and token refresh for most scenarios. The authentication middleware works with both App Router and Pages Router, but has important differences in what it protects: **What middleware protects:** - βœ… **API Routes** (App and Pages Router) - All routes matching `protectedApis` patterns - βœ… **Session & Token Endpoints** - `/api/auth/session` and `/api/auth/token` are automatically protected when using `'SESSION'` strategy (customizable via `sessionConfig.sessionEndpoint` and `sessionConfig.tokenEndpoint` config) - βœ… **Pages** (Pages Router) - Server-rendered pages matching `protectedPages` patterns - βœ… **Server Components that are pages** (App Router) - Page components matching `protectedPages` patterns **What middleware does NOT protect:** - ❌ **Server Actions** (App Router) - Must use `createServerActionAuth()` for manual checks - ❌ **Server Components that are not pages** (App Router) - Server Components render during the RSC phase before middleware runs. Thus, only Server Components that are pages (route segments) are protected because they trigger a full page request that passes through middleware first. - ❌ **Client Components** (App Router) - Client Components render in the browser after the initial page load. Authentication is enforced on the initial page request via middleware, but subsequent client-side auth state management should use Wristband's [@wristband/react-client-auth](https://github.com/wristband-dev/react-client-auth) frontend SDK. Add middleware configuration to your `wristband.ts` file: ```typescript // src/wristband.ts (continued - add to existing file) // ... /** * Authentication middleware that protects routes in Next.js middleware. * * Automatically handles: * - Session validation for protected routes * - Token refresh when access tokens expire * - 401 responses for unauthenticated API requests * - Login redirects for unauthenticated page requests */ export const requireMiddlewareAuth = wristbandAuth.createMiddlewareAuth({ authStrategies: ['SESSION'], sessionConfig: { sessionOptions }, protectedApis: ['/api/v1(.*)'], // Regex patterns for protected API routes protectedPages: ['/', '/dashboard', '/settings(.*)'], // Regex patterns for protected pages }); ``` Now create the middleware or proxy file (depending on your version of Next.js) at the root of your `src` directory (or project root if not using `src`). **Next.js 16+**: ```typescript // src/proxy.ts import { NextRequest } from 'next/server'; import { requireMiddlewareAuth } from './wristband'; export async function proxy(req: NextRequest) { return await requireMiddlewareAuth(req); } export const config = { /* * Match all paths except for: * 1. /_next (Next.js internals) * 2. /fonts (inside /public) * 3. /examples (inside /public) * 4. all root files inside /public (e.g. /favicon.ico) */ matcher: ['/((?!_next|fonts|examples|[\\w-]+\\.\\w+).*)'], }; ``` **Next.js 15 and earlier**: ```typescript // src/middleware.ts import { NextRequest } from 'next/server'; import { requireMiddlewareAuth } from './wristband'; export async function middleware(req: NextRequest) { return await requireMiddlewareAuth(req); } export const config = { /* * Match all paths except for: * 1. /_next (Next.js internals) * 2. /fonts (inside /public) * 3. /examples (inside /public) * 4. all root files inside /public (e.g. /favicon.ico) */ matcher: ['/((?!_next|fonts|examples|[\\w-]+\\.\\w+).*)'], }; ``` The middleware automatically: - βœ… **Validates authentication** - Checks each auth strategy in order until one succeeds - βœ… **Refreshes expired tokens** - When using `'SESSION'` strategy AND when `refreshToken` and `expiresAt` are present in session - βœ… **Extends session expiration** - Rolling session window on each authenticated request (`'SESSION'` strategy only) - βœ… **Returns 401 for API Routes** - Unauthenticated requests to protected API routes - βœ… **Redirects pages to login** - Unauthenticated requests to protected pages (customizable via `onPageUnauthenticated`) - βœ… **Auto-protects auth endpoints** - Session and Token Endpoints protected by default (`'SESSION'` strategy only) - βœ… **Auto-bypasses Server Actions** - Server Action routes skip middleware protection (must manually check auth) <br> #### Protect Server Actions (App Router Only) Server Actions are **not protected by middleware** because they execute as POST requests to internal Next.js endpoints that bypass the middleware/proxy layer. Add the Server Action auth helper to your Wristband file: ```typescript // src/wristband.ts (continued - add to existing file) // ... /** * Authentication helper for Server Actions. * * Server Actions bypass Next.js middleware, so they must perform their own auth checks. * This helper validates the session and automatically refreshes expired tokens. */ export const requireServerActionAuth = wristbandAuth.appRouter.createServerActionAuth({ sessionOptions, }); ``` Here's an example of how to use it in your Server Actions: ```typescript // src/app/actions/my-action.ts 'use server'; import { cookies } from 'next/headers'; import { requireServerActionAuth } from './wristband'; export async function updateUserProfile(formData: FormData) { // The helper function will return you the current session if authentication succeeds. const cookieStore = await cookies(); const { authenticated, reason, session } = await requireServerActionAuth(cookieStore); // Check authentication result if (!authenticated) { return { error: 'Unauthorized', reason }; } // Access the authenticated session const { userId } = session; // ...your business logic here... return { success: true }; } ``` <br> #### Manual Session Access (Optional) In most cases, middleware and `createServerActionAuth()` handle all authentication needs. For advanced use cases where you need direct session access for custom logic, conditional rendering, or fine-grained session mutations, you can manually retrieve session data in the following contexts: - **API Route Handlers** (App Router & Pages Router) - **Server Components** (App Router) - Read-only access - **Server Actions** (App Router) - For advanced session mutations beyond `createServerActionAuth()` - **`getServerSideProps()`** (Pages Router) ##### App Router: Server Components (Read-Only) Be aware that Server Components cannot modify sessions (read-only) because they render during the RSC (React Server Components) phase where response headers and cookies cannot be set. If you need to read session data in a Server Component, add this helper: ```typescript // src/wristband.ts (continued - add to existing file) import { getReadOnlySessionFromCookies, NextJsCookieStore } from '@wristband/nextjs-auth'; // ... /** * Retrieves read-only session for Server Components. */ export function getServerComponentSession(cookieStore: NextJsCookieStore) { return getReadOnlySessionFromCookies(cookieStore, sessionOptions); } ``` Here's an example of how to use it in your Server Components: ```typescript // src/app/dashboard/page.tsx import { cookies } from 'next/headers'; import { getServerComponentSession } from '../../wristband'; export default async function DashboardPage() { const cookieStore = await cookies(); const session = await getServerComponentSession(cookieStore); const { isAuthenticated, userId } = session; if (!isAuthenticated) { return <div>Please log in.</div>; } return <div>Welcome, {userId}</div>; } ``` ##### App Router: Server Actions (Advanced) For advanced use cases where you need direct session manipulation without using `createServerActionAuth()`, add these helpers: ```typescript // src/wristband.ts (continued - add to existing file) import { getMutableSessionFromCookies, saveSessionWithCookies, destroySessionWithCookies, MutableSession, } from '@wristband/nextjs-auth'; // ... /** * Retrieves mutable session for Server Actions. * Call saveServerActionSession() after modifying to persist changes. */ export async function getServerActionSession(cookies: NextJsCookieStore) { return await getMutableSessionFromCookies(cookies, sessionOptions); } /** * Saves modified session data back to cookies (Server Actions only). */ export async function saveServerActionSession(cookies: NextJsCookieStore, session: MutableSession) { await saveSessionWithCookies(cookies, session); } /** * Destroys session and clears cookies (Server Actions only). */ export function destroyServerActionSession(cookies: NextJsCookieStore, session: MutableSession) { destroySessionWithCookies(cookies, session); } ``` Here's an example of how to use it in your Server Actions: ```typescript // src/app/actions/my-action.ts 'use server'; import { cookies } from 'next/headers'; import { destroyServerActionSession, getServerActionSession, saveServerActionSession } from '../../wristband'; export async function customAction() { // Get session (without performing auth check) const cookieStore = await cookies(); const session = await getServerActionSession(cookieStore); // Manually peform auth check if (!session.isAuthenticated) { // Destroy session destroyServerActionSession(cookieStore, session); return { error: 'Unauthorized' }; } // Modify session and save changes session.customField = 'value'; await saveServerActionSession(cookieStore, session); return { success: true }; } ``` ##### App Router: API Routes API routes for the App Router can use the `getRequestSession()` helper already defined in [Section 2](#2-set-up-session-management): ```typescript // src/app/api/orders/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getRequestSession } from '../../../../wristband'; export async function GET(req: NextRequest) { const session = await getRequestSession(req); if (!session.isAuthenticated) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } return NextResponse.json({ orders: [], userId: session.userId }); } ``` ##### Pages Router: API Routes and getServerSideProps() API routes for the Pages Router can use the `getSession()` helper already defined in [Section 2](#2-set-up-session-management): **API Route:** ```typescript // src/pages/api/profile.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { getSession } from '../../wristband'; export default async function apiRouteHandler(req: NextApiRequest, res: NextApiResponse) { const session = await getSession(req, res); if (!session.isAuthenticated) { return res.status(401).json({ error: 'Unauthorized' }); } return res.json({ userId: session.userId }); } ``` **SSR:** ```typescript // pages/dashboard.tsx import type { GetServerSideProps } from 'next'; import { getSession } from '../wristband'; export const getServerSideProps: GetServerSideProps = async (context) => { const session = await getSession(context.req, context.res); if (!session.isAuthenticated) { return { redirect: { destination: '/api/auth/login', permanent: false }, }; } return { props: { userId: session.userId }, }; }; export default function Dashboard({ userId }: { userId: string }) { return <div>Welcome, {userId}</div>; } ``` <br> ### 5) Pass Your Access Token to Downstream APIs > [!NOTE] > This is only applicable if you wish to call Wristband's APIs directly or protect your application's other downstream backend APIs. If you intend to utilize Wristband APIs within your application or secure any backend APIs or downstream services using the access token provided by Wristband, you must include this token in the `Authorization` HTTP request header. ```bash Authorization: Bearer <access_token_value> ``` For example, if you were using attempting to fetch user data from Wristband in an API route, you would pass the access token from your application session into the `Authorization` header as follows: ```typescript const session = await getRequestSession(req); const { accessToken, userId } = session; const userResponse = await fetch(`https://yourapp-yourcompany.us.wristband.dev/api/v1/users/${userId}`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` }, }); if (userResponse.status === 401) { redirect('/api/auth/login'); return null; } const user = await userResponse.json(); console.log(user); // Output -> { id: 123, ... } ``` #### Using Access Tokens from the Frontend For scenarios where your frontend needs to make direct API calls with the user's access token, use the [Token Endpoint](#token-endpoint-optional) to securely retrieve the current access token. <br> ## Wristband Auth Configuration Options The `createWristbandAuth()` function is used to instatiate the Wristband SDK. It takes an `AuthConfig` type as an argument. ### Auth Config Options `AuthConfig` contains the full set of options for integrating Wristband auth, including required, optional, and auto-configured values. | AuthConfig Field | Type | Required | Auto-Configurable | Description | | ---------------- | ---- | -------- | ----------------- | ----------- | | autoConfigureEnabled | boolean | No | _N/A_ | Flag that tells the SDK to automatically set some of the SDK configuration values by calling to Wristband's SDK Auto-Configuration Endpoint. Any manually provided configurations will take precedence over the configs returned from the endpoint. Auto-configure is enabled by default. When disabled, if manual configurations are not provided, then an error will be thrown. | | clientId | string | Yes | No | The ID of the Wristband client. | | clientSecret | string | Yes | No | The client's secret. | | customApplicationLoginPageUrl | string | No | Yes | Custom Application-Level Login Page URL (i.e. Tenant Discovery Page URL). This value only needs to be provided if you are self-hosting the application login page. By default, the SDK will use your Wristband-hosted Application-Level Login page URL. If this value is provided, the SDK will redirect to this URL in certain cases where it cannot resolve a proper Tenant-Level Login URL. | | dangerouslyDisableSecureCookies | boolean | No | No | USE WITH CAUTION: If set to `true`, the "Secure" attribute will not be included in any cookie settings. This should only be done when testing in local development environments that don't have HTTPS enabed. If not provided, this value defaults to `false`. | | isApplicationCustomDomainActive | boolean | No | Yes | Indicates whether your Wristband application is configured with an application-level custom domain that is active. This tells the SDK which URL format to use when constructing the Wristband Authorize Endpoint URL. This has no effect on any tenant custom domains passed to your Login Endpoint either via the `tenant_custom_domain` query parameter or via the `defaultTenantCustomDomain` config. Defaults to `false`. | | loginStateSecret | string | No | No | A 32 character (or longer) secret used for encryption and decryption of login state cookies. If not provided, it will default to using the client secret. For enhanced security, it is recommended to provide a value that is unique from the client secret. You can run `openssl rand -base64 32` to create a secret from your CLI. | | loginUrl | string | Yes | Yes | The URL of your application's login endpoint. This is the endpoint within your application that redirects to Wristband to initialize the login flow. If you intend to use tenant subdomains in your Login Endpoint URL, then this value must contain the `{tenant_domain}` placeholder. For example: `https://{tenant_domain}.yourapp.com/auth/login`. | | parseTenantFromRootDomain | string | Only if using tenant subdomains in your application | Yes | The root domain for your application. This value only needs to be specified if you intend to use tenant subdomains in your Login and Callback Endpoint URLs. The root domain should be set to the portion of the domain that comes after the tenant subdomain. For example, if your application uses tenant subdomains such as `tenantA.yourapp.com` and `tenantB.yourapp.com`, then the root domain should be set to `yourapp.com`. This has no effect on any tenant custom domains passed to your Login Endpoint either via the `tenant_custom_domain` query parameter or via the `defaultTenantCustomDomain` config. When this configuration is enabled, the SDK extracts the tenant subdomain from the host and uses it to construct the Wristband Authorize URL. | | redirectUri | string | Yes | Yes | The URI that Wristband will redirect to after authenticating a user. This should point to your application's callback endpoint. If you intend to use tenant subdomains in your Callback Endpoint URL, then this value must contain the `{tenant_domain}` placeholder. For example: `https://{tenant_domain}.yourapp.com/auth/callback`. | | scopes | string[] | No | No | The scopes required for authentication. Refer to the docs for [currently supported scopes](https://docs.wristband.dev/docs/oauth2-and-openid-connect-oidc#supported-openid-scopes). The default value is `[openid, offline_access, email]`. | | tokenExpirationBuffer | number | No | No | Buffer time (in seconds) to subtract from the access token’s expiration time. This causes the token to be treated as expired before its actual expiration, helping to avoid token expiration during API calls. Defaults to 60 seconds. | | wristbandApplicationVanityDomain | string | Yes | No | The vanity domain of the Wristband application. | <br> ### `createWristbandAuth()` ```ts function createWristbandAuth(authConfig: AuthConfig): WristbandAuth {} ``` This function creates an instance of `WristbandAuth` using lazy auto-configuration. Auto-configuration is enabled by default and will fetch any missing configuration values from the Wristband SDK Configuration Endpoint when any auth function is first called (i.e. `login`, `callback`, etc.). Set `autoConfigureEnabled` to `false` disable to prevent the SDK from making an API request to the Wristband SDK Configuration Endpoint. In the event auto-configuration is disabled, you must manually configure all required values. Manual configuration values take precedence over auto-configured values. > **⚠️ Auto-Configuration in Edge Runtimes** > > While auto-configuration works well in Node.js runtime environments, **manual configuration is strongly recommended when using Next.js Edge Runtime** (Edge API Routes, Middleware, and Edge-rendered pages) due to the following limitations: > > - **Cold start latency**: Auto-configuration requires an API call to the Wristband SDK Configuration Endpoint on every cold start, which can impact response times for authentication flows in Edge Runtime. > - **No persistent memory**: Edge Runtime instances don't maintain in-memory caches between requests, causing the SDK to refetch configuration data on every invocation > > For production Next.js applications using Edge Runtime, you can set `autoConfigureEnabled: false` and provide all required configuration values manually. This is especially critical for authentication middleware that runs on every protected route. **Minimal config with auto-configure (default behavior)** ```ts const auth = createWristbandAuth({ clientId: "your-client-id", clientSecret: "your-secret", wristbandApplicationVanityDomain: "auth.yourapp.io" }); ``` **Manual override with partial auto-configure for some fields** ```ts const auth = createWristbandAuth({ clientId: "your-client-id", clientSecret: "your-secret", wristbandApplicationVanityDomain: "auth.yourapp.io", loginUrl: "https://yourapp.io/auth/login", // Manually override "loginUrl" // "redirectUri" will be auto-configured }); ``` **Auto-configure disabled** ```ts const auth = createWristbandAuth({ autoConfigureEnabled: false, clientId: "your-client-id", clientSecret: "your-secret", wristbandApplicationVanityDomain: "auth.custom.com", // Must manually configure non-auto-configurable fields isApplicationCustomDomainActive: true, loginUrl: "https://{tenant_domain}.custom.com/auth/login", redirectUri: "https://{tenant_domain}.custom.com/auth/callback", parseTenantFromRootDomain: "custom.com", }); ``` <br> ## Auth API ### login() ```ts /* *** App Router *** */ // Definition login: (req: NextRequest, loginConfig?: LoginConfig) => Promise<NextResponse>; // Usage return await wristbandAuth.appRouter.login(req); /* *** Pages Router *** */ // Definition login: (req: NextApiRequest, res: NextApiResponse, loginConfig?: LoginConfig) => Promise<string>; // Usage const authorizeUrl = await wristbandAuth.pagesRouter.login(req, res); res.redirect(authorizeUrl); ``` Wristband requires that your application specify a Tenant-Level domain when redirecting to the Wristband Authorize Endpoint when initiating an auth request. When the frontend of your application redirects the user to your Next.js Login Endpoint, there are two ways to accomplish getting the `tenantName` information: passing a query parameter or using tenant subdomains. The `login()` function can also take optional configuration if your application needs custom behavior: | LoginConfig Field | Type | Required | Description | | ----------------- | ---- | -------- | ----------- | | customState | JSON | No | Additional state to be saved in the Login State Cookie. Upon successful completion of an auth request/login attempt, your Callback Endpoint will return this custom state (unmodified) as part of the return type. | | defaultTenantName | string | No | An optional default tenant name to use for the login request in the event the tenant name cannot be found in either the subdomain or query parameters (depending on your subdomain configuration). | | defaultTenantCustomDomain | string | No | An optional default tenant custom domain to use for the login request in the event the tenant custom domain cannot be found in the query parameters. | | returnUrl | string | No | The URL to return to after authentication is completed. If a value is provided, then it takes precedence over the `return_url` request query parameter. | #### Which Domains Are Used in the Authorize URL? Wristband supports various tenant domain configurations, including subdomains and custom domains. The SDK automatically determines the appropriate domain configuration when constructing the Wristband Authorize URL, which your login endpoint will redirect users to during the login flow. The selection follows this precedence order: 1. `tenant_custom_domain` query parameter: If provided, this takes top priority. 2. Tenant subdomain in the URL: Used if subdomains are enabled and the subdomain is present. 3. `tenant_name` query parameter: Evaluated if no tenant subdomain is detected. 4. `defaultTenantCustomDomain` in LoginConfig: Used if none of the above are present. 5. `defaultTenantDomain` in LoginConfig: Used as the final fallback. If none of these are specified, the SDK redirects users to the Application-Level Login (Tenant Discovery) Page. #### Tenant Name Query Param If your application does not wish to utilize subdomains for each tenant, you can pass the `tenant_name` query parameter to your Login Endpoint, and the SDK will be able to make the appropriate redirection to the Wristband Authorize Endpoint. ```sh GET https://yourapp.io/api/auth/login?tenant_name=customer01 ``` Your AuthConfig would look like the following when creating an SDK instance without any subdomains: ```ts const wristbandAuth = createWristbandAuth({ clientId: "ic6saso5hzdvbnof3bwgccejxy", clientSecret: "30e9977124b13037d035be10d727806f", loginStateSecret: '7ffdbecc-ab7d-4134-9307-2dfcc52f7475', loginUrl: "https://yourapp.io/auth/login", redirectUri: "https://yourapp.io/auth/callback", wristbandApplicationVanityDomain: "yourapp-yourcompany.us.wristband.dev", }); ``` #### Tenant Subdomains If your application wishes to utilize tenant subdomains, then you do not need to pass a query param when redirecting to your Next.js Login Endpoint. The SDK will parse the tenant subdomain from the URL in order to make the redirection to the Wristband Authorize Endpoint. You will also need to tell the SDK what your application's root domain is in order for it to correctly parse the subdomain. ```sh GET https://customer01.yourapp.io/api/auth/login ``` Your AuthConfig would look like the following when creating an SDK instance when using subdomains: ```ts const wristbandAuth = createWristbandAuth({ clientId: "ic6saso5hzdvbnof3bwgccejxy", clientSecret: "30e9977124b13037d035be10d727806f", loginStateSecret: '7ffdbecc-ab7d-4134-9307-2dfcc52f7475', loginUrl: "https://{tenant_domain}.yourapp.io/auth/login", redirectUri: "https://{tenant_domain}.yourapp.io/auth/callback", parseTenantFromRootDomain: "yourapp.io", wristbandApplicationVanityDomain: "yourapp-yourcompany.us.wristband.dev", }); ``` #### Default Tenant Name For certain use cases, it may be useful to specify a default tenant name in the event that the `login()` function cannot find a tenant name in either the query parameters or in the URL subdomain. You can specify a fallback default tenant name via a `LoginConfig` object. For example: ```ts await wristbandAuth.pagesRouter.login(req, res, { defaultTenantName: 'default' }); ``` #### Tenant Custom Domain Query Param If your application wishes to utilize tenant custom domains, you can pass the `tenant_custom_domain` query parameter to your Login Endpoint, and the SDK will be able to make the appropriate redirection to the Wristband Authorize Endpoint. ```sh GET https://yourapp.io/auth/login?tenant_custom_domain=mytenant.com ``` The tenant custom domain takes precedence over all other possible domains else when present. #### Default Tenant Custom Domain For certain use cases, it may be useful to specify a default tenant custom domain in the event that the `login()` function cannot find a tenant custom domain in the query parameters. You can specify a fallback default tenant custom domain via a `LoginConfig` object: ```ts await wristbandAuth.appRouter.login(req, { defaultTenantCustomDomain: 'mytenant.com' }); ``` The default tenant custom domain takes precedence over all other possible domains else when present except when the `tenant_custom_domain` query parameter exists in the request. #### Custom State Before your Login Endpoint redirects to Wristband, it will create a Login State Cookie to cache all necessary data required in the Callback Endpoint to complete any auth requests. You can inject additional state into that cookie via a `LoginConfig` object. For example: ```ts await wristbandAuth.appRouter.login(req, { customState: { test: 'abc' } }); ``` > [!WARNING] > Injecting custom state is an advanced feature, and it is recommended to use `customState` sparingly. Most applications may not need it at all. The max cookie size is 4kB. From our own tests, passing a `customState` JSON of at most 1kB should be a safe ceiling. #### Login Hints Wristband will redirect to your Next.js Login Endpoint for workflows like Application-Level Login (Tenant Discovery) and can pass the `login_hint` query parameter as part of the redirect request: ```sh GET https://customer01.yourapp.io/api/auth/login?login_hint=user@wristband.dev ``` If Wristband passes this parameter, it will be appended as part of the redirect request to the Wristband Authorize Endpoint. Typically, the email form field on the Tenant-Level Login page is pre-filled when a user has previously entered their email on the Application-Level Login Page. #### Return URLs It is possible that users will try to access a location within your application that is not some default landing page. In those cases, t