1+ import { NetworkError } from '@azure/msal-common' ;
2+ import {
3+ LogLevel ,
4+ ManagedIdentityApplication ,
5+ ManagedIdentityConfiguration ,
6+ AuthenticationResult ,
7+ PublicClientApplication ,
8+ ConfidentialClientApplication , AuthorizationUrlRequest , AuthorizationCodeRequest , CryptoProvider , Configuration , NodeAuthOptions , AccountInfo
9+ } from '@azure/msal-node' ;
10+ import { RetryPolicy , TokenManager , TokenManagerConfig , ReAuthenticationError } from '@redis/client/lib/client/authx' ;
11+ import { EntraidCredentialsProvider } from './entraid-credentials-provider' ;
12+ import { MSALIdentityProvider } from './msal-identity-provider' ;
13+
14+ const FALLBACK_SCOPE = 'https://redis.azure.com/.default' ;
15+
16+ export type AuthorityConfig =
17+ | { type : 'multi-tenant' ; tenantId : string }
18+ | { type : 'custom' ; authorityUrl : string }
19+ | { type : 'default' } ;
20+
21+ export type PKCEParams = {
22+ code : string ;
23+ verifier : string ;
24+ clientInfo ?: string ;
25+ }
26+
27+ export type CredentialParams = {
28+ clientId : string ;
29+ scopes ?: string [ ] ;
30+ authorityConfig ?: AuthorityConfig ;
31+
32+ tokenManagerConfig : TokenManagerConfig
33+ onReAuthenticationError ?: ( error : ReAuthenticationError ) => void ;
34+ }
35+
36+ export type AuthCodePKCEParams = CredentialParams & {
37+ redirectUri : string ;
38+ } ;
39+
40+ export type ClientSecretCredentialsParams = CredentialParams & {
41+ clientSecret : string ;
42+ } ;
43+
44+ export type ClientCredentialsWithCertificateParams = CredentialParams & {
45+ certificate : {
46+ thumbprint : string ;
47+ privateKey : string ;
48+ x5c ?: string ;
49+ } ;
50+ } ;
51+
52+ const loggerOptions = {
53+ loggerCallback ( loglevel : LogLevel , message : string , containsPii : boolean ) {
54+ if ( ! containsPii ) console . log ( message ) ;
55+ } ,
56+ piiLoggingEnabled : false ,
57+ logLevel : LogLevel . Verbose
58+ }
59+
60+ /**
61+ * The most imporant part of the RetryPolicy is the shouldRetry function. This function is used to determine if a request should be retried based
62+ * on the error returned from the identity provider. The defaultRetryPolicy is used to retry on network errors only.
63+ */
64+ export const DEFAULT_RETRY_POLICY : RetryPolicy = {
65+ // currently only retry on network errors
66+ shouldRetry : ( error : unknown ) => error instanceof NetworkError ,
67+ maxAttempts : 10 ,
68+ initialDelayMs : 100 ,
69+ maxDelayMs : 100000 ,
70+ backoffMultiplier : 2 ,
71+ jitterPercentage : 0.1
72+
73+ } ;
74+
75+ export const DEFAULT_TOKEN_MANAGER_CONFIG : TokenManagerConfig = {
76+ retry : DEFAULT_RETRY_POLICY ,
77+ expirationRefreshRatio : 0.7 // Refresh token when 70% of the token has expired
78+ }
79+
80+ /**
81+ * This class is used to help with the Authorization Code Flow with PKCE.
82+ * It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider.
83+ */
84+ export class AuthCodeFlowHelper {
85+ private constructor (
86+ readonly client : PublicClientApplication ,
87+ readonly scopes : string [ ] ,
88+ readonly redirectUri : string
89+ ) { }
90+
91+ async getAuthCodeUrl ( pkceCodes : {
92+ challenge : string ;
93+ challengeMethod : string ;
94+ } ) : Promise < string > {
95+ const authCodeUrlParameters : AuthorizationUrlRequest = {
96+ scopes : this . scopes ,
97+ redirectUri : this . redirectUri ,
98+ codeChallenge : pkceCodes . challenge ,
99+ codeChallengeMethod : pkceCodes . challengeMethod
100+ } ;
101+
102+ return this . client . getAuthCodeUrl ( authCodeUrlParameters ) ;
103+ }
104+
105+ async acquireTokenByCode ( params : PKCEParams ) : Promise < AuthenticationResult > {
106+ const tokenRequest : AuthorizationCodeRequest = {
107+ code : params . code ,
108+ scopes : this . scopes ,
109+ redirectUri : this . redirectUri ,
110+ codeVerifier : params . verifier ,
111+ clientInfo : params . clientInfo
112+ } ;
113+
114+ return this . client . acquireTokenByCode ( tokenRequest ) ;
115+ }
116+
117+ static async generatePKCE ( ) : Promise < {
118+ verifier : string ;
119+ challenge : string ;
120+ challengeMethod : string ;
121+ } > {
122+ const cryptoProvider = new CryptoProvider ( ) ;
123+ const { verifier, challenge } = await cryptoProvider . generatePkceCodes ( ) ;
124+ return {
125+ verifier,
126+ challenge,
127+ challengeMethod : 'S256'
128+ } ;
129+ }
130+
131+ static create ( params : {
132+ clientId : string ;
133+ redirectUri : string ;
134+ scopes ?: string [ ] ;
135+ authorityConfig ?: AuthorityConfig ;
136+ } ) : AuthCodeFlowHelper {
137+ const config = {
138+ auth : {
139+ clientId : params . clientId ,
140+ authority : EntraIdCredentialsProviderFactory . getAuthority ( params . authorityConfig ?? { type : 'default' } )
141+ } ,
142+ system : {
143+ loggerOptions
144+ }
145+ } ;
146+
147+ return new AuthCodeFlowHelper (
148+ new PublicClientApplication ( config ) ,
149+ params . scopes ?? [ 'user.read' ] ,
150+ params . redirectUri
151+ ) ;
152+ }
153+ }
154+
155+ /**
156+ * This class is used to create credentials providers for different types of authentication flows.
157+ */
158+ export class EntraIdCredentialsProviderFactory {
159+
160+ static getAuthority ( config : AuthorityConfig ) : string {
161+ switch ( config . type ) {
162+ case 'multi-tenant' :
163+ return `https://login.microsoftonline.com/${ config . tenantId } ` ;
164+ case 'custom' :
165+ return config . authorityUrl ;
166+ case 'default' :
167+ return 'https://login.microsoftonline.com/common' ;
168+ default :
169+ throw new Error ( 'Invalid authority configuration' ) ;
170+ }
171+ }
172+
173+ /**
174+ * This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities.
175+ * for user-assigned managed identities, the developer needs to pass either the client ID, full resource identifier,
176+ * or the object ID of the managed identity when creating ManagedIdentityApplication.
177+ *
178+ * @param params
179+ * @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID,
180+ * full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication.
181+ *
182+ * @private
183+ */
184+ public static createManagedIdentityProvider (
185+ params : CredentialParams , userAssignedClientId ?: string
186+ ) : EntraidCredentialsProvider {
187+ const config : ManagedIdentityConfiguration = {
188+ // For user-assigned identity, include the client ID
189+ ...( userAssignedClientId && {
190+ managedIdentityIdParams : {
191+ userAssignedClientId
192+ }
193+ } ) ,
194+ system : {
195+ loggerOptions
196+ }
197+ } ;
198+
199+ const client = new ManagedIdentityApplication ( config ) ;
200+
201+ const idp = new MSALIdentityProvider (
202+ ( ) => client . acquireToken ( {
203+ resource : params . scopes ?. [ 0 ] ?? FALLBACK_SCOPE ,
204+ forceRefresh : true
205+ } ) . then ( x => x === null ? Promise . reject ( 'Token is null' ) : x )
206+ ) ;
207+
208+ return new EntraidCredentialsProvider (
209+ new TokenManager ( idp , params . tokenManagerConfig ) ,
210+ idp ,
211+ { onReAuthenticationError : params . onReAuthenticationError }
212+ ) ;
213+ }
214+
215+ /**
216+ * This method is used to create a credentials provider for system-assigned managed identities.
217+ * @param params
218+ */
219+ static createForSystemAssignedManagedIdentity (
220+ params : CredentialParams
221+ ) : EntraidCredentialsProvider {
222+ return this . createManagedIdentityProvider ( params ) ;
223+ }
224+
225+ /**
226+ * This method is used to create a credentials provider for user-assigned managed identities.
227+ * It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration.
228+ * @param params
229+ */
230+ static createForUserAssignedManagedIdentity (
231+ params : CredentialParams
232+ ) : EntraidCredentialsProvider {
233+ return this . createManagedIdentityProvider ( params , params . clientId ) ;
234+ }
235+
236+ private static _createForClientCredentials (
237+ authConfig : NodeAuthOptions ,
238+ params : CredentialParams
239+ ) : EntraidCredentialsProvider {
240+ const config : Configuration = {
241+ auth : {
242+ ...authConfig ,
243+ authority : this . getAuthority ( params . authorityConfig ?? { type : 'default' } )
244+ } ,
245+ system : {
246+ loggerOptions
247+ }
248+ } ;
249+
250+ const client = new ConfidentialClientApplication ( config ) ;
251+
252+ const idp = new MSALIdentityProvider (
253+ ( ) => client . acquireTokenByClientCredential ( {
254+ skipCache : true ,
255+ scopes : params . scopes ?? [ FALLBACK_SCOPE ]
256+ } ) . then ( x => x === null ? Promise . reject ( 'Token is null' ) : x )
257+ ) ;
258+
259+ return new EntraidCredentialsProvider ( new TokenManager ( idp , params . tokenManagerConfig ) , idp ,
260+ { onReAuthenticationError : params . onReAuthenticationError } ) ;
261+ }
262+
263+ /**
264+ * This method is used to create a credentials provider for service principals using certificate.
265+ * @param params
266+ */
267+ static createForClientCredentialsWithCertificate (
268+ params : ClientCredentialsWithCertificateParams
269+ ) : EntraidCredentialsProvider {
270+ return this . _createForClientCredentials (
271+ {
272+ clientId : params . clientId ,
273+ clientCertificate : params . certificate
274+ } ,
275+ params
276+ ) ;
277+ }
278+
279+ /**
280+ * This method is used to create a credentials provider for service principals using client secret.
281+ * @param params
282+ */
283+ static createForClientCredentials (
284+ params : ClientSecretCredentialsParams
285+ ) : EntraidCredentialsProvider {
286+ return this . _createForClientCredentials (
287+ {
288+ clientId : params . clientId ,
289+ clientSecret : params . clientSecret
290+ } ,
291+ params
292+ ) ;
293+ }
294+
295+ /**
296+ * This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
297+ * @param params
298+ */
299+ static createForAuthorizationCodeWithPKCE (
300+ params : AuthCodePKCEParams
301+ ) : {
302+ getPKCECodes : ( ) => Promise < {
303+ verifier : string ;
304+ challenge : string ;
305+ challengeMethod : string ;
306+ } > ;
307+ getAuthCodeUrl : (
308+ pkceCodes : { challenge : string ; challengeMethod : string }
309+ ) => Promise < string > ;
310+ createCredentialsProvider : (
311+ params : PKCEParams
312+ ) => EntraidCredentialsProvider ;
313+ } {
314+
315+ const requiredScopes = [ 'user.read' , 'offline_access' ] ;
316+ const scopes = [ ...new Set ( [ ...( params . scopes || [ ] ) , ...requiredScopes ] ) ] ;
317+
318+ const authFlow = AuthCodeFlowHelper . create ( {
319+ clientId : params . clientId ,
320+ redirectUri : params . redirectUri ,
321+ scopes : scopes ,
322+ authorityConfig : params . authorityConfig
323+ } ) ;
324+
325+ return {
326+ getPKCECodes : AuthCodeFlowHelper . generatePKCE ,
327+ getAuthCodeUrl : ( pkceCodes ) => authFlow . getAuthCodeUrl ( pkceCodes ) ,
328+ createCredentialsProvider : ( pkceParams ) => {
329+
330+ // This is used to store the initial credentials account to be used
331+ // for silent token acquisition after the initial token acquisition.
332+ let initialCredentialsAccount : AccountInfo | null = null ;
333+
334+ const idp = new MSALIdentityProvider (
335+ async ( ) => {
336+ if ( ! initialCredentialsAccount ) {
337+ let authResult = await authFlow . acquireTokenByCode ( pkceParams ) ;
338+ initialCredentialsAccount = authResult . account ;
339+ return authResult ;
340+ } else {
341+ return authFlow . client . acquireTokenSilent ( {
342+ forceRefresh : true ,
343+ account : initialCredentialsAccount ,
344+ scopes
345+ } ) ;
346+ }
347+
348+ }
349+ ) ;
350+ const tm = new TokenManager ( idp , params . tokenManagerConfig ) ;
351+ return new EntraidCredentialsProvider ( tm , idp , { onReAuthenticationError : params . onReAuthenticationError } ) ;
352+ }
353+ } ;
354+ }
355+ }
0 commit comments