@equinor/fusion-framework-module-msal 7.1.0 → 7.2.1

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.
@@ -113,6 +113,21 @@ export declare class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
113
113
  * ```
114
114
  */
115
115
  setRequiresAuth(requiresAuth: boolean): this;
116
+ /**
117
+ * Sets a default login hint for authentication flows.
118
+ *
119
+ * The login hint is used to pre-fill the username during authentication and
120
+ * enables silent SSO when no account is available.
121
+ *
122
+ * @param loginHint - The preferred username/email to use as login hint
123
+ * @returns The configurator instance for method chaining
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * configurator.setLoginHint('user@company.com');
128
+ * ```
129
+ */
130
+ setLoginHint(loginHint?: string): this;
116
131
  /**
117
132
  * @deprecated - since version 5.1.0, use setClient instead
118
133
  */
@@ -3,7 +3,7 @@ import { TelemetryLevel } from '@equinor/fusion-framework-module-telemetry';
3
3
  import { BaseModuleProvider } from '@equinor/fusion-framework-module/provider';
4
4
  import type { MsalConfig } from './MsalConfigurator';
5
5
  import type { AcquireTokenOptionsLegacy, IMsalProvider } from './MsalProvider.interface';
6
- import type { AcquireTokenResult, IMsalClient, LoginOptions, LoginResult, LogoutOptions } from './MsalClient.interface';
6
+ import type { AcquireTokenOptions, AcquireTokenResult, IMsalClient, LoginOptions, LoginResult, LogoutOptions } from './MsalClient.interface';
7
7
  import type { AccountInfo, AuthenticationResult } from './types';
8
8
  import type { MsalModuleVersion } from './static';
9
9
  export type { IMsalProvider };
@@ -33,6 +33,12 @@ export type { IMsalProvider };
33
33
  */
34
34
  export declare class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsalProvider {
35
35
  #private;
36
+ /**
37
+ * Default OAuth scopes used when the caller provides no scopes.
38
+ *
39
+ * Resolves to the app's Entra ID configured permissions via the `/.default` scope.
40
+ */
41
+ get defaultScopes(): string[];
36
42
  /**
37
43
  * The MSAL module version enum value indicating the API compatibility level.
38
44
  *
@@ -106,7 +112,7 @@ export declare class MsalProvider extends BaseModuleProvider<MsalConfig> impleme
106
112
  * }
107
113
  * ```
108
114
  */
109
- acquireAccessToken(options: AcquireTokenOptionsLegacy): Promise<string | undefined>;
115
+ acquireAccessToken(options?: AcquireTokenOptions | AcquireTokenOptionsLegacy): Promise<string | undefined>;
110
116
  /**
111
117
  * Acquire full authentication result for the specified scopes
112
118
  *
@@ -136,7 +142,7 @@ export declare class MsalProvider extends BaseModuleProvider<MsalConfig> impleme
136
142
  * });
137
143
  * ```
138
144
  */
139
- acquireToken(options: AcquireTokenOptionsLegacy): Promise<AcquireTokenResult>;
145
+ acquireToken(options?: AcquireTokenOptions | AcquireTokenOptionsLegacy): Promise<AcquireTokenResult>;
140
146
  /**
141
147
  * Authenticates a user using Microsoft Authentication Library.
142
148
  *
@@ -83,7 +83,7 @@ export interface IMsalProvider extends IProxyProvider {
83
83
  * });
84
84
  * ```
85
85
  */
86
- acquireAccessToken(options: AcquireTokenOptionsLegacy): Promise<string | undefined>;
86
+ acquireAccessToken(options?: AcquireTokenOptions | AcquireTokenOptionsLegacy): Promise<string | undefined>;
87
87
  /**
88
88
  * Acquires a full authentication result including token and account information.
89
89
  *
@@ -101,7 +101,7 @@ export interface IMsalProvider extends IProxyProvider {
101
101
  * });
102
102
  * ```
103
103
  */
104
- acquireToken(options: AcquireTokenOptionsLegacy): Promise<AcquireTokenResult>;
104
+ acquireToken(options?: AcquireTokenOptions | AcquireTokenOptionsLegacy): Promise<AcquireTokenResult>;
105
105
  /**
106
106
  * Authenticates a user interactively with Microsoft Identity Platform.
107
107
  *
@@ -39,6 +39,11 @@ export type AuthConfigFn = (builder: {
39
39
  * @param requiresAuth - If true, app will attempt automatic login on initialization
40
40
  */
41
41
  setRequiresAuth: (requiresAuth: boolean) => void;
42
+ /**
43
+ * Set a default login hint used for silent SSO and pre-filled usernames
44
+ * @param loginHint - Preferred username/email to use for login hint
45
+ */
46
+ setLoginHint: (loginHint: string) => void;
42
47
  }) => void;
