@@ -13,26 +13,44 @@ import {
1313 getKnownAuthorities ,
1414 getMSALLogLevel ,
1515 handleMsalError ,
16+ msalToPublic ,
1617 publicToMsal ,
1718} from "../utils" ;
1819
1920import { AuthenticationRequiredError } from "../../errors" ;
20- import { CertificateParts } from "../types" ;
21+ import { AuthenticationRecord , CertificateParts } from "../types" ;
2122import { IdentityClient } from "../../client/identityClient" ;
2223import { MsalNodeOptions } from "./msalNodeCommon" ;
2324import { calculateRegionalAuthority } from "../../regionalAuthority" ;
2425import { getLogLevel } from "@azure/logger" ;
2526import { resolveTenantId } from "../../util/tenantIdUtils" ;
27+ import { DeviceCodePromptCallback } from "../../credentials/deviceCodeCredentialOptions" ;
2628
2729/**
2830 * The logger for all MsalClient instances.
2931 */
3032const msalLogger = credentialLogger ( "MsalClient" ) ;
3133
34+ export interface GetTokenWithSilentAuthOptions extends GetTokenOptions {
35+ /**
36+ * Disables automatic authentication. If set to true, the method will throw an error if the user needs to authenticate.
37+ *
38+ * @remarks
39+ *
40+ * This option will be set to `false` when the user calls `authenticate` directly on a credential that supports it.
41+ */
42+ disableAutomaticAuthentication ?: boolean ;
43+ }
44+
3245/**
3346 * Represents a client for interacting with the Microsoft Authentication Library (MSAL).
3447 */
3548export interface MsalClient {
49+ getTokenByDeviceCode (
50+ arrayScopes : string [ ] ,
51+ userPromptCallback : DeviceCodePromptCallback ,
52+ options ?: GetTokenWithSilentAuthOptions ,
53+ ) : Promise < AccessToken > ;
3654 /**
3755 * Retrieves an access token by using a client certificate.
3856 *
@@ -74,12 +92,21 @@ export interface MsalClient {
7492 clientSecret : string ,
7593 options ?: GetTokenOptions ,
7694 ) : Promise < AccessToken > ;
95+
96+ /**
97+ * Retrieves the last authenticated account. This method expects an authentication record to have been previously loaded.
98+ *
99+ * An authentication record could be loaded by calling the `getToken` method, or by providing an `authenticationRecord` when creating a credential.
100+ */
101+ getActiveAccount ( ) : AuthenticationRecord | undefined ;
77102}
78103
79104/**
80105 * Options for creating an instance of the MsalClient.
81106 */
82- export type MsalClientOptions = Partial < Omit < MsalNodeOptions , "clientId" | "tenantId" > > ;
107+ export type MsalClientOptions = Partial <
108+ Omit < MsalNodeOptions , "clientId" | "tenantId" | "disableAutomaticAuthentication" >
109+ > ;
83110
84111/**
85112 * Generates the configuration for MSAL (Microsoft Authentication Library).
@@ -173,6 +200,40 @@ export function createMsalClient(
173200 pluginConfiguration : msalPlugins . generatePluginConfiguration ( createMsalClientOptions ) ,
174201 } ;
175202
203+ const publicApps : Map < string , msal . PublicClientApplication > = new Map ( ) ;
204+ async function getPublicApp (
205+ options : GetTokenOptions = { } ,
206+ ) : Promise < msal . PublicClientApplication > {
207+ const appKey = options . enableCae ? "CAE" : "default" ;
208+
209+ let publicClientApp = publicApps . get ( appKey ) ;
210+ if ( publicClientApp ) {
211+ msalLogger . getToken . info ( "Existing PublicClientApplication found in cache, returning it." ) ;
212+ return publicClientApp ;
213+ }
214+
215+ // Initialize a new app and cache it
216+ msalLogger . getToken . info (
217+ `Creating new PublicClientApplication with CAE ${ options . enableCae ? "enabled" : "disabled" } .` ,
218+ ) ;
219+
220+ const cachePlugin = options . enableCae
221+ ? state . pluginConfiguration . cache . cachePluginCae
222+ : state . pluginConfiguration . cache . cachePlugin ;
223+
224+ state . msalConfig . auth . clientCapabilities = options . enableCae ? [ "cp1" ] : undefined ;
225+
226+ publicClientApp = new msal . PublicClientApplication ( {
227+ ...state . msalConfig ,
228+ broker : { nativeBrokerPlugin : state . pluginConfiguration . broker . nativeBrokerPlugin } ,
229+ cache : { cachePlugin : await cachePlugin } ,
230+ } ) ;
231+
232+ publicApps . set ( appKey , publicClientApp ) ;
233+
234+ return publicClientApp ;
235+ }
236+
176237 const confidentialApps : Map < string , msal . ConfidentialClientApplication > = new Map ( ) ;
177238 async function getConfidentialApp (
178239 options : GetTokenOptions = { } ,
@@ -272,7 +333,7 @@ To work with multiple accounts for the same Client ID and Tenant ID, please prov
272333 async function withSilentAuthentication (
273334 msalApp : msal . ConfidentialClientApplication | msal . PublicClientApplication ,
274335 scopes : Array < string > ,
275- options : GetTokenOptions ,
336+ options : GetTokenWithSilentAuthOptions ,
276337 onAuthenticationRequired : ( ) => Promise < msal . AuthenticationResult | null > ,
277338 ) : Promise < AccessToken > {
278339 let response : msal . AuthenticationResult | null = null ;
@@ -282,7 +343,7 @@ To work with multiple accounts for the same Client ID and Tenant ID, please prov
282343 if ( e . name !== "AuthenticationRequiredError" ) {
283344 throw e ;
284345 }
285- if ( createMsalClientOptions . disableAutomaticAuthentication ) {
346+ if ( options . disableAutomaticAuthentication ) {
286347 throw new AuthenticationRequiredError ( {
287348 scopes,
288349 getTokenOptions : options ,
@@ -324,14 +385,24 @@ To work with multiple accounts for the same Client ID and Tenant ID, please prov
324385
325386 const msalApp = await getConfidentialApp ( options ) ;
326387
327- return withSilentAuthentication ( msalApp , scopes , options , ( ) =>
328- msalApp . acquireTokenByClientCredential ( {
388+ try {
389+ const response = await msalApp . acquireTokenByClientCredential ( {
329390 scopes,
330391 authority : state . msalConfig . auth . authority ,
331392 azureRegion : calculateRegionalAuthority ( ) ,
332393 claims : options ?. claims ,
333- } ) ,
334- ) ;
394+ } ) ;
395+ ensureValidMsalToken ( scopes , response , options ) ;
396+
397+ msalLogger . getToken . info ( formatSuccess ( scopes ) ) ;
398+
399+ return {
400+ token : response . accessToken ,
401+ expiresOnTimestamp : response . expiresOn . getTime ( ) ,
402+ } ;
403+ } catch ( err : any ) {
404+ throw handleMsalError ( scopes , err , options ) ;
405+ }
335406 }
336407
337408 async function getTokenByClientAssertion (
@@ -345,15 +416,25 @@ To work with multiple accounts for the same Client ID and Tenant ID, please prov
345416
346417 const msalApp = await getConfidentialApp ( options ) ;
347418
348- return withSilentAuthentication ( msalApp , scopes , options , ( ) =>
349- msalApp . acquireTokenByClientCredential ( {
419+ try {
420+ const response = await msalApp . acquireTokenByClientCredential ( {
350421 scopes,
351422 authority : state . msalConfig . auth . authority ,
352423 azureRegion : calculateRegionalAuthority ( ) ,
353424 claims : options ?. claims ,
354425 clientAssertion,
355- } ) ,
356- ) ;
426+ } ) ;
427+ ensureValidMsalToken ( scopes , response , options ) ;
428+
429+ msalLogger . getToken . info ( formatSuccess ( scopes ) ) ;
430+
431+ return {
432+ token : response . accessToken ,
433+ expiresOnTimestamp : response . expiresOn . getTime ( ) ,
434+ } ;
435+ } catch ( err : any ) {
436+ throw handleMsalError ( scopes , err , options ) ;
437+ }
357438 }
358439
359440 async function getTokenByClientCertificate (
@@ -366,20 +447,66 @@ To work with multiple accounts for the same Client ID and Tenant ID, please prov
366447 state . msalConfig . auth . clientCertificate = certificate ;
367448
368449 const msalApp = await getConfidentialApp ( options ) ;
369-
370- return withSilentAuthentication ( msalApp , scopes , options , ( ) =>
371- msalApp . acquireTokenByClientCredential ( {
450+ try {
451+ const response = await msalApp . acquireTokenByClientCredential ( {
372452 scopes,
453+ authority : state . msalConfig . auth . authority ,
373454 azureRegion : calculateRegionalAuthority ( ) ,
455+ claims : options ?. claims ,
456+ } ) ;
457+ ensureValidMsalToken ( scopes , response , options ) ;
458+
459+ msalLogger . getToken . info ( formatSuccess ( scopes ) ) ;
460+
461+ return {
462+ token : response . accessToken ,
463+ expiresOnTimestamp : response . expiresOn . getTime ( ) ,
464+ } ;
465+ } catch ( err : any ) {
466+ throw handleMsalError ( scopes , err , options ) ;
467+ }
468+ }
469+
470+ async function getTokenByDeviceCode (
471+ scopes : string [ ] ,
472+ deviceCodeCallback : DeviceCodePromptCallback ,
473+ options : GetTokenWithSilentAuthOptions = { } ,
474+ ) : Promise < AccessToken > {
475+ msalLogger . getToken . info ( `Attempting to acquire token using device code` ) ;
476+
477+ const msalApp = await getPublicApp ( options ) ;
478+
479+ return withSilentAuthentication ( msalApp , scopes , options , ( ) => {
480+ const requestOptions : msal . DeviceCodeRequest = {
481+ scopes,
482+ cancel : options ?. abortSignal ?. aborted ?? false ,
483+ deviceCodeCallback,
374484 authority : state . msalConfig . auth . authority ,
375485 claims : options ?. claims ,
376- } ) ,
377- ) ;
486+ } ;
487+ const deviceCodeRequest = msalApp . acquireTokenByDeviceCode ( requestOptions ) ;
488+ if ( options . abortSignal ) {
489+ options . abortSignal . addEventListener ( "abort" , ( ) => {
490+ requestOptions . cancel = true ;
491+ } ) ;
492+ }
493+
494+ return deviceCodeRequest ;
495+ } ) ;
496+ }
497+
498+ function getActiveAccount ( ) : AuthenticationRecord | undefined {
499+ if ( ! state . cachedAccount ) {
500+ return undefined ;
501+ }
502+ return msalToPublic ( clientId , state . cachedAccount ) ;
378503 }
379504
380505 return {
506+ getActiveAccount,
381507 getTokenByClientSecret,
382508 getTokenByClientAssertion,
383509 getTokenByClientCertificate,
510+ getTokenByDeviceCode,
384511 } ;
385512}
0 commit comments