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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/dist/esm/AuthConfigurator.interface.js +2 -0
  5. package/dist/esm/AuthConfigurator.interface.js.map +1 -0
  6. package/dist/esm/AuthConfigurator.js +112 -0
  7. package/dist/esm/AuthConfigurator.js.map +1 -0
  8. package/dist/esm/AuthProvider.interface.js +2 -0
  9. package/dist/esm/AuthProvider.interface.js.map +1 -0
  10. package/dist/esm/AuthProvider.js +109 -0
  11. package/dist/esm/AuthProvider.js.map +1 -0
  12. package/dist/esm/AuthProviderInteractive.js +88 -0
  13. package/dist/esm/AuthProviderInteractive.js.map +1 -0
  14. package/dist/esm/AuthTokenProvider.js +50 -0
  15. package/dist/esm/AuthTokenProvider.js.map +1 -0
  16. package/dist/esm/create-auth-cache.js +67 -0
  17. package/dist/esm/create-auth-cache.js.map +1 -0
  18. package/dist/esm/create-auth-client.js +35 -0
  19. package/dist/esm/create-auth-client.js.map +1 -0
  20. package/dist/esm/create-auth-server.js +81 -0
  21. package/dist/esm/create-auth-server.js.map +1 -0
  22. package/dist/esm/enable-module.js +31 -0
  23. package/dist/esm/enable-module.js.map +1 -0
  24. package/dist/esm/error.js +64 -0
  25. package/dist/esm/error.js.map +1 -0
  26. package/dist/esm/index.js +4 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/module.js +26 -0
  29. package/dist/esm/module.js.map +1 -0
  30. package/dist/esm/version.js +3 -0
  31. package/dist/esm/version.js.map +1 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -0
  33. package/dist/types/AuthConfigurator.d.ts +55 -0
  34. package/dist/types/AuthConfigurator.interface.d.ts +153 -0
  35. package/dist/types/AuthProvider.d.ts +81 -0
  36. package/dist/types/AuthProvider.interface.d.ts +55 -0
  37. package/dist/types/AuthProviderInteractive.d.ts +73 -0
  38. package/dist/types/AuthTokenProvider.d.ts +43 -0
  39. package/dist/types/create-auth-cache.d.ts +32 -0
  40. package/dist/types/create-auth-client.d.ts +24 -0
  41. package/dist/types/create-auth-server.d.ts +34 -0
  42. package/dist/types/enable-module.d.ts +24 -0
  43. package/dist/types/error.d.ts +59 -0
  44. package/dist/types/index.d.ts +5 -0
  45. package/dist/types/module.d.ts +24 -0
  46. package/dist/types/version.d.ts +1 -0
  47. package/package.json +46 -0
  48. package/src/AuthConfigurator.interface.ts +163 -0
  49. package/src/AuthConfigurator.ts +131 -0
  50. package/src/AuthProvider.interface.ts +53 -0
  51. package/src/AuthProvider.ts +119 -0
  52. package/src/AuthProviderInteractive.ts +117 -0
  53. package/src/AuthTokenProvider.ts +56 -0
  54. package/src/create-auth-cache.ts +85 -0
  55. package/src/create-auth-client.ts +40 -0
  56. package/src/create-auth-server.ts +93 -0
  57. package/src/enable-module.ts +35 -0
  58. package/src/error.ts +66 -0
  59. package/src/index.ts +9 -0
  60. package/src/module.ts +52 -0
  61. package/src/version.ts +2 -0
  62. package/tsconfig.json +15 -0