43
48
  /**
44
49
  * Enables MSAL authentication module in the framework.
@@ -1 +1 @@
1
- export declare const version = "7.1.0";
1
+ export declare const version = "7.2.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-module-msal",
3
- "version": "7.1.0",
3
+ "version": "7.2.1",
4
4
  "description": "Microsoft Authentication Library (MSAL) integration module for Fusion Framework",
5
5
  "main": "dist/esm/index.js",
6
6
  "types": "dist/types/index.d.ts",
@@ -58,8 +58,8 @@
58
58
  "semver": "^7.5.4",
59
59
  "typescript": "^5.8.2",
60
60
  "zod": "^4.1.8",
61
- "@equinor/fusion-framework-module": "^5.0.5",
62
- "@equinor/fusion-framework-module-telemetry": "^4.6.3"
61
+ "@equinor/fusion-framework-module-telemetry": "^4.6.3",
62
+ "@equinor/fusion-framework-module": "^5.0.5"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@equinor/fusion-framework-module-telemetry": {
@@ -43,6 +43,7 @@ const MsalConfigSchema = z.object({
43
43
  provider: z.custom<IMsalProvider>().optional(),
44
44
  requiresAuth: z.boolean().optional(),
45
45
  redirectUri: z.string().optional(),
46
+ loginHint: z.string().optional(),
46
47
  authCode: z.string().optional(),
47
48
  version: z.string().transform((x: string) => String(semver.coerce(x))),
48
49
  telemetry: TelemetryConfigSchema,
@@ -180,6 +181,25 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
180
181
  return this;
181
182
  }
182
183
 
184
+ /**
185
+ * Sets a default login hint for authentication flows.
186
+ *
187
+ * The login hint is used to pre-fill the username during authentication and
188
+ * enables silent SSO when no account is available.
189
+ *
190
+ * @param loginHint - The preferred username/email to use as login hint
191
+ * @returns The configurator instance for method chaining
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * configurator.setLoginHint('user@company.com');
196
+ * ```
197
+ */
198
+ setLoginHint(loginHint?: string): this {
199
+ this._set('loginHint', async () => loginHint);
200
+ return this;
201
+ }
202
+
183
203
  /**
184
204
  * @deprecated - since version 5.1.0, use setClient instead
185
205
  */
@@ -95,7 +95,9 @@ export interface IMsalProvider extends IProxyProvider {
95
95
  * });
96
96
  * ```
97
97
  */
98
- acquireAccessToken(options: AcquireTokenOptionsLegacy): Promise<string | undefined>;
98
+ acquireAccessToken(
99
+ options?: AcquireTokenOptions | AcquireTokenOptionsLegacy,
100
+ ): Promise<string | undefined>;
99
101
 
100
102
  /**
101
103
  * Acquires a full authentication result including token and account information.
@@ -114,7 +116,9 @@ export interface IMsalProvider extends IProxyProvider {
114
116
  * });
115
117
  * ```
116
118
  */
117
- acquireToken(options: AcquireTokenOptionsLegacy): Promise<AcquireTokenResult>;
119
+ acquireToken(
120
+ options?: AcquireTokenOptions | AcquireTokenOptionsLegacy,
121
+ ): Promise<AcquireTokenResult>;
118
122
 
119
123
  /**
120
124
  * Authenticates a user interactively with Microsoft Identity Platform.
@@ -61,6 +61,17 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
61
61
  };
62
62
  #requiresAuth?: boolean;
63
63
  #authCode?: string;
64
+ #loginHint?: string;
65
+
66
+ /**
67
+ * Default OAuth scopes used when the caller provides no scopes.
68
+ *
69
+ * Resolves to the app's Entra ID configured permissions via the `/.default` scope.
70
+ */
71
+ get defaultScopes(): string[] {
72
+ const clientId = this.#client.clientId;
73
+ return clientId ? [`${clientId}/.default`] : [];
74
+ }
64
75
 
