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
+ } ) . then ( x => x === null ? Promise . reject ( 'Token is null' ) : x )
205
+ ) ;
206
+
207
+ return new EntraidCredentialsProvider (
208
+ new TokenManager ( idp , params . tokenManagerConfig ) ,
209
+ idp ,
210
+ { onReAuthenticationError : params . onReAuthenticationError }
211
+ ) ;
212
+ }
213
+
214
+ /**
215
+ * This method is used to create a credentials provider for system-assigned managed identities.
216
+ * @param params
217
+ */
218
+ static createForSystemAssignedManagedIdentity (
219
+ params : CredentialParams
220
+ ) : EntraidCredentialsProvider {
221
+ return this . createManagedIdentityProvider ( params ) ;
222
+ }
223
+
224
+ /**
225
+ * This method is used to create a credentials provider for user-assigned managed identities.
226
+ * It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration.
227
+ * @param params
228
+ */
229
+ static createForUserAssignedManagedIdentity (
230
+ params : CredentialParams
231
+ ) : EntraidCredentialsProvider {
232
+ return this . createManagedIdentityProvider ( params , params . clientId ) ;
233
+ }
234
+
235
+ private static _createForClientCredentials (
236
+ authConfig : NodeAuthOptions ,
237
+ params : CredentialParams
238
+ ) : EntraidCredentialsProvider {
239
+ const config : Configuration = {
240
+ auth : {
241
+ ...authConfig ,
242
+ authority : this . getAuthority ( params . authorityConfig ?? { type : 'default' } )
243
+ } ,
244
+ system : {
245
+ loggerOptions
246
+ }
247
+ } ;
248
+
249
+ const client = new ConfidentialClientApplication ( config ) ;
250
+
251
+ const idp = new MSALIdentityProvider (
252
+ ( ) => client . acquireTokenByClientCredential ( {
253
+ scopes : params . scopes ?? [ FALLBACK_SCOPE ]
254
+ } ) . then ( x => x === null ? Promise . reject ( 'Token is null' ) : x )
255
+ ) ;
256
+
257
+ return new EntraidCredentialsProvider ( new TokenManager ( idp , params . tokenManagerConfig ) , idp ,
258
+ { onReAuthenticationError : params . onReAuthenticationError } ) ;
259
+ }
260
+
261
+ /**
262
+ * This method is used to create a credentials provider for service principals using certificate.
263
+ * @param params
264
+ */
265
+ static createForClientCredentialsWithCertificate (
266
+ params : ClientCredentialsWithCertificateParams
267
+ ) : EntraidCredentialsProvider {
268
+ return this . _createForClientCredentials (
269
+ {
270
+ clientId : params . clientId ,
271
+ clientCertificate : params . certificate
272
+ } ,
273
+ params
274
+ ) ;
275
+ }
276
+
277
+ /**
278
+ * This method is used to create a credentials provider for service principals using client secret.
279
+ * @param params
280
+ */
281
+ static createForClientCredentials (
282
+ params : ClientSecretCredentialsParams
283
+ ) : EntraidCredentialsProvider {
284
+ return this . _createForClientCredentials (
285
+ {
286
+ clientId : params . clientId ,
287
+ clientSecret : params . clientSecret
288
+ } ,
289
+ params
290
+ ) ;
291
+ }
292
+
293
+ /**
294
+ * This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
295
+ * @param params
296
+ */
297
+ static createForAuthorizationCodeWithPKCE (
298
+ params : AuthCodePKCEParams
299
+ ) : {
300
+ getPKCECodes : ( ) => Promise < {
301
+ verifier : string ;
302
+ challenge : string ;
303
+ challengeMethod : string ;
304
+ } > ;
305
+ getAuthCodeUrl : (
306
+ pkceCodes : { challenge : string ; challengeMethod : string }
307
+ ) => Promise < string > ;
308
+ createCredentialsProvider : (
309
+ params : PKCEParams
310
+ ) => EntraidCredentialsProvider ;
311
+ } {
312
+
313
+ const requiredScopes = [ 'user.read' , 'offline_access' ] ;
314
+ const scopes = [ ...new Set ( [ ...( params . scopes || [ ] ) , ...requiredScopes ] ) ] ;
315
+
316
+ const authFlow = AuthCodeFlowHelper . create ( {
317
+ clientId : params . clientId ,
318
+ redirectUri : params . redirectUri ,
319
+ scopes : scopes ,
320
+ authorityConfig : params . authorityConfig
321
+ } ) ;
322
+
323
+ return {
324
+ getPKCECodes : AuthCodeFlowHelper . generatePKCE ,
325
+ getAuthCodeUrl : ( pkceCodes ) => authFlow . getAuthCodeUrl ( pkceCodes ) ,
326
+ createCredentialsProvider : ( pkceParams ) => {
327
+
328
+ // This is used to store the initial credentials account to be used
329
+ // for silent token acquisition after the initial token acquisition.
330
+ let initialCredentialsAccount : AccountInfo | null = null ;
331
+
332
+ const idp = new MSALIdentityProvider (
333
+ async ( ) => {
334
+ if ( ! initialCredentialsAccount ) {
335
+ let authResult = await authFlow . acquireTokenByCode ( pkceParams ) ;
336
+ initialCredentialsAccount = authResult . account ;
337
+ return authResult ;
338
+ } else {
339
+ return authFlow . client . acquireTokenSilent ( {
340
+ account : initialCredentialsAccount ,
341
+ scopes
342
+ } ) ;
343
+ }
344
+
345
+ }
346
+ ) ;
347
+ const tm = new TokenManager ( idp , params . tokenManagerConfig ) ;
348
+ return new EntraidCredentialsProvider ( tm , idp , { onReAuthenticationError : params . onReAuthenticationError } ) ;
349
+ }
350
+ } ;
351
+ }
352
+ }
0 commit comments