UNPKG

@wristband/react-client-auth

Version:

A lightweight React SDK that pairs with your backend server auth to initialize and sync frontend sessions via secure session cookies.

585 lines (575 loc) 23.8 kB
import * as react_jsx_runtime from 'react/jsx-runtime'; import { PropsWithChildren } from 'react'; /** * Represents an error encountered in the Wristband SDK. This error is thrown by various SDK operations * including session retrieval, token acquisition, and other authentication-related failures. It provides * a specific error code for easier handling and debugging, along with the original error if available. * * @example Session error handling * ```typescript * const { authError } = useWristbandAuth(); * * if (authError) { * switch (authError.code) { * case WristbandErrorCode.INVALID_SESSION_RESPONSE: * console.error('Session configuration error:', authError.message); * break; * case WristbandErrorCode.SESSION_FETCH_FAILED: * console.error('Session network error:', authError.message); * break; * default: * console.error('Unexpected error') * } * } * ``` * * @example Token error handling * ```typescript * try { * const token = await getToken(); * } catch (error) { * if (error instanceof WristbandError) { * switch (error.code) { * case WristbandErrorCode.UNAUTHENTICATED: * console.error('User not authenticated'); * break; * case WristbandErrorCode.INVALID_TOKEN_RESPONSE: * console.error('Token endpoint configuration error:', error.message); * break; * case WristbandErrorCode.TOKEN_FETCH_FAILED: * console.error('Token network error:', error.message); * break; * } * } * } * ``` */ declare class WristbandError extends Error { readonly code: WristbandErrorCode; readonly originalError?: unknown | undefined; /** * @param code - The specific {@link WristbandErrorCode} indicating the failure reason. * @param message - A human-readable error message. * @param originalError - (Optional) The original error thrown during the request, if available. */ constructor(code: WristbandErrorCode, message: string, originalError?: unknown | undefined); } /** * Authentication status enum representing the possible states of user authentication. * * This enum is used throughout the authentication flow to represent the current state * of the authentication process and helps components determine what UI to display. */ declare enum AuthStatus { /** * The authentication state is currently being determined. This is the initial state during session validation. */ LOADING = "LOADING", /** * The user is successfully authenticated with a valid session. Protected resources can be accessed in this state. */ AUTHENTICATED = "AUTHENTICATED", /** * The user is not authenticated or the session is invalid. Access to protected resources should be denied in this * state. */ UNAUTHENTICATED = "UNAUTHENTICATED" } /** * Context interface providing authentication state and session data throughout the application. * * This context is provided by the WristbandAuthProvider and can be accessed using the * useWristbandAuth() and useWristbandSession() hooks. * * @template TSessionMetadata - Type for custom session metadata, defaulting to unknown. */ interface IWristbandAuthContext<TSessionMetadata = unknown> { /** * {@link WristbandError} object containing error details when authentication fails, * or `null` when no error has occurred. */ authError: WristbandError | null; /** * Current authentication status representing the state of the authentication process. * Use this for showing appropriate UI based on auth state (loading indicators, login forms, etc.) */ authStatus: AuthStatus; /** * This function clears all client-side auth state including authentication status, user data, * session metadata, and cached tokens. Use this when you need to completely reset the * SDK state, typically for testing, error recovery, or when implementing custom logout flows. * * NOTE: This only clears React state and does not invalidate server-side sessions or * redirect the user. For standard logout, redirect to your logout URL instead. * * @example * ```typescript * const { clearAuthData } = useWristbandAuth(); * * const handleCriticalError = () => { * clearAuthData(); // Reset all state * redirectToLogin('/api/auth/login'); * }; * ``` */ clearAuthData: () => void; /** * Clears the cached access token and forces the next getToken() call to fetch a fresh token, * assuming that the user still has an authenticated session. * * NOTE: This only clears the client-side token cache. If the user's session remains * active, then the getToken() will continue to work by fetching new tokens from the * configured tokenUrl endpoint. * * @example * ```typescript * const { getToken, clearToken } = useWristbandToken(); * * const forceTokenFetch = async () => { * clearToken(); // Clear cached token * const freshToken = await getToken(); // Fetches new token * }; * ``` */ clearToken: () => void; /** * Retrieves a valid access token for making authenticated API calls to resource servers. Returns a * cached token if available and not expired; otherwise fetches a fresh token from the configured * "tokenUrl" endpoint. Your server's Token Endpoint should automatically handle token expiration and * refresh using the user's session cookie. * * If the token endpoint returns a 401 (unauthorized), the cached token state will be * automatically cleared and the user may be redirected to login depending on configuration. * * @returns Promise that resolves to a valid access token string * @throws Error if tokenUrl is not configured, user is not authenticated, or token retrieval fails * * @example * ```typescript * const { getToken } = useWristbandToken(); * * const callAPI = async () => { * try { * const token = await getToken(); * const response = await fetch('/api/protected', { * headers: { 'Authorization': `Bearer ${token}` } * }); * } catch (error) { * console.error('Token retrieval failed:', error); * } * }; * ``` */ getToken: () => Promise<string>; /** * Boolean flag indicating if the user is authenticated. * Use this for conditional rendering of authenticated content. */ isAuthenticated: boolean; /** * Boolean flag indicating if the authentication state is still being determined. * Use this to show loading indicators during initial session validation. */ isLoading: boolean; /** * Custom metadata associated with the authenticated session. The type is defined by the TSessionMetadata * generic parameter. Available only when authentication is successful. */ metadata: TSessionMetadata; /** * Identifier for the tenant associated with the authenticated session. * Available only when authentication is successful. */ tenantId: string; /** * Function to update the session metadata. This allows components to modify session data without replacing * the entire object. * * @param newMetadata - Partial metadata object containing properties to update */ updateMetadata: (newMetadata: Partial<TSessionMetadata>) => void; /** * Unique identifier for the authenticated user. * Available only when authentication is successful. */ userId: string; } interface IWristbandAuthProviderProps<TSessionMetadata = unknown> extends PropsWithChildren<{ /** * Name of the CSRF cookie that the server sets. This enables CSRF protection for requests made to your * backend server. * @default 'CSRF-TOKEN' */ csrfCookieName?: string; /** * Name of the CSRF header that will be sent with authenticated requests. This should match the header name * your server expects for CSRF validation. * @default 'X-CSRF-TOKEN' */ csrfHeaderName?: string; /** * When true, unauthenticated users will remain on the current page instead of being redirected to your backend * server's Login Endpoint. This is useful for public pages that have both authenticated and * unauthenticated states. * @default false */ disableRedirectOnUnauthenticated?: boolean; /** * This URL should point to your backend server's Login Endpoint that handles the authentication flow with Wristband. * @required */ loginUrl: string; /** * Callback that executes after a successful session response but before authentication state updates. * * This hook is designed for operations that must complete before the component tree re-renders * due to authentication state changes. Common use cases include: * * - Caching user data with state management solutions (React Query, Redux, etc.) * - Preloading critical resources needed immediately after authentication * - Initializing analytics or monitoring services with user identity * - Setting up user-specific configurations * * If this callback returns a Promise, the authentication state update will be * delayed until the Promise resolves, ensuring all async operations complete first. * * NOTE: For simply transforming the metadata stored in context, use transformSessionMetadata() instead. * * @example * onSessionSuccess={async (sessionResponse) => { * // Cache user data in React Query * queryClient.setQueryData(['user-profile'], sessionResponse.metadata.profile); * * // Prefetch critical user permissions * await queryClient.prefetchQuery( * ['user-permissions'], * () => fetchPermissions(sessionResponse.userId) * ); * }} * * @param sessionResponse The complete session data returned from the session endpoint * @returns void or a Promise that resolves when all operations are complete */ onSessionSuccess?: (sessionResponse: SessionResponse) => Promise<void> | void; /** * This URL should point to your backend server's Session Endpoint, which returns an authenticated user's userId, * tenantId, and any optional metadata. * @required */ sessionUrl: string; /** * Optional URL endpoint for retrieving access tokens from your application's backend server. * When provided, enables getToken() functionality in the useWristbandAuth() hook for making * direct API calls to resource servers with Bearer tokens. If not provided, applications should * use session-based authentication for all API calls. * * The token endpoint should: * - Ensure the user has a valid session cookie * - Handle token refresh automatically using the session state, if necessary. * - Return a JSON response with { accessToken: string, expiresAt: number } * * @example "/api/auth/token" */ tokenUrl?: string; /** * Function to transform raw metadata from the session response before storing it in context. * * Use this to format, type, or filter the metadata that your components will access * via the useWristbandSession() hook. This is particularly useful for: * * - Converting data types (e.g., string dates to Date objects) * - Adding computed properties * - Filtering out unnecessary properties * - Ensuring type safety for the metadata * * @example * transformSessionMetadata={(rawMetadata: unknown): MySessionData => ({ * name: rawMetadata.displayName, * email: rawMetadata.email, * hasOwnerRole: rawMetadata.roles.some(role => isAdminRole(role.name) * })} * * @param rawSessionMetadata The unprocessed metadata object from the session response * @returns A transformed metadata object of type TSessionMetadata */ transformSessionMetadata?: (rawSessionMetadata: unknown) => TSessionMetadata; }> { } /** * Response structure returned by the session endpoint. * * This interface represents the expected data structure your backend server should return from its Session Endpoint. * It contains the essential user information needed to establish an authenticated session in your application. * * @interface SessionResponse * @property {unknown} metadata - Additional user data or session information. The type is 'unknown' to allow flexible schema that can be transformed by your application as needed. * @property {string} tenantId - The identifier for the tenant the user belongs to. * @required * @property {string} userId - The unique identifier for the authenticated user. * @required * * @example * { * "userId": "user123456", * "tenantId": "tenant123456", * "metadata": { * "displayName": "Jane Doe", * "email": "jane@example.com", * "roles": ["admin", "user"] * } * } */ interface SessionResponse { metadata: unknown; tenantId: string; userId: string; } /** * Error codes for failures in the Wristband SDK. */ declare enum WristbandErrorCode { /** An invalid login URL value was provided to the SDK. */ INVALID_LOGIN_URL = "INVALID_LOGIN_URL", /** An invalid logout URL value was provided to the SDK (primarily for `redirectToLogout()`). */ INVALID_LOGOUT_URL = "INVALID_LOGOUT_URL", /** The session endpoint response is missing required fields. */ INVALID_SESSION_RESPONSE = "INVALID_SESSION_RESPONSE", /** An invalid session URL value was provided to the SDK. */ INVALID_SESSION_URL = "INVALID_SESSION_URL", /** The token endpoint response is missing required fields. */ INVALID_TOKEN_RESPONSE = "INVALID_TOKEN_RESPONSE", /** An invalid token URL value was provided to the SDK (only occurs if using `getToken()`). */ INVALID_TOKEN_URL = "INVALID_TOKEN_URL", /** The session endpoint returned an error other than 401. */ SESSION_FETCH_FAILED = "SESSION_FETCH_FAILED", /** The token endpoint returned an error other than 401. */ TOKEN_FETCH_FAILED = "TOKEN_FETCH_FAILED", /** The user is not authenticated and cannot request a session or token. */ UNAUTHENTICATED = "UNAUTHENTICATED" } /** * WristbandAuthProvider establishes an authenticated session with your backend server * by making a request to your session endpoint. It manages authentication state and * provides session data to all child components through React Context. * * This component should be placed near the root of your application to make authentication * state and session data available throughout your component tree. * * @example Basic usage * ```jsx * function App() { * return ( * <WristbandAuthProvider * loginUrl="/api/auth/login" * sessionUrl="/api/auth/session" * > * <YourAppComponents /> * </WristbandAuthProvider> * ); * } * ``` * * * @example Using access tokens directly in React * ```jsx * function App() { * return ( * <WristbandAuthProvider * loginUrl="/api/auth/login" * sessionUrl="/api/auth/session" * tokenUrl="/api/auth/token" * > * <YourAppComponents /> * </WristbandAuthProvider> * ); * } * ``` * * @example With custom session metadata handling * ```jsx * function App() { * const queryClient = useQueryClient(); * * return ( * <WristbandAuthProvider * loginUrl="/api/auth/login" * sessionUrl="/api/auth/session" * transformSessionMetadata={(rawMetadata) => ({ * name: rawMetadata.displayName, * email: rawMetadata.email, * role: rawMetadata.userRole * })} * onSessionSuccess={(sessionData) => { * // Cache additional data in React Query * queryClient.setQueryData(['user-permissions'], sessionData.permissions); * }} * > * <YourAppComponents /> * </WristbandAuthProvider> * ); * } * ``` * * Once rendered, child components can access authentication state using the hooks: * - useWristbandAuth() - For authentication status (isAuthenticated, isLoading, authStatus, clearAuthData) * - useWristbandSession() - For session data (userId, tenantId, metadata) * - useWristbandToken() - For client-side token management, if applicable (getToken, clearToken) * * @template TSessionMetaData - Type for the transformed session metadata, if applicable. */ declare function WristbandAuthProvider<TSessionMetaData = unknown>({ children, csrfCookieName, csrfHeaderName, disableRedirectOnUnauthenticated, loginUrl, onSessionSuccess, sessionUrl, tokenUrl, transformSessionMetadata, }: IWristbandAuthProviderProps<TSessionMetaData>): react_jsx_runtime.JSX.Element; declare function useWristbandAuth(): Pick<IWristbandAuthContext, 'isAuthenticated' | 'isLoading' | 'authStatus' | 'authError' | 'clearAuthData'>; declare function useWristbandSession<TSessionData = unknown>(): { metadata: TSessionData; tenantId: string; userId: string; updateMetadata: (newMetadata: Partial<TSessionData>) => void; }; declare function useWristbandToken(): Pick<IWristbandAuthContext, 'getToken' | 'clearToken'>; /** * Configuration options for customizing the login redirect behavior. */ interface LoginRedirectConfig { /** * Optional hint to pre-fill the Tenant Login Page form with a specific username or email. * * When provided, this value will be sent as the "login_hint" query parameter, which can be used by your * authentication endpoint to pre-populate the login form, improving the user experience by reducing manual input. * * @example "user@example.com" */ loginHint?: string; /** * Optional URL to redirect back to after successful authentication. * * This URL will be sent as the "return_url" query parameter to your Login Endpoint. After successful login, your * auth flow should redirect users back to this URL. If not provided, the current page URL will be used as the * return destination. * * @example "https://app.example.com/dashboard" */ returnUrl?: string; /** * Optional tenant domain name for directing users to the correct Tenant Login Page. * * When provided, this value is sent as the "tenant_domain" query parameter to your backend server's Login Ednpoint, * enabling automatic tenant routing during login. * * This parameter is primarily useful when: * - All tenants in your application share a common app URL (i.e. no subdomains, no custom domains) * - You want to avoid tenant discovery during login * * @example "acme-corp" // Directs to Acme Corporation's Tenant Login Page */ tenantDomain?: string; /** * Optional tenant custom domain for directing users to the correct Tenant Login Page. * * When provided, this value is sent as the "tenant_custom_domain" query parameter to your * backend server's Login Endpoint, allowing routing to tenant custom domains. * * This parameter is useful when: * - Tenants have their own custom domains * - You want to avoid tenant discovery during login * * @example "auth.acme.com" // Directs to Acme's tenant custom domain */ tenantCustomDomain?: string; } /** * Configuration options for redirecting to your server's logout endpoint. * @interface LogoutRedirectConfig */ interface LogoutRedirectConfig { /** * Optional tenant domain name for directing users to the correct Tenant Login Page. * * When provided, this value is sent as the "tenant_domain" query parameter to your backend server's Logout Ednpoint, * enabling automatic tenant routing during logout. * * This parameter is primarily useful when: * - All tenants in your application share a common app URL (i.e. no subdomains, no custom domains) * - You want to avoid tenant discovery during login * * @example "acme-corp" // Directs to Acme Corporation's Tenant Login Page */ tenantDomain?: string; /** * Optional tenant custom domain for directing users to the correct Tenant Login Page. * * When provided, this value is sent as the "tenant_custom_domain" query parameter to your * backend server's Login Endpoint, allowing routing to tenant custom domains. * * This parameter is useful when: * - Tenants have their own custom domains * - You want to avoid tenant discovery during login * * @example "auth.acme.com" // Directs to Acme's tenant custom domain */ tenantCustomDomain?: string; } /** * Redirects the user to your backend server's Login Endpoint with optional configuration parameters (browser only). * * This function initiates a redirect to the specified login URL and appends relevant query parameters * based on the provided configuration. The redirect will preserve the current page URL as the return * destination by default, allowing users to resume where they left off after authentication. * * @param {string} loginUrl - The login endpoint URL that handles authentication * @param {LoginRedirectConfig} config - Optional configuration for customizing the login experience * @returns {Promise<void>} A promise that resolves when the redirect is triggered * @throws {WristbandError} If loginUrl is undefined, null, or empty. * * @example * // Basic redirect to login endpoint * await redirectToLogin('/api/auth/login'); * * @example * // Redirect with pre-filled email address * await redirectToLogin('/api/auth/login', { * loginHint: 'user@example.com' * }); * * @example * // Redirect with custom return destination and tenant domain * await redirectToLogin('/api/auth/login', { * returnUrl: 'https://app.example.com/dashboard', * tenantDomain: 'acme-corp' * }); * * @example * // Redirect with custom return destination and tenant domain * await redirectToLogin('/api/auth/login', { * returnUrl: 'https://app.example.com/dashboard', * tenantCustomDomain: 'auth.acme.com' * }); */ declare function redirectToLogin(loginUrl: string, config?: LoginRedirectConfig): void; /** * Redirects the user to your backend server's Logout Endpoint with optional configuration (browser only). * * This function navigates the user to the specified logout URL and can append additional parameters as needed. * * @param {string} logoutUrl - The URL of your server's Logout Endpoint * @param {LogoutRedirectConfig} config - Optional configuration for the logout redirect * @returns {Promise<void>} A promise that resolves when the redirect is triggered * @throws {WristbandError} If logoutUrl is undefined, null, or empty. * * @example * // Basic redirect to logout endpoint * await redirectToLogout('/api/auth/logout'); * * @example * // Redirect with tenant domain parameter * await redirectToLogout('/api/auth/logout', { * tenantDomain: 'acme-corp' * }); * * @example * // Redirect with tenant domain parameter * await redirectToLogout('/api/auth/logout', { * tenantCustomDomain: 'auth.acme.com' * }); */ declare function redirectToLogout(logoutUrl: string, config?: LogoutRedirectConfig): void; export { AuthStatus, WristbandAuthProvider, WristbandError, WristbandErrorCode, redirectToLogin, redirectToLogout, useWristbandAuth, useWristbandSession, useWristbandToken }; export type { LoginRedirectConfig, LogoutRedirectConfig, SessionResponse };