65
76
  /**
66
77
  * The MSAL module version enum value indicating the API compatibility level.
@@ -127,6 +138,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
127
138
  });
128
139
  this.#requiresAuth = config.requiresAuth;
129
140
  this.#telemetry = config.telemetry;
141
+ this.#loginHint = config.loginHint;
130
142
 
131
143
  // Extract auth code from config if present
132
144
  // This will be used during initialize to exchange for tokens
@@ -234,9 +246,9 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
234
246
  } else if (!this.#client.hasValidClaims) {
235
247
  // Priority 2: No valid session found - attempt automatic login
236
248
  // This handles first-time app load when no authentication state exists
237
- // Note: Using empty scopes here as we don't know what scopes the app needs yet
249
+ // Note: Using default scopes here as we don't know what scopes the app needs yet
238
250
  // App should call acquireToken with actual scopes after initialization
239
- const loginResult = await this.login({ request: { scopes: [] } });
251
+ const loginResult = await this.login({ request: { scopes: this.defaultScopes } });
240
252
  if (loginResult?.account) {
241
253
  // Automatic login successful - set as active account
242
254
  this.#client.setActiveAccount(loginResult.account);
@@ -269,7 +281,9 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
269
281
  * }
270
282
  * ```
271
283
  */
272
- async acquireAccessToken(options: AcquireTokenOptionsLegacy): Promise<string | undefined> {
284
+ async acquireAccessToken(
285
+ options?: AcquireTokenOptions | AcquireTokenOptionsLegacy,
286
+ ): Promise<string | undefined> {
273
287
  const { accessToken } = (await this.acquireToken(options)) ?? {};
274
288
  return accessToken;
275
289
  }
@@ -303,47 +317,68 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
303
317
  * });
