UNPKG

@joouis/msal-react-utility

Version:

The utility package for @azure/msal-react.

354 lines (266 loc) 10.1 kB
# MSAL-REACT-Utility ## Introduction MSAL-REACT-Utility is a lightweight supplementary package for `@azure/msal-react` that addresses common authentication challenges in React applications using Microsoft Authentication Library (MSAL). While MSAL provides robust authentication capabilities, developers often struggle with complex token management, race conditions, verbose error handling, and repetitive response parsing code, requiring significant boilerplate in their applications. This package provides a set of custom React hooks and utility functions that abstract these complexities away with zero configuration, automatic token refresh, simplified type-safe APIs, built-in error handling, and integrated loading state management. By handling the authentication plumbing for you, MSAL-REACT-Utility allows you to focus on building your application's core features rather than wrestling with authentication logic. ## Usage This package provides React hooks and utility functions to simplify authentication and API calls in React applications using MSAL. ### Types and Interfaces ```typescript // Core types used throughout the library enum TokenType { id = 'id', // For ID tokens access = 'access', // For access tokens } interface IGetTokenOptions { tokenType?: TokenType; // Type of token to request (default: TokenType.access) requestConfigs?: SilentRequest; // Optional MSAL request configurations } // Error thrown when trying to make a request while another is in progress class RequestInProgressError extends Error { constructor() { super('Request is in progress!'); } } ``` ### Hooks #### useGetToken Acquires an access or ID token for authentication. ```typescript import { useGetToken, TokenType } from '@joouis/msal-react-utility'; const MyComponent = () => { // You can provide default request configuration when initializing the hook const getToken = useGetToken({ scopes: ['User.Read', 'Mail.Read'], prompt: 'select_account' }); const fetchData = async () => { // Get access token (default) const accessToken = await getToken(); // Get ID token specifically const idToken = await getToken({ tokenType: TokenType.id }); // Get token with custom request config const customToken = await getToken({ tokenType: TokenType.access, requestConfigs: { scopes: ['Files.Read'], forceRefresh: true } }); }; return <button onClick={fetchData}>Fetch Data</button>; }; ``` **Input:** - `defaultRequestConfigs?: SilentRequest` - Default token request configuration from MSAL **Output:** - `GetToken` function: `(opts?: IGetTokenOptions) => Promise<string | undefined>` - `opts.tokenType`: Specifies whether to return an access token or ID token (default: `TokenType.access`) - `opts.requestConfigs`: Override or extend the default request configurations **Notes:** - Automatically handles token caching and refresh - Manages token acquisition during MSAL interaction flows - Falls back to redirect login flow if no active account is found - For ID tokens, automatically refreshes if token is close to expiration (< 2 minutes) #### useFetchWithToken Makes authenticated API requests with automatic token handling. ```typescript import { useFetchWithToken } from '@joouis/msal-react-utility'; const MyComponent = () => { // Can provide default token request configuration const fetchWithToken = useFetchWithToken({ scopes: ['User.Read', 'api://your-api/access'] }); const fetchData = async () => { const response = await fetchWithToken( 'https://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json' // Authorization header is automatically added }, body: JSON.stringify({ key: 'value' }), }, // Optional token options { tokenType: TokenType.access, requestConfigs: { forceRefresh: true } } ); if (response.ok) { const data = await response.json(); console.log(data); } }; return <button onClick={fetchData}>Fetch Data</button>; }; ``` **Input:** - `tokenRequestConfigs?: SilentRequest` - Optional token request configuration **Output:** - Fetch function: `(input: string | URL | Request, init?: RequestInit, getTokenOpts?: IGetTokenOptions) => Promise<Response>` - Returns a standard Fetch Response with Authorization header automatically added - Returns an error Response with status 401 if token acquisition fails #### useFetchWithStatus Makes authenticated API requests with loading state management. ```typescript import { useFetchWithStatus } from '@joouis/msal-react-utility'; // Specify the expected return type as a generic parameter interface UserData { id: string; name: string; email: string; } const MyComponent = () => { // The hook manages loading state internally const { isLoading, _fetch } = useFetchWithStatus<UserData>( 'https://api.example.com/user', { method: 'GET', headers: { 'Accept': 'application/json' } } ); const fetchUserData = async () => { try { // Will throw RequestInProgressError if called while already loading const userData = await _fetch( // Optional request payload to override defaults { method: 'POST', body: JSON.stringify({ filter: 'active' }) }, // Optional token options { tokenType: TokenType.access } ); console.log(userData.name); // Typed as UserData } catch (error) { // RequestInProgressError is thrown if a request is already in progress if (error instanceof RequestInProgressError) { console.log('Request already in progress'); } else { console.error('Failed to fetch user data', error); } } }; return ( <div> <button onClick={fetchUserData} disabled={isLoading}> {isLoading ? 'Loading...' : 'Fetch User Data'} </button> </div> ); }; ``` **Input:** - `input: string | URL | Request` - The resource to fetch - `init?: RequestInit` - Optional fetch configuration **Output:** - Object containing: - `isLoading: boolean` - Whether a request is currently in progress - `_fetch: (payload?: RequestInit, getTokenOpts?: IGetTokenOptions) => Promise<T>` - Function to make authenticated requests **Notes:** - Prevents multiple simultaneous requests to the same endpoint - Manages loading state automatically - Properly types the response data - Integrates with `getResponseData` to parse the response based on content type #### useEventCallback Creates a callback with stable reference identity across renders. ```typescript import { useEventCallback } from '@joouis/msal-react-utility'; import { useState } from 'react'; interface FormData { name: string; email: string; } const MyComponent = ({ onSubmit }: { onSubmit: (data: FormData) => void }) => { const [formData, setFormData] = useState<FormData>({ name: '', email: '' }); // This callback's identity remains stable even when formData changes // This is useful for event handlers that need to reference latest state // without causing unnecessary re-renders of child components const handleSubmit = useEventCallback(() => { console.log('Submitting with data:', formData); onSubmit(formData); }); return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}> {/* Form inputs */} <button type="submit">Submit</button> </form> ); }; ``` **Input:** - `handler: T extends (...args: any[]) => any` - Function to stabilize **Output:** - Stabilized function with the same signature as the input **Notes:** - Useful for callbacks that depend on frequently changing values but shouldn't trigger re-renders - Solves the issue of creating new function references in render - Similar to the upcoming React `useEvent` hook ### Utilities #### getResponseData Parses response data based on content type. ```typescript import { getResponseData } from '@joouis/msal-react-utility'; interface ApiResponse { results: Array<{ id: number; name: string }>; pagination: { next: string | null }; } const fetchData = async () => { const response = await fetch('https://api.example.com/data'); if (!response.ok) { throw new Error(`Error: ${response.status} ${response.statusText}`); } // Automatically determines how to parse the response based on Content-Type header const data = await getResponseData<ApiResponse>(response); // Data is properly typed console.log(`Found ${data.results.length} items`); return data; }; ``` **Input:** - `response: Response` - Fetch response object **Output:** - `Promise<T>` - Promise resolving to parsed data of type T **Supported Content Types:** - `application/json` - Parses as JSON - `text/*` - Returns as text - `application/octet-stream` - Returns as ArrayBuffer - `application/xml` or `text/xml` - Returns as text - `image/*`, `video/*`, `audio/*`, `application/pdf` - Returns as Blob - Other types - Attempts JSON parsing first, falls back to text #### sleep Delays execution for specified milliseconds. ```typescript import { sleep } from '@joouis/msal-react-utility'; const delayedOperation = async () => { console.log('Starting operation'); // Wait for 1 second await sleep(1000); console.log('After 1 second'); // Can be used in polling scenarios let attempts = 0; while (attempts < 5) { try { const result = await checkOperationStatus(); if (result.status === 'completed') { return result.data; } attempts++; await sleep(2000); // Wait 2 seconds between attempts } catch (error) { console.error('Error checking status', error); } } throw new Error('Operation timed out'); }; ``` **Input:** - `ms: number` - Milliseconds to delay **Output:** - `Promise<void>` - Promise that resolves after the delay ## TODO - A context to register logger and default auth configs. - Add test script.