UNPKG

@wristband/express-auth

Version:

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

950 lines (699 loc) β€’ 89.9 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 Express [![npm package](https://img.shields.io/badge/npm%20i-express--auth-brightgreen)](https://www.npmjs.com/package/@wristband/express-auth) [![version number](https://img.shields.io/github/v/release/wristband-dev/express-auth?color=green&label=version)](https://github.com/wristband-dev/express-auth/releases) [![Actions Status](https://github.com/wristband-dev/express-auth/workflows/Test/badge.svg)](https://github.com/wristband-dev/express-auth/actions) [![License](https://img.shields.io/github/license/wristband-dev/express-auth)](https://github.com/wristband-dev/express-auth/blob/main/LICENSE.md) Enterprise-ready authentication for multi-tenant [Express applications](https://expressjs.com) using OAuth 2.1 and OpenID Connect standards. It supports both CommonJS and ES Modules and includes TypeScript declaration files. <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 [Express demo applications](#wristband-multi-tenant-express-demo-app). The demo showcases 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 API Routes](#4-protect-your-api-routes) - [5) Use Your Access Token with APIs](#5-use-your-access-token-with-apis) - [Auth Configuration Options](#auth-configuration-options) - [createWristbandAuth()](#createwristbandauth) - [discoverWristbandAuth()](#discoverwristbandauth) - [Auth API](#auth-api) - [login()](#login) - [callback()](#callback) - [logout()](#logout) - [refreshTokenIfExpired()](#refreshtokenifexpired) - [Session Management](#session-management) - [Session Configuration](#session-configuration) - [The Session Object](#the-session-object) - [Session Access Patterns](#session-access-patterns) - [Session API](#session-api) - [session.fromCallback()](#sessionfromcallbackcallbackdata-customfields) - [session.save()](#sessionsave) - [session.destroy()](#sessiondestroy) - [session.getSessionResponse()](#sessiongetsessionresponsemetadata) - [session.getTokenResponse()](#sessiongettokenresponse) - [CSRF Protection](#csrf-protection) - [Authentication Middleware](#authentication-middleware) - [createAuthMiddleware()](#createauthmiddleware) - [SESSION Strategy](#session-strategy) - [JWT Strategy](#jwt-strategy) - [Strategy Order](#strategy-order) - [Using the Auth Middleware](#using-the-auth-middleware) - [Middleware Error Handling](#middleware-error-handling) - [Related Wristband SDKs](#related-wristband-sdks) - [Wristband Multi-Tenant Express Demo App](#wristband-multi-tenant-express-demo-app) - [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 6.x (latest)](migration/v6/README.md) - [Instructions for migrating to Version 5.x](migration/v5/README.md) - [Instructions for migrating to Version 4.x](migration/v4/README.md) - [Instructions for migrating to Version 3.x](migration/v3/README.md) <br> ## Prerequisites > **⚑ Try Our Express Quickstart!** > > For the fastest way to get started with Express authentication, follow our [Quick Start Guide](https://docs.wristband.dev/docs/auth-quick-start). It walks you through setting up a working Express 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 - [Express](https://expressjs.com/) >= 4.0.0 - Your preferred package manager (npm >= 9.6.0, yarn, pnpm, etc.) <br> ## Installation ```bash # With npm npm install @wristband/express-auth # Or with yarn yarn add @wristband/express-auth # Or with pnpm pnpm add @wristband/express-auth ``` <br> ## Usage ### 1) Initialize the Auth SDK First, create an instance of `WristbandAuth` in your Express 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. ```typescript // src/wristband.ts import { createWristbandAuth } from '@wristband/express-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: "auth.yourapp.io", }); ``` <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 the session middleware and authentication middleware to your Express app: ```typescript // src/wristband.ts (continued) import { createWristbandAuth } from '@wristband/express-auth'; import { createWristbandSession } from '@wristband/express-auth/session'; export const wristbandAuth = createWristbandAuth({ clientId: "replace-me-with-your-client-id", clientSecret: "replace-me-with-your-client-secret", wristbandApplicationVanityDomain: "auth.yourapp.io", }); // Session configuration used across all middlewares. const sessionOptions = { secrets: 'dummyval-b5c1-463a-812c-0d8db87c0ec5', // 32+ character secret maxAge: 3600, // 1 hour in seconds secure: process.env.NODE_ENV === 'production', }; // Session middleware for encrypted cookie-based session management. // Wrapped in a factory function to ensure a fresh middleware instance per app.use() call. export function wristbandSession() { return createWristbandSession(sessionOptions); }; ``` Then apply the session middleware to your Express app: ```typescript // src/app.ts import express from 'express'; import { wristbandSession } from './wristband'; const app = express(); // Add Wristband session middleware. app.use(wristbandSession()); ... ``` This approach requires no additional dependencies and works seamlessly with Wristband auth callbacks. > [!NOTE] > If you prefer server-side sessions (Redis, databases, etc.) or want to use a different session library like [express-session](https://github.com/expressjs/session), you can skip importing `/session` and manage sessions however you'd like. Just make sure to store the Wristband tokens in your session after authentication. <br> ### 3) Add Auth Endpoints There are <ins>four core API endpoints</ins> your Express server should expose to facilitate both the Login and Logout workflows in Wristband. You'll need to add them to wherever your Express routes/controllers are. <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. ```typescript // src/routes/auth-routes.ts import { wristbandAuth } from '../wristband'; // Login Endpoint - Route path can be whatever you prefer app.get('/auth/login', async (req, res) => { const loginUrl = await wristbandAuth.login(req, res); res.redirect(loginUrl); }); ``` #### 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. ```typescript // src/routes/auth-routes.ts (continued) ... // Callback Endpoint - Route path can be whatever you prefer app.get('/auth/callback', async (req, res) => { const callbackResult = await wristbandAuth.callback(req, res); const { callbackData, reason, redirectUrl, type } = callbackResult; // For certain edge cases, the SDK will require you to redirect back to login. if (type === 'redirect_required') { console.debug(reason); // <- Optional debugging info res.redirect(redirectUrl); return; } // Save necessary fields in the user's session. req.session.fromCallback(callbackData); await req.session.save(); // Send the user back to your application. res.redirect(callbackData.returnUrl || `<your_app_home_url>`); }); ``` #### 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). ```typescript // src/routes/auth-routes.ts (continued) ... // Logout Endpoint - Route path can be whatever you prefer app.get('/auth/logout', async (req, res) => { const { refreshToken, tenantName } = req.session; // Always destroy your application's session. req.session.destroy(); const logoutUrl = await wristbandAuth.logout(req, res, { tenantName, refreshToken }); 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 [4) Protect Your API Routes](#4-protect-your-api-routes). ```typescript // src/routes/auth-routes.ts (continued) ... // Session Endpoint - Route path can be whatever you prefer app.get('/auth/session', (req, res) => { const sessionResponse = req.session.getSessionResponse({ foo: 'bar' }); res.header('Cache-Control', 'no-store'); res.header('Pragma', 'no-cache'); return res.status(200).json(sessionResponse); }); ``` 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 [4) Protect Your API Routes](#4-protect-your-api-routes). ```typescript // src/routes/auth-routes.ts (continued) ... // Token Endpoint - Route path can be whatever you prefer app.get('/auth/token', (req, res) => { const tokenResponse = req.session.getTokenResponse(); res.header('Cache-Control', 'no-store'); res.header('Pragma', 'no-cache'); return res.status(200).json(tokenResponse); }); ``` 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('/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 API Routes Once your auth endpoints are set up, you can use the authentication middleware to protect routes that require authentication. The SDK provides a factory function `createAuthMiddleware()` that supports multiple authentication strategies including session-based and JWT bearer token authentication. #### Set Up Authentication Middleware First, configure the authentication middleware in your Wristband configuration file. At minimum, you'll need an auth middleware for protecting your Session Endpoint (and optionally your Token Endpoint if you created it). ```typescript // src/wristband.ts (continued - add to existing file) ... const sessionOptions = { /* your session options */ }; /** * Authentication middleware that protects routes. */ export const requireWristbandAuth = wristbandAuth.createAuthMiddleware({ authStrategies: ['SESSION'], sessionConfig: { sessionOptions } }); ``` Then apply the middleware to all of your protected routes: **Auth Routes:** ```typescript // src/routes/auth-routes.ts (continued) import { wristbandAuth, requireWristbandAuth } from '../wristband'; ... // Session Endpoint, protected by auth middleware app.get('/auth/session', requireWristbandAuth, (req, res) => { /* endpoint logic */ }); // Optional: Token Endpoint, protected by auth middleware app.get('/auth/token', requireWristbandAuth, (req, res) => { /* endpoint logic */ }); ``` **Protected Resource Routes:** ```typescript // src/routes/protected-routes.ts import express from 'express'; import { requireWristbandAuth } from '../wristband'; const router = express.Router(); // Protect any routes that require an authenticated session. router.get('api/hello-world', requireWristbandAuth, (req, res) => { res.status(200).json({ message: 'Hello World!' }) }); export default router; ``` 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 (with up to 3 retry attempts) - βœ… Extends session expiration - Rolling session window on each authenticated request (`SESSION` strategy only) - βœ… Validates CSRF tokens - Checks CSRF token in request header, if enabled in your session options (`SESSION` strategy only) - βœ… Returns 401 for unauthenticated requests - Automatically rejects requests that fail all auth strategies <br> ### 5) Use Your Access Token with APIs > [!NOTE] > This section is only applicable if you need to call Wristband APIs or protect your own backend services with Wristband tokens. 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 your access token in the `Authorization` HTTP request header. ```bash Authorization: Bearer <your_access_token> ``` The access token is available in different ways depending on your authentication strategy. #### Session-Based Authentication When using the Session Middleware, the access token is stored in `req.session.accessToken`: ```typescript app.post('/api/orders', requireWristbandAuth, async (req, res) => { try { const newOrder = { ...req.body }; db.save(newOrder) await axios.post('https://api.example.com/email-receipt', newOrder, { // Pass your access token to downstream API headers: { Authorization: `Bearer ${req.session.accessToken}` } }); res.status(201).send(); } catch (error) { res.status(500).send('Internal Server Error'); } }); ``` #### JWT Bearer Token Authentication When using the `JWT` Auth Middleware strategy, the decoded JWT payload is available in `req.auth`, and the raw JWT string is available in `req.auth.jwt`: ```typescript app.post('/api/orders', requireWristbandAuth, async (req, res) => { try { const newOrder = { ...req.body }; db.save(newOrder) await axios.post('https://api.example.com/email-receipt', newOrder, { // Pass your access token to downstream API headers: { Authorization: `Bearer ${req.auth.jwt}` } }); res.status(201).send(); } catch (error) { res.status(500).send('Internal Server Error'); } }); ``` #### 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> ## Auth Configuration Options There are two functions you can use for initializing the SDK in your application: `createWristbandAuth()` and `discoverWristbandAuth()`. | Method | When Config is Fetched | Use When | | ------ | ---------------------- | -------- | | `createWristbandAuth()` (default) | Lazily, on first auth method call (login, callback, etc.) | Standard usage - allows your app to start without waiting for config | | `discoverWristbandAuth()` | Eagerly, immediately when called | You want to fail fast at startup if auto-config is unavailable | Both functions accept an `AuthConfig` object containing the settings required to integrate Wristband authentication. | 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 [UserInfo API docs](https://docs.wristband.dev/reference/userinfov1) for currently supported 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` 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. **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> ### discoverWristbandAuth() This function creates an instance of `WristbandAuth` with eager auto-configuration. Unlike `createWristbandAuth()`, this function immediately fetches and resolves all auto-configuration values from the Wristband SDK Configuration Endpoint during initialization. This is useful when you want to fail fast when auto-configuration is unavailable, or when you need configuration values resolved before making any auth function calls. Manual configuration values take precedence over auto-configured values. > [!WARNING] > NOTE: For CommonJS environments, createWristbandAuth() provides simpler integration since it doesn't require async module initialization patterns. **Eager auto-configure with error handling** ```ts try { const wristbandAuth = await discoverWristbandAuth({ clientId: "your-client-id", clientSecret: "your-secret", wristbandApplicationVanityDomain: "auth.yourapp.io" }); // // ...Configuration is now resolved and validated... // } catch (error) { console.error('Auto-configuration failed:', error.message); } ``` <br> ## Auth API ### login() ```typescript // Definition login(req: Request, res: Response, config?: LoginConfig): Promise<string>; // Usage const loginUrl = await login(req, res); ``` 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 Express 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 `parseTenantFromRootDomain` is specified and there is a subdomain present in the host. 3. `tenant_name` query parameter: Evaluated if no tenant subdomain is found in the host. 4. `defaultTenantCustomDomain` in LoginConfig: Used if none of the above are present. 5. `defaultTenantName` 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/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 Express Login Endpoint. The SDK will parse the tenant subdomain from the host 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/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: ```ts const loginUrl = await wristbandAuth.login(req, res, { defaultTenantName: 'default' }); res.redirect(loginUrl); ``` #### 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 const loginUrl = await wristbandAuth.login(req, res, { defaultTenantCustomDomain: 'mytenant.com' }); res.redirect(loginUrl); ``` The default tenant custom domain takes precedence over all other possible domain configurations when present except for the case where 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: ```ts const loginUrl = await wristbandAuth.login(req, res, { customState: { test: 'abc' } }); res.redirect(loginUrl); ``` > [!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 Express 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/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. #### IDP Hints If you want to bypass the Wristband-hosted Tenant Login Page entirely and send users directly to a specific identity provider's login page, you can pass the `idp_hint` query parameter to your Login Endpoint: ```sh GET https://customer01.yourapp.io/auth/login?idp_hint=google ``` The value should be the `name` field of an identity provider that is currently enabled for the tenant. When Wristband receives this hint, it checks if the provided IdP name matches an enabled identity provider for that tenant: - **Matching external IdP (e.g. `google`)** β€” Wristband skips the Tenant Login Page entirely and redirects the user straight to that external IdP's login page (e.g. Google's login page). - **Wristband IdP name** β€” Wristband displays the Tenant Login Page but shows only the Wristband email-based login form, hiding any external IdP buttons. - **Unrecognized or invalid value** β€” Wristband ignores the hint and displays the Tenant Login Page as normal. This is useful when your application already knows which identity provider a user should authenticate with, allowing you to skip the IdP selection step entirely and provide a more seamless login experience. #### 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, they would expect to immediately land back at that desired location after logging in. This is a better experience for the user, especially in cases where they have application URLs bookmarked for convenience. Given that your frontend will redirect users to your Express Login Endpoint, you can either include it in your Login Config: ```ts const loginUrl = await wristbandAuth.login(req, res, { returnUrl: 'https://customer01.yourapp.io/settings/profile', }); res.redirect(loginUrl); ``` ...or you can pass a `return_url` query parameter when redirecting to your Login Endpoint: ```sh GET https://customer01.yourapp.io/auth/login?return_url=https://customer01.yourapp.io/settings/profile ``` The return URL is stored in the Login State Cookie, and it is available to you in your Callback Endpoint after the SDK's `callback()` method is done executing. You can choose to send users to that return URL (if necessary). The Login Config takes precedence over the query parameter in the event a value is provided for both. ##### Return URL Preservation During Tenant Discovery When the `login()` method cannot resolve a tenant domain from the request (subdomain, query parameters, or defaults), the SDK redirects users to the Application-Level Login (Tenant Discovery) Page. To ensure a seamless user experience, any provided return URL values are automatically preserved by appending them to the `state` query parameter. This allows the return URL to be propagated back to the Login Endpoint once tenant discovery is complete, ensuring users land at their originally intended destination after authentication. <br> ### callback() ```typescript // Definition callback(req: Request, res: Response): Promise<CallbackResult>; // Usage const callbackResult = await callback(req, res); ``` After a user authenticates on the Tenant-Level Login Page, Wristband will redirect to your Express Callback Endpoint with an authorization code which can be used to exchange for an access token. It will also pass the state parameter that was generated during the Login Endpoint. ```sh GET https://customer01.yourapp.io/auth/callback?state=f983yr893hf89ewn0idjw8e9f&code=shcsh90jf9wc09j9w0jewc ``` The SDK will validate that the incoming state matches the Login State Cookie, and then it will call the Wristband Token Endpoint to exchange the authorizaiton code for JWTs. Lastly, it will call the Wristband Userinfo Endpoint to get any user data as specified by the `scopes` in your SDK configuration. The return type of the callback function is a `CallbackResult` object containing the result of what happened during callback execution as well as any accompanying data: | CallbackResult Field | Type | Description | | -------------------- | ---- | ----------- | | callbackData | CallbackData or `undefined` | The callback data received after authentication (`'completed'` result only). | | reason | CallbackFailureReason or `undefined` | The reason why the callback did not complete successfully (`'redirect_required'` only). | | redirectUrl | string or `undefined` | The URL that the user should redirected to (`'redirect_required'` only). For some edge cases, the SDK will require a redirect to restart the login flow. | | type | CallbackResultType | String literal representing the end result of callback execution.<br><br> Possible values: `'completed'` or `'redirect_required'`. | <br> The `CallbackResultType` can be one of the following string literal values: | CallbackResultType | Description | | ------------------ | ----------- | | `'completed'` | Indicates that the callback is successfully completed and data is available for creating a session. | | `'redirect_required'` | Indicates that a redirect to the login endpoint is required. | <br> When the callback returns a `'redirect_required'` result, the `reason` field indicates why the callback failed: | CallbackFailureReason | Description | | --------------------- | ----------- | | `'missing_login_state'` | Login state cookie was not found (cookie expired or bookmarked callback URL). | | `'invalid_login_state'` | Login state validation failed (possible CSRF attack or cookie tampering) | | `'login_required'` | Wristband returned a login_required error (session expired or max_age elapsed). | | `'invalid_grant'` | Authorization code was invalid, expired, or already used. | <br> When the callback returns a `'completed'` result, all of the token and userinfo data also gets returned. This enables your application to create an application session for the user and then redirect them back into your application. The `CallbackData` is defined as follows: | CallbackData Field | Type | Description | | ------------------ | ---- | ----------- | | accessToken | string | The access token that can be used for accessing Wristband APIs as well as protecting your application's backend APIs. | | customState | JSON or `undefined` | If you injected custom state into the Login State Cookie during the Login Endpoint for the current auth request, then that same custom state will be returned in this field. | | expiresAt | number | The absolute expiration time of the access token in milliseconds since the Unix epoch. The `tokenExpirationBuffer` SDK configuration is accounted for in this value. | | expiresIn | number | The duration from the current time until the access token is expired (in seconds). The `tokenExpirationBuffer` SDK configuration is accounted for in this value. | | idToken | string | The ID token uniquely identifies the user that is authenticating and contains claim data about the user. | | refreshToken | string or `undefined` | The refresh token that renews expired access tokens with Wristband, maintaining continuous access to services. | | returnUrl | string or `undefined` | The URL to return to after authentication is completed. | | tenantCustomDomain | string | The tenant custom domain for the tenant that the user belongs to (if applicable). | | tenantName | string | The name of the tenant the user belongs to. | | userinfo | `UserInfo` | Data for the current user retrieved from the Wristband Userinfo Endpoint. The data returned in this object follows the format laid out in the [Wristband Userinfo Endpoint documentation](https://docs.wristband.dev/reference/userinfov1). The exact fields that get returned are based on the scopes you configured in the SDK. | The `UserInfo` type is defined as follows: | UserInfo Field | Type | Always Returned | Description | | -------------- | ---- | --------------- | ----------- | | userId | string | Yes | ID of the user (mapped from "sub" claim). | | tenantId | string | Yes | ID of the tenant that the user belongs to (mapped from "tnt_id" claim). | | applicationId | string | Yes | ID of the application that the user belongs to (mapped from "app_id" claim). | | identityProviderName | string | Yes | Name of the identity provider (mapped from "idp_name" claim). | | fullName | string or `undefined` | No | End-User's full name in displayable form (mapped from "name" claim; requires `profile` scope). | | givenName | string or `undefined` | No | Given name(s) or first name(s) of the End-User (requires `profile` scope). | | familyName | string or `undefined` | No | Surname(s) or last name(s) of the End-User (requires `profile` scope). | | middleName | string or `undefined` | No | Middle name(s) of the End-User (requires `profile` scope). | | nickname | string or `undefined` | No | Casual name of the End-User (requires `profile` scope). | | displayName | string or `undefined` | No | Shorthand name by which the End-User wishes to be referred (requires `profile` scope). | | pictureUrl | string or `undefined` | No | URL of the End-User's profile picture (requires `profile` scope). | | email | string or `undefined` | No | End-User's preferred email address (requires `email` scope). | | emailVerified | boolean or `undefined` | No | True if the End-User's email address has been verified (requires `email` scope). | | gender | string or `undefined` | No | End-User's gender (requires `profile` scope). | | birthdate | string or `undefined` | No | End-User's birthday in YYYY-MM-DD format (requires `profile` scope). | | timeZone | string or `undefined` | No | End-User's time zone (requires `profile` scope). | | locale | string or `undefined` | No | End-User's locale as BCP47 language tag, e.g., "en-US" (requires `profile` scope). | | phoneNumber | string or `undefined` | No | End-User's telephone number in E.164 format (requires `phone` scope). | | phoneNumberVerified | boolean or `undefined` | No | True if the End-User's phone number has been verified (requires `phone` scope). | | updatedAt | number or `undefined` | No | Time the End-User's information was last updated as Unix timestamp (requires `profile` scope). | | roles | `UserInfoRole[]` or `undefined` | No | The roles assigned to the user (requires `roles` scope). | | customClaims | `Record<string, any>` or `undefined` | No | Object containing any configured custom claims. | The `UserInfoRole` type is defined as follows: | UserInfoRole Field | Type | Description | | ------------------ | ---- | ----------- | | id | string | Globally unique ID of the role. | | name | string | The role name (e.g., "app:app-name:admin"). | | displayName | string | The human-readable display name for the role. | <br> #### Redirect Responses There are certain scenarios where instead of callback data being returned by the SDK, a redirect URL is returned instead. The following are edge cases where this occurs: - The Login State Cookie is missing by the time Wristband redirects back to the Callback Endpoint. - The `state` query parameter sent from Wristband to your Callback Endpoint does not match the Login State Cookie. - Wristband sends an `error` query parameter to your Callback Endpoint, and it is an expected error type that the SDK knows how to resolve. The location of where the user gets redirected to in these scenarios depends on if the application is using tenant subdomains and if the SDK is able to determine which tenant the user is currently attempting to log in to. The resolution happens in the following order: 1. If the tenant domain can be determined, then the user will get redirected back to your Express Login Endpoint. 2. Otherwise, the user will be sent to the Wristband-hosted Tenant-Level Login Page URL. In these events, the your application should redirect the user to that location. #### Error Parameters Certain edge cases are possible where Wristband encounters an error during the processing of an auth request. These are the following query parameters that are sent for those cases to your Callback Endpoint: | Query Parameter | Description | | --------------- | ----------- | | error | Indicates an error that occurred during the Login Workflow. | | error_description | A human-readable description or explanation of the error to help diagnose and resolve issues more effectively. | ```sh GET https://customer01.yourapp.io/auth/callback?state=f983yr893hf89ewn0idjw8e9f&error=login_required&error_description=User%20must%20re-authenticate%20because%20the%20specified%20max_age%20value%20has%20elapsed ``` The error types that get automatically resolved in the SDK are: | Error | Description | | ----- | ----------- | | login_required | Indicates that the user needs to log in to continue. This error can occur in scenarios where the user's session has expired, the user is not currently authenticated, or Wristband requires the user to explicitly log in again for security reasons. | For all other error types, the SDK will throw a `WristbandError` object (containing the error and description) that your application can catch and handle. Most errors come from SDK configuration issues during development that should be addressed before release to production. <br> ### logout() ```ts // Definition logout(req: Request, res: Response, config?: LogoutConfig): Promise<string>; // Usage const logoutUrl = await logout(req, res, { refreshToken: '98yht308hf902hc90wh09' }); ``` When users of your application are ready to log out and/or their application session expires, your frontend should redirect the user to your Express Logout Endpoint. ```sh GET https://customer01.yourapp.io/auth/logout ``` If your application created a session, it should destroy it before invoking the `logout()` function. This function can also take an optional `LogoutConfig` argument: | LogoutConfig Field | Type | Required | Description | | ------------------ | ---- | -------- | ----------- | | redirectUrl | string | No | Optional URL that Wristband will redirect to after the logout operation has completed. This will also take precedence over the `customApplicationLoginPageUrl` (if specified) in the SDK AuthConfig if the tenant domain cannot be determined when attempting to redirect to the Wristband Logout Endpoint. | | refreshToken | string | No | The refresh token to revoke. | | state | string | No | Optional value that will be appended as a query parameter to the resolved logout URL, if provided. Maximum length of 512 characters. | | tenantCustomDomain | string | No | The tenant custom domain for the tenant that the user belongs to (if applicable). | | tenantName | string | No | The name of the tenant the user belongs to. | #### Which Domains Are Used in the Logout URL? Wristband supports various tenant domain configurations, including subdomains and custom domains. The SDK automatically determines the appropriate domain configuration when constructing the Wristband Logout URL, which your login endpoint will redirect users to duri