UNPKG

@equinor/fusion-framework-module-msal-node

Version:

Fusion Framework module for secure Azure AD authentication in Node.js using MSAL. Supports interactive, silent, and token-only authentication modes with encrypted token storage.

94 lines (87 loc) 4.26 kB
import type { AuthenticationResult, PublicClientApplication } from '@azure/msal-node'; import { createServer } from 'node:http'; import URL from 'node:url'; import { AuthServerError, AuthServerTimeoutError } from './error.js'; const DEFAULT_SERVER_TIMEOUT = 300000 as const; // 5 minutes /** * Creates a temporary HTTP server to handle the OAuth 2.0 authorization code flow for interactive authentication. * * This function is used in interactive authentication scenarios to listen for the authorization code * returned by Azure AD after the user authenticates in the browser. It exchanges the code for an access token * using the provided `PublicClientApplication` instance. The server automatically shuts down after a successful * authentication, error, or timeout. * * @param client - The MSAL `PublicClientApplication` instance used to acquire tokens. * @param scopes - An array of scopes for which the token is requested. * @param options - Configuration for the authentication server. * @param options.port - The port on which the server will listen for the authentication response. * @param options.codeVerifier - The PKCE code verifier used for enhanced security (optional). * @param options.timeout - Timeout in milliseconds before the server shuts down if no response is received (default: 5 minutes). * * @returns A promise that resolves with the `AuthenticationResult` upon successful authentication, * or rejects with an error if authentication fails or times out. * * @throws {@link AuthServerError} If no authorization code is received or if token acquisition fails. * @throws {@link AuthServerTimeoutError} If the server times out before receiving a response. * * @example * ```typescript * const result = await createAuthServer(client, ['user.read'], { port: 3000, codeVerifier }); * console.log(result.accessToken); * ``` */ export const createAuthServer = ( client: PublicClientApplication, scopes: string[], options: { port: number; codeVerifier?: string; timeout?: number; }, ): Promise<AuthenticationResult> => { const { port, timeout = DEFAULT_SERVER_TIMEOUT } = options; return new Promise<AuthenticationResult>((resolve, reject) => { // Set a timeout for the server to close if no response is received in time const timeoutId = setTimeout(() => { server.close(); reject(new AuthServerTimeoutError('Authentication server timed out')); }, timeout); // Create a temporary HTTP server to listen for the OAuth 2.0 redirect const server = createServer(async (req, res) => { // Parse the URL and extract the query parameters from the redirect const query = URL.parse(req.url ?? '', true).query; if (!query.code) { // If no authorization code is present, return an error to the browser and reject the promise const error = new AuthServerError('No authorization code received'); res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(error.message); server.close(); return reject(error); } try { // Attempt to exchange the authorization code for an access token const tokenResponse = await client.acquireTokenByCode({ code: Array.isArray(query.code) ? query.code[0] : query.code, scopes: scopes, codeVerifier: options?.codeVerifier, redirectUri: `http://localhost:${port}`, }); // On success, notify the user in the browser and resolve the promise res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('Authentication successful! You can close this window.'); resolve(tokenResponse); } catch (err) { // If token acquisition fails, return an error to the browser and reject the promise const error = new AuthServerError('Authentication failed', { cause: err as Error }); res.writeHead(500, { 'Content-Type': 'text/html' }); res.end(`<b>${error.message}</b><br>${(err as Error).message}`); reject(error); } finally { // Always clean up: close the server and clear the timeout server.close(); clearTimeout(timeoutId); } }).listen(port); }); }; export default createAuthServer;