@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.
122 lines • 5.87 kB
JavaScript
import { createHash, randomBytes } from 'node:crypto';
import openBrowser from 'open';
import { createAuthServer } from './create-auth-server.js';
import { AuthProvider } from './AuthProvider.js';
/**
* Encodes a byte buffer to Base64 URL encoding (RFC 4648 §5) without padding.
*
* Used for PKCE code verifier/challenge values (RFC 7636).
*
* @param buffer - Bytes to encode.
* @returns Base64url-encoded string without trailing `=` padding.
*/
const base64UrlEncode = (buffer) => buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
/**
* Generates PKCE verifier and challenge for the authorization code flow.
*
* Uses a 32-octet cryptographically random verifier (recommended by RFC 7636)
* and derives the S256 challenge as `BASE64URL-ENCODE(SHA256(ASCII(verifier)))`.
*
* @returns PKCE verifier and S256 challenge.
*/
const generatePkceCodes = () => {
const verifier = base64UrlEncode(randomBytes(32));
const challenge = base64UrlEncode(createHash('sha256').update(verifier).digest());
return { verifier, challenge };
};
/**
* Implementation of an interactive authentication provider for the Fusion MSAL Node module.
*
* Extends {@link AuthProvider} to support user-driven authentication flows using the authorization code flow with PKCE.
* This class opens the user's default browser for authentication and handles the response via a local server.
*
* This implementation is intended for scenarios where interactive login is required, such as CLI tools or development utilities.
*
* Developers extending this provider can customize the interactive flow, server handling, or PKCE logic as needed.
* Ensure that any changes remain consistent with the expected interface and security best practices.
*
* @see AuthProvider for non-interactive (silent) authentication flows.
* @see AuthProviderTokenOnly for token-only scenarios.
*/
export class AuthProviderInteractive extends AuthProvider {
#options;
/**
* @param client - The MSAL `PublicClientApplication` instance used for token operations.
* @param options - Interactive login options, including local server configuration.
*/
constructor(client, options) {
super(client);
this.#options = options;
}
/**
* Initiates the login process using the authorization code flow with PKCE.
*
* This method generates a PKCE code verifier and challenge to enhance security
* and prevent authorization code interception attacks. It constructs an
* authorization code URL, opens the default browser for user authentication,
* and starts a local server to handle the authentication response.
*
* @param scopes - An array of scopes that specify the permissions being requested.
* @returns A promise that resolves to an `AuthenticationResult` containing the
* authentication details upon successful login.
*
* @throws Will throw an error if the PKCE code generation, browser opening, or
* authentication server setup fails.
*/
async login(options) {
const { scopes } = options.request;
const { port, onOpen } = this.#options.server;
// Generate a new PKCE code verifier and challenge
// This is used to enhance security in the authorization code flow
// by preventing authorization code interception attacks.
const { verifier, challenge } = generatePkceCodes();
const authCodeUrl = await this._client.getAuthCodeUrl({
scopes,
redirectUri: `http://localhost:${port}`,
codeChallenge: challenge,
codeChallengeMethod: 'S256',
});
// open default browser to authenticate
await openBrowser(authCodeUrl);
// callback to open the auth code url
if (onOpen)
onOpen(authCodeUrl);
return createAuthServer(this._client, scopes, {
codeVerifier: verifier,
port,
});
}
/**
* Acquires an authentication token for the specified scopes.
*
* This method first attempts to acquire a token silently using the accounts
* available in the token cache. If no accounts are found and interactive login
* is allowed, it initiates an interactive login flow. If interactive login is
* not allowed and no accounts are found, an error is thrown.
*
* @param scopes - An array of strings representing the scopes for which the token is requested.
* @param options - Optional parameters for token acquisition.
* @param options.interactive - A boolean indicating whether interactive login is allowed
* if no accounts are found in the cache. Defaults to `false`.
* @returns A promise that resolves to an `AuthenticationResult` containing the acquired token.
* @throws {@link NoAccountsError} If no accounts are found in the cache and interactive login is not allowed.
* @throws {@link SilentTokenAcquisitionError} If an error occurs during silent token acquisition.
*/
async acquireToken(options) {
const { scopes } = options.request ?? { scopes: [] };
// No cached account — fall back to an interactive login flow
if ((await this.getAccount()) === null) {
return this.login({ request: { scopes } });
}
try {
return await super.acquireToken(options);
}
catch {
// Silent acquisition failed (e.g. no cached token for this resource/audience).
// Fall back to interactive login so the user only needs one browser session
// rather than seeing an unhandled error or a separate prompted login elsewhere.
return this.login({ request: { scopes } });
}
}
}
//# sourceMappingURL=AuthProviderInteractive.js.map