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