@@ -0,0 +1,85 @@
1
+ import {
2
+ DataProtectionScope,
3
+ Environment,
4
+ PersistenceCreator,
5
+ PersistenceCachePlugin,
6
+ type IPersistence,
7
+ } from '@azure/msal-node-extensions';
8
+
9
+ import { tmpdir } from 'node:os';
10
+
11
+ import path from 'node:path';
12
+
13
+ /**
14
+ * Resolves the directory path for storing the authentication cache.
15
+ *
16
+ * Uses the user's root directory if available, otherwise falls back to the OS temp directory.
17
+ *
18
+ * @returns The resolved cache directory path as a string.
19
+ */
20
+ const resolveCachePath = () => {
21
+ return Environment?.getUserRootDirectory() ?? tmpdir();
22
+ };
23
+
24
+ /**
25
+ * Resolves the file path for the authentication cache based on tenant and client IDs.
26
+ *
27
+ * @param tenantId - The Azure AD tenant ID.
28
+ * @param clientId - The Azure AD client/application ID.
29
+ * @returns The full file path for the cache file.
30
+ */
31
+ const resolveCacheFilePath = (tenantId: string, clientId: string) => {
32
+ return path.join(resolveCachePath(), `.token-cache-${tenantId}_${clientId}`);
33
+ };
34
+
35
+ /**
36
+ * Creates a persistence cache for storing authentication data securely on disk.
37
+ *
38
+ * The cache is encrypted and scoped to the current user for security. It is uniquely identified
39
+ * by the provided tenant and client IDs, and is associated with the 'fusion-framework' service.
40
+ *
41
+ * @param tenantId - The Azure AD tenant ID used to identify the cache.
42
+ * @param clientId - The Azure AD client/application ID used to identify the cache.
43
+ * @returns A promise that resolves to the created persistence cache instance.
44
+ */
45
+ export const createPersistenceCache = async (
46
+ tenantId: string,
47
+ clientId: string,
48
+ ): Promise<IPersistence> => {
49
+ return PersistenceCreator.createPersistence({
50
+ cachePath: resolveCacheFilePath(tenantId, clientId),
51
+ serviceName: 'fusion-framework',
52
+ accountName: [tenantId, clientId].join('_'),
53
+ dataProtectionScope: DataProtectionScope.CurrentUser,
54
+ });
55
+ };
56
+
57
+ /**
58
+ * Clears the persistence cache for a specific tenant and client.
59
+ *
60
+ * Deletes the cache file and all associated authentication data for the given tenant and client IDs.
61
+ *
62
+ * @param tenantId - The Azure AD tenant ID.
63
+ * @param clientId - The Azure AD client/application ID.
64
+ * @returns A promise that resolves when the cache has been successfully cleared.
65
+ */
66
+ export const clearPersistenceCache = async (tenantId: string, clientId: string): Promise<void> => {
67
+ const cache = await createPersistenceCache(tenantId, clientId);
68
+ await cache.delete();
69
+ };
70
+
71
+ /**
72
+ * Creates a `PersistenceCachePlugin` instance for use with MSAL, using the provided tenant and client IDs.
73
+ *
74
+ * This plugin enables MSAL to use the secure persistence cache for token storage.
75
+ *
76
+ * @param tenantId - The Azure AD tenant ID.
77
+ * @param clientId - The Azure AD client/application ID.
78
+ * @returns A promise that resolves to an instance of `PersistenceCachePlugin`.
79
+ */
80
+ export const createPersistenceCachePlugin = async (
81
+ tenantId: string,
82
+ clientId: string,
83
+ ): Promise<PersistenceCachePlugin> => {
84
+ return new PersistenceCachePlugin(await createPersistenceCache(tenantId, clientId));
85
+ };
@@ -0,0 +1,40 @@
1
+ import { PublicClientApplication } from '@azure/msal-node';
2
+
3
+ import { createPersistenceCachePlugin } from './create-auth-cache.js';
4
+
5
+ /**
6
+ * Creates and configures a new MSAL PublicClientApplication instance for Azure AD authentication.
7
+ *
8
+ * This function sets up the client with the specified tenant and client IDs, and attaches a secure
9
+ * persistence cache plugin for storing authentication tokens on disk. The resulting client can be used
10
+ * for both silent and interactive authentication flows, depending on how it is integrated.
11
+ *
12
+ * @param tenantId - The Azure Active Directory tenant ID (directory identifier).
13
+ * @param clientId - The client/application ID registered in Azure AD.
14
+ * @returns A Promise that resolves to a configured instance of `PublicClientApplication`.
15
+ *
16
+ * @remarks
17
+ * The returned client uses a secure, user-scoped cache for token storage. This is recommended for CLI tools,
18
+ * background services, and other Node.js applications that require persistent authentication.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const client = await createAuthClient('your-tenant-id', 'your-client-id');
23
+ * // Use the client with MSAL APIs for authentication flows
24
+ * ```
25
+ */
26
+ export const createAuthClient = async (
27
+ tenantId: string,
28
+ clientId: string,
29
+ ): Promise<PublicClientApplication> => {
30
+ const cachePlugin = await createPersistenceCachePlugin(tenantId, clientId);
31
+ return new PublicClientApplication({
32
+ auth: {
33
+ clientId: clientId,
34
+ authority: `https://login.microsoftonline.com/${tenantId}`,
35
+ },
36
+ cache: { cachePlugin },
37
+ });
38
+ };
39
+
40
+ export default createAuthClient;
@@ -0,0 +1,93 @@
1
+ import type { AuthenticationResult, PublicClientApplication } from '@azure/msal-node';
2
+ import { createServer } from 'node:http';
3
+ import URL from 'node:url';
4
+
5
+ import { AuthServerError, AuthServerTimeoutError } from './error.js';
6
+
7
+ const DEFAULT_SERVER_TIMEOUT = 300000 as const; // 5 minutes
8
+
9
+ /**
10
+ * Creates a temporary HTTP server to handle the OAuth 2.0 authorization code flow for interactive authentication.
11
+ *
12
+ * This function is used in interactive authentication scenarios to listen for the authorization code
13
+ * returned by Azure AD after the user authenticates in the browser. It exchanges the code for an access token
14
+ * using the provided `PublicClientApplication` instance. The server automatically shuts down after a successful
15
+ * authentication, error, or timeout.
16
+ *
17
+ * @param client - The MSAL `PublicClientApplication` instance used to acquire tokens.
18
+ * @param scopes - An array of scopes for which the token is requested.
19
+ * @param options - Configuration for the authentication server.
20
+ * @param options.port - The port on which the server will listen for the authentication response.
21
+ * @param options.codeVerifier - The PKCE code verifier used for enhanced security (optional).
22
+ * @param options.timeout - Timeout in milliseconds before the server shuts down if no response is received (default: 5 minutes).
23
+ *
24
+ * @returns A promise that resolves with the `AuthenticationResult` upon successful authentication,
25
+ * or rejects with an error if authentication fails or times out.
26
+ *
27
+ * @throws {@link AuthServerError} If no authorization code is received or if token acquisition fails.
28
+ * @throws {@link AuthServerTimeoutError} If the server times out before receiving a response.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const result = await createAuthServer(client, ['user.read'], { port: 3000, codeVerifier });
33
+ * console.log(result.accessToken);
34
+ * ```
35
+ */
36
+ export const createAuthServer = (
37
+ client: PublicClientApplication,
38
+ scopes: string[],
39
+ options: {
40
+ port: number;
41
+ codeVerifier?: string;
42
+ timeout?: number;
43
+ },
44
+ ): Promise<AuthenticationResult> => {
45
+ const { port, timeout = DEFAULT_SERVER_TIMEOUT } = options;
46
+ return new Promise<AuthenticationResult>((resolve, reject) => {
47
+ // Set a timeout for the server to close if no response is received in time
48
+ const timeoutId = setTimeout(() => {
49
+ server.close();
50
+ reject(new AuthServerTimeoutError('Authentication server timed out'));
51
+ }, timeout);
52
+
53
+ // Create a temporary HTTP server to listen for the OAuth 2.0 redirect
54
+ const server = createServer(async (req, res) => {
55
+ // Parse the URL and extract the query parameters from the redirect
56
+ const query = URL.parse(req.url ?? '', true).query;
57
+ if (!query.code) {
58
+ // If no authorization code is present, return an error to the browser and reject the promise
59
+ const error = new AuthServerError('No authorization code received');
60
+ res.writeHead(400, { 'Content-Type': 'text/html' });
61
+ res.end(error.message);
62
+ server.close();
63
+ return reject(error);
64
+ }
65
+
66
+ try {
67
+ // Attempt to exchange the authorization code for an access token
68
+ const tokenResponse = await client.acquireTokenByCode({
69
+ code: Array.isArray(query.code) ? query.code[0] : query.code,
70
+ scopes: scopes,
71
+ codeVerifier: options?.codeVerifier,
72
+ redirectUri: `http://localhost:${port}`,
73
+ });
74
+ // On success, notify the user in the browser and resolve the promise
75
+ res.writeHead(200, { 'Content-Type': 'text/html' });
76
+ res.end('Authentication successful! You can close this window.');
77
+ resolve(tokenResponse);
78
+ } catch (err) {
79
+ // If token acquisition fails, return an error to the browser and reject the promise
80
+ const error = new AuthServerError('Authentication failed', { cause: err as Error });
81
+ res.writeHead(500, { 'Content-Type': 'text/html' });
82
+ res.end(`<b>${error.message}</b><br>${(err as Error).message}`);
83
+ reject(error);
84
+ } finally {
85
+ // Always clean up: close the server and clear the timeout
86
+ server.close();
87
+ clearTimeout(timeoutId);
88
+ }
89
+ }).listen(port);
90
+ });
91
+ };
92
+
93
+ export default createAuthServer;
@@ -0,0 +1,35 @@
1
+ import type { IModuleConfigurator, IModulesConfigurator } from '@equinor/fusion-framework-module';
2
+ import { module, type MsalNodeModule } from './module';
3
+
4
+ /**
5
+ * Enables the MSAL Node module by registering its configuration with the provided modules configurator.
6
+ *
7
+ * This function should be called to add the MSAL Node authentication module to a Fusion Framework application.
8
+ * It accepts a configurator instance and a configuration function, which is used to define the module's behavior and settings.
9
+ *
10
+ * @param configurator - The modules configurator instance used to register the MSAL Node module.
11
+ * @param configure - A configuration function for customizing the module (e.g., setting client ID, tenant ID, mode).
12
+ *
13
+ * @see IAuthProvider for the authentication provider interface exposed by the module.
14
+ * @see IAuthConfigurator for the configuration builder interface used in the configure callback.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * enableModule(configurator, (builder) => {
19
+ * builder.setMode('interactive');
20
+ * builder.setClientConfig('your-tenant-id', 'your-client-id');
21
+ * });
22
+ * ```
23
+ */
24
+ export const enableModule = (
25
+ // biome-ignore lint/suspicious/noExplicitAny: @todo -remove when types sorted in provider interface
26
+ configurator: IModulesConfigurator<any, any>,
27
+ configure: IModuleConfigurator<MsalNodeModule>['configure'],
28
+ ) => {
29
+ configurator.addConfig({
30
+ module,
31
+ configure,
32
+ });
33
+ };
34
+
35
+ export default enableModule;
package/src/error.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Error thrown when no accounts are available for an operation that requires one.
3
+ *
4
+ * Typically used when attempting to acquire a token or perform an action that requires
5
+ * a user account, but none are found in the MSAL cache.
6
+ *
7
+ * @param message - Description of the error.
8
+ * @param options - Optional error options, including a cause for error chaining.
9
+ */
10
+ export class NoAccountsError extends Error {
11
+ static readonly Name: string = 'NoAccountsError';
12
+ constructor(message: string, options?: { cause?: unknown }) {
13
+ super(message, options);
14
+ this.name = NoAccountsError.Name;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Error thrown when silent token acquisition fails.
20
+ *
21
+ * This error is used when MSAL cannot acquire a token silently, often due to missing
22
+ * credentials, expired tokens, or lack of a valid session.
23
+ *
24
+ * @param message - Description of the error.
25
+ * @param options - Optional error options, including a cause for error chaining.
26
+ */
27
+ export class SilentTokenAcquisitionError extends Error {
28
+ static readonly Name: string = 'SilentTokenAcquisitionError';
29
+ constructor(message: string, options?: { cause?: unknown }) {
30
+ super(message, options);
31
+ this.name = SilentTokenAcquisitionError.Name;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Error representing a failure or issue in the authentication server flow.
37
+ *
38
+ * Used to signal problems during the OAuth 2.0 authorization code flow, such as
39
+ * missing codes, invalid requests, or token exchange failures. Supports error chaining.
40
+ *
41
+ * @param message - Description of the error.
42
+ * @param options - Optional error options, including a cause for error chaining.
43
+ */
44
+ export class AuthServerError extends Error {
45
+ static readonly Name: string = 'AuthServerError';
46
+ constructor(message: string, options?: { cause?: Error }) {
47
+ super(message, options);
48
+ this.name = AuthServerError.Name;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Error thrown when the authentication server times out waiting for a response.
54
+ *
55
+ * Extends {@link AuthServerError} to provide additional context for timeout scenarios.
56
+ *
57
+ * @param message - Description of the error.
58
+ * @param options - Optional error options, including a cause for error chaining.
59
+ */
60
+ export class AuthServerTimeoutError extends AuthServerError {
61
+ static readonly Name: string = 'AuthServerTimeoutError';
62
+ constructor(message: string, options?: { cause?: Error }) {
63
+ super(message, options);
64
+ this.name = AuthServerTimeoutError.Name;
65
+ }
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { AuthProvider } from './AuthProvider.js';
2
+
3
+ export type { IAuthProvider } from './AuthProvider.interface.js';
4
+
5
+ export type { IAuthConfigurator, AuthConfig } from './AuthConfigurator.interface.js';
6
+
7
+ export { module as authModule, type MsalNodeModule } from './module.js';
8
+
9
+ export { enableModule as enableAuthModule } from './enable-module.js';
package/src/module.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { Module } from '@equinor/fusion-framework-module';
2
+ import { AuthConfigurator } from './AuthConfigurator';
3
+ import { AuthProvider } from './AuthProvider';
4
+ import { AuthTokenProvider } from './AuthTokenProvider';
5
+ import { AuthProviderInteractive } from './AuthProviderInteractive';
6
+ import type { IAuthProvider } from './AuthProvider.interface';
7
+
8
+ /**
9
+ * MSAL Node authentication module for the Fusion Framework.
10
+ *
11
+ * This module provides authentication capabilities for Node.js applications using Microsoft's MSAL library.
12
+ * It supports multiple authentication modes: token-only, interactive (browser-based), and silent (cached credentials).
13
+ *
14
+ * The module exposes a unified provider interface for acquiring tokens and managing authentication state.
15
+ *
16
+ * - In `token_only` mode, a static access token is used (see {@link AuthTokenProvider}).
17
+ * - In `interactive` mode, the user is prompted via a local server and browser (see {@link AuthProviderInteractive}).
18
+ * - In all other cases, silent authentication is attempted using cached credentials (see {@link AuthProvider}).
19
+ *
20
+ * @see AuthProvider
21
+ * @see AuthProviderInteractive
22
+ * @see AuthTokenProvider
23
+ * @see IAuthProvider
24
+ * @see AuthConfigurator
25
+ */
26
+ export type MsalNodeModule = Module<'auth', IAuthProvider, AuthConfigurator>;
27
+
28
+ export const module: MsalNodeModule = {
29
+ name: 'auth',
30
+ configure: () => new AuthConfigurator(),
31
+ initialize: async (args) => {
32
+ const config = await args.config.createConfigAsync(args);
33
+
34
+ switch (config.mode) {
35
+ case 'token_only':
36
+ return new AuthTokenProvider(config.accessToken);
37
+
38
+ case 'interactive': {
39
+ const { client, server } = config;
40
+ if (!server) {
41
+ throw new Error('Server configuration is required for interactive mode');
42
+ }
43
+ return new AuthProviderInteractive(client, { server });
44
+ }
45
+
46
+ default:
47
+ return new AuthProvider(config.client);
48
+ }
49
+ },
50
+ };
51
+
52
+ export default module;
package/src/version.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Generated by genversion.
2
+ export const version = '0.1.0';
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist/esm",
5
+ "rootDir": "src",
6
+ "declarationDir": "./dist/types"
7
+ },
8
+ "references": [
9
+ {
10
+ "path": "../module"
11
+ }
12
+ ],
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "lib"]
15
+ }