1+ import { BasicAuth } from '@redis/authx' ;
2+ import { createClient } from '@redis/client' ;
3+ import { EntraIdCredentialsProviderFactory } from '../lib/entra-id-credentials-provider-factory' ;
4+ import { strict as assert } from 'node:assert' ;
5+ import { spy , SinonSpy } from 'sinon' ;
6+ import { randomUUID } from 'crypto' ;
7+ import { loadFromJson , RedisEndpointsConfig } from '@redis/test-utils/lib/cae-client-testing' ;
8+ import { EntraidCredentialsProvider } from '../lib/entraid-credentials-provider' ;
9+
10+ describe ( 'EntraID Integration Tests' , ( ) => {
11+
12+ it ( 'client configured with client secret should be able to authenticate/re-authenticate' , async ( ) => {
13+ const config = readConfigFromEnv ( ) ;
14+ await runAuthenticationTest ( ( ) =>
15+ EntraIdCredentialsProviderFactory . createForClientCredentials ( {
16+ clientId : config . clientId ,
17+ clientSecret : config . clientSecret ,
18+ authorityConfig : { type : 'multi-tenant' , tenantId : config . tenantId } ,
19+ tokenManagerConfig : {
20+ expirationRefreshRatio : 0.0001
21+ }
22+ } )
23+ ) ;
24+ } ) ;
25+
26+ it ( 'client configured with client certificate should be able to authenticate/re-authenticate' , async ( ) => {
27+ const config = readConfigFromEnv ( ) ;
28+ await runAuthenticationTest ( ( ) =>
29+ EntraIdCredentialsProviderFactory . createForClientCredentialsWithCertificate ( {
30+ clientId : config . clientId ,
31+ certificate : {
32+ privateKey : config . privateKey ,
33+ thumbprint : config . cert
34+ } ,
35+ authorityConfig : { type : 'multi-tenant' , tenantId : config . tenantId } ,
36+ tokenManagerConfig : {
37+ expirationRefreshRatio : 0.0001
38+ }
39+ } )
40+ ) ;
41+ } ) ;
42+
43+ it ( 'client with system managed identity should be able to authenticate/re-authenticate' , async ( ) => {
44+ const config = readConfigFromEnv ( ) ;
45+ await runAuthenticationTest ( ( ) =>
46+ EntraIdCredentialsProviderFactory . createForSystemAssignedManagedIdentity ( {
47+ clientId : config . clientId ,
48+ authorityConfig : { type : 'multi-tenant' , tenantId : config . tenantId } ,
49+ tokenManagerConfig : {
50+ expirationRefreshRatio : 0.0001
51+ }
52+ } )
53+ ) ;
54+ } ) ;
55+
56+ it ( 'client with user managed system identity should be able to authenticate/re-authenticate' , async ( ) => {
57+ const config = readConfigFromEnv ( ) ;
58+ await runAuthenticationTest ( ( ) =>
59+ EntraIdCredentialsProviderFactory . createForUserAssignedManagedIdentity ( {
60+ clientId : config . clientId ,
61+ userAssignedClientId : config . userAssignedManagedId ,
62+ authorityConfig : { type : 'multi-tenant' , tenantId : config . tenantId } ,
63+ tokenManagerConfig : {
64+ expirationRefreshRatio : 0.0001
65+ }
66+ } )
67+ ) ;
68+ } ) ;
69+
70+ interface TestConfig {
71+ clientId : string ;
72+ clientSecret : string ;
73+ authority : string ;
74+ tenantId : string ;
75+ redisScopes : string ;
76+ cert : string ;
77+ privateKey : string ;
78+ userAssignedManagedId : string ;
79+ endpoints : RedisEndpointsConfig ;
80+ }
81+
82+ const readConfigFromEnv = ( ) : TestConfig => {
83+ const requiredEnvVars = {
84+ AZURE_CLIENT_ID : process . env . AZURE_CLIENT_ID ,
85+ AZURE_CLIENT_SECRET : process . env . AZURE_CLIENT_SECRET ,
86+ AZURE_AUTHORITY : process . env . AZURE_AUTHORITY ,
87+ AZURE_TENANT_ID : process . env . AZURE_TENANT_ID ,
88+ AZURE_REDIS_SCOPES : process . env . AZURE_REDIS_SCOPES ,
89+ AZURE_CERT : process . env . AZURE_CERT ,
90+ AZURE_PRIVATE_KEY : process . env . AZURE_PRIVATE_KEY ,
91+ AZURE_USER_ASSIGNED_MANAGED_ID : process . env . AZURE_USER_ASSIGNED_MANAGED_ID ,
92+ REDIS_ENDPOINTS_CONFIG_PATH : process . env . REDIS_ENDPOINTS_CONFIG_PATH
93+ } ;
94+
95+ Object . entries ( requiredEnvVars ) . forEach ( ( [ key , value ] ) => {
96+ if ( value == undefined ) {
97+ throw new Error ( `${ key } environment variable must be set` ) ;
98+ }
99+ } ) ;
100+
101+ return {
102+ endpoints : loadFromJson ( requiredEnvVars . REDIS_ENDPOINTS_CONFIG_PATH ) ,
103+ clientId : requiredEnvVars . AZURE_CLIENT_ID ,
104+ clientSecret : requiredEnvVars . AZURE_CLIENT_SECRET ,
105+ authority : requiredEnvVars . AZURE_AUTHORITY ,
106+ tenantId : requiredEnvVars . AZURE_TENANT_ID ,
107+ redisScopes : requiredEnvVars . AZURE_REDIS_SCOPES ,
108+ cert : requiredEnvVars . AZURE_CERT ,
109+ privateKey : requiredEnvVars . AZURE_PRIVATE_KEY ,
110+ userAssignedManagedId : requiredEnvVars . AZURE_USER_ASSIGNED_MANAGED_ID
111+ } ;
112+ } ;
113+
114+ interface TokenDetail {
115+ token : string ;
116+ exp : number ;
117+ iat : number ;
118+ lifetime : number ;
119+ uti : string ;
120+ }
121+
122+ const setupTestClient = ( credentialsProvider : EntraidCredentialsProvider ) => {
123+ const config = readConfigFromEnv ( ) ;
124+ const client = createClient ( {
125+ url : config . endpoints [ 'standalone-entraid-acl' ] . endpoints [ 0 ] ,
126+ credentialsProvider
127+ } ) ;
128+
129+ const clientInstance = ( client as any ) . _self ;
130+ const reAuthSpy : SinonSpy = spy ( clientInstance , 'reAuthenticate' ) ;
131+
132+ return { client, reAuthSpy } ;
133+ } ;
134+
135+ const runClientOperations = async ( client : any ) => {
136+ const startTime = Date . now ( ) ;
137+ while ( Date . now ( ) - startTime < 1000 ) {
138+ const key = randomUUID ( ) ;
139+ await client . set ( key , 'value' ) ;
140+ const value = await client . get ( key ) ;
141+ assert . equal ( value , 'value' ) ;
142+ await client . del ( key ) ;
143+ }
144+ } ;
145+
146+ const validateTokens = ( reAuthSpy : SinonSpy ) => {
147+ assert ( reAuthSpy . callCount >= 1 ,
148+ `reAuthenticate should have been called at least once, but was called ${ reAuthSpy . callCount } times` ) ;
149+
150+ const tokenDetails : TokenDetail [ ] = reAuthSpy . getCalls ( ) . map ( call => {
151+ const creds = call . args [ 0 ] as BasicAuth ;
152+ const tokenPayload = JSON . parse (
153+ Buffer . from ( creds . password . split ( '.' ) [ 1 ] , 'base64' ) . toString ( )
154+ ) ;
155+
156+ return {
157+ token : creds . password ,
158+ exp : tokenPayload . exp ,
159+ iat : tokenPayload . iat ,
160+ lifetime : tokenPayload . exp - tokenPayload . iat ,
161+ uti : tokenPayload . uti
162+ } ;
163+ } ) ;
164+
165+ // Verify unique tokens
166+ const uniqueTokens = new Set ( tokenDetails . map ( detail => detail . token ) ) ;
167+ assert . equal (
168+ uniqueTokens . size ,
169+ reAuthSpy . callCount ,
170+ `Expected ${ reAuthSpy . callCount } different tokens, but got ${ uniqueTokens . size } unique tokens`
171+ ) ;
172+
173+ // Verify all tokens are not cached (i.e. have the same lifetime)
174+ const uniqueLifetimes = new Set ( tokenDetails . map ( detail => detail . lifetime ) ) ;
175+ assert . equal (
176+ uniqueLifetimes . size ,
177+ 1 ,
178+ `Expected all tokens to have the same lifetime, but found ${ uniqueLifetimes . size } different lifetimes: ${ [ uniqueLifetimes ] . join ( ', ' ) } seconds`
179+ ) ;
180+
181+ // Verify that all tokens have different uti (unique token identifier)
182+ const uniqueUti = new Set ( tokenDetails . map ( detail => detail . uti ) ) ;
183+ assert . equal (
184+ uniqueUti . size ,
185+ reAuthSpy . callCount ,
186+ `Expected all tokens to have different uti, but found ${ uniqueUti . size } different uti in: ${ [ uniqueUti ] . join ( ', ' ) } `
187+ ) ;
188+ } ;
189+
190+ const runAuthenticationTest = async ( setupCredentialsProvider : ( ) => any ) => {
191+ const { client, reAuthSpy } = setupTestClient ( setupCredentialsProvider ( ) ) ;
192+
193+ try {
194+ await client . connect ( ) ;
195+ await runClientOperations ( client ) ;
196+ validateTokens ( reAuthSpy ) ;
197+ } finally {
198+ await client . destroy ( ) ;
199+ }
200+ } ;
201+
202+ } ) ;
0 commit comments