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