304
318
  * ```
305
319
  */
306
- async acquireToken(options: AcquireTokenOptionsLegacy): Promise<AcquireTokenResult> {
307
- const {
308
- behavior = 'redirect',
309
- silent = true,
310
- request = {} as AcquireTokenOptions['request'],
311
- } = options;
320
+ async acquireToken(
321
+ options?: AcquireTokenOptions | AcquireTokenOptionsLegacy,
322
+ ): Promise<AcquireTokenResult> {
323
+ // Determine behavior and silent options, with defaults (redirect and true respectively)
324
+ const behavior = options?.behavior ?? 'redirect';
325
+
326
+ // Silent mode defaults to true, meaning the provider will attempt silent token acquisition first
327
+ const silent = options?.silent ?? true;
312
328
 
313
- const account = request.account ?? this.account ?? undefined;
314
- // Extract scopes from either new format (request.scopes) or legacy format (scopes)
315
- const scopes = options.request?.scopes ?? options?.scopes ?? [];
329
+ const defaultScopes = this.defaultScopes;
316
330
 
331
+ const inputRequest = options?.request;
332
+
333
+ // Determine the account to use for token acquisition, prioritizing request-specific account, then active account
334
+ const account = inputRequest?.account ?? this.account ?? undefined;
335
+
336
+ // Extract caller-provided scopes from either new format (request.scopes) or legacy format (scopes)
337
+ const candidateScopes =
338
+ inputRequest?.scopes ?? (options as AcquireTokenOptionsLegacy)?.scopes ?? [];
339
+
340
+ const scopes =
341
+ candidateScopes.length > 0 ? candidateScopes : defaultScopes.length > 0 ? defaultScopes : [];
342
+
343
+ // Prepare telemetry properties for this token acquisition attempt
317
344
  const telemetryProperties = { behavior, silent, scopes };
318
345
 
319
346
  // Track usage of deprecated legacy scopes format for migration monitoring
320
- if (options.scopes) {
347
+ if ((options as AcquireTokenOptionsLegacy)?.scopes) {
321
348
  this._trackEvent('acquireToken.legacy-scopes-provided', TelemetryLevel.Warning, {
322
349
  properties: telemetryProperties,
323
350
  });
324
351
  }
325
352
 
326
353
  // Handle empty scopes - currently monitoring for telemetry, will throw in future
327
- if (scopes.length === 0) {
328
- const exception = new Error('Empty scopes provided, not allowed');
329
- this._trackException('acquireToken.missing-scope', TelemetryLevel.Warning, {
330
- exception,
331
- properties: telemetryProperties,
332
- });
333
- // TODO: throw exception when sufficient metrics are collected
334
- // This allows us to monitor how often empty scopes are provided before enforcing validation
354
+ if (candidateScopes.length === 0) {
355
+ if (defaultScopes.length > 0) {
356
+ this._trackEvent('acquireToken.missing-scope.defaulted', TelemetryLevel.Warning, {
357
+ properties: { ...telemetryProperties, defaultScopes },
358
+ });
359
+ } else {
360
+ const exception = new Error(
361
+ 'Empty scopes provided and clientId is missing for default scope',
362
+ );
363
+ this._trackException('acquireToken.missing-scope', TelemetryLevel.Warning, {
364
+ exception,
365
+ properties: telemetryProperties,
366
+ });
367
+ // TODO: throw exception when sufficient metrics are collected
368
+ // This allows us to monitor how often empty scopes are provided before enforcing validation
369
+ }
335
370
  }
336
371
 
337
372
  try {
338
373
  const measurement = this._trackMeasurement('acquireToken', TelemetryLevel.Information, {
339
374
  properties: telemetryProperties,
340
375
  });
341
- // Merge account, original request options, and resolved scopes
342
- // Account ensures context awareness, request preserves custom options, scopes uses resolved value
376
+ // Merge account, original request options, and resolved scopes.
377
+ // Account ensures context awareness, request preserves custom options, scopes uses resolved value.
343
378
  const result = await this.#client.acquireToken({
344
379
  behavior,
345
380
  silent,
346
- request: { ...options.request, account, scopes },
381
+ request: { ...inputRequest, account, scopes },
347
382
  });
348
383
  measurement?.measure();
349
384
  return result;
@@ -406,6 +441,16 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
406
441
  async login(options: LoginOptions): Promise<LoginResult> {
407
442
  const { behavior = 'redirect', silent = true, request } = options;
408
443
 
444
+ request.loginHint ??=
445
+ this.#loginHint ?? this.account?.username ?? this.account?.loginHint ?? undefined;
446
+
447
+ const defaultScopes = this.defaultScopes;
448
+
449
+ // Fallback to app default scope when possible; empty scopes tracked for monitoring
450
+ if (!request.scopes || request.scopes.length === 0) {
451
+ request.scopes = defaultScopes.length > 0 ? defaultScopes : [];
452
+ }
453
+
409
454
  // Determine if silent login is possible based on available account/hint information
410
455
  // Silent login requires either an account object or a loginHint to work
411
456
  const canLoginSilently = silent && (request.account || request.loginHint);
@@ -416,10 +461,9 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
416
461
  // This allows silent login to work automatically with existing authentication state
417
462
  request.account ??= this.account ?? undefined;
418
463
 
419
- // Default to empty scopes if none provided
420
- // Empty scopes are tracked for monitoring but allowed for compatibility
421
- if (!request.scopes) {
422
- request.scopes = [];
464
+ // If scopes are still empty here, we couldn't derive a default scope (e.g. missing clientId).
465
+ // Track for monitoring; behavior will be enforced once we have sufficient metrics.
466
+ if (request.scopes.length === 0) {
423
467
  this._trackEvent('login.missing-scope', TelemetryLevel.Warning, {
424
468
  properties: telemetryProperties,
425
469
  });
package/src/module.ts CHANGED
@@ -91,6 +91,11 @@ export type AuthConfigFn = (builder: {
91
91
  * @param requiresAuth - If true, app will attempt automatic login on initialization
92
92
  */
93
93
  setRequiresAuth: (requiresAuth: boolean) => void;
94
+ /**
95
+ * Set a default login hint used for silent SSO and pre-filled usernames
96
+ * @param loginHint - Preferred username/email to use for login hint
97
+ */
98
+ setLoginHint: (loginHint: string) => void;
94
99
  }) => void;
95
100
 
96
101
  /**
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '7.1.0';
2
+ export const version = '7.2.1';