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+
9+ describe ( 'EntraID Integration Tests' , ( ) => {
10+
11+ interface TestConfig {
12+ clientId : string ;
13+ clientSecret : string ;
14+ authority : string ;
15+ tenantId : string ;
16+ redisScopes : string ;
17+ cert : string ;
18+ privateKey : string ;
19+ userAssignedManagedId : string
20+ endpoints : RedisEndpointsConfig
21+ }
22+
23+ const readConfigFromEnv = ( ) : TestConfig => {
24+ const requiredEnvVars = {
25+ AZURE_CLIENT_ID : process . env . AZURE_CLIENT_ID ,
26+ AZURE_CLIENT_SECRET : process . env . AZURE_CLIENT_SECRET ,
27+ AZURE_AUTHORITY : process . env . AZURE_AUTHORITY ,
28+ AZURE_TENANT_ID : process . env . AZURE_TENANT_ID ,
29+ AZURE_REDIS_SCOPES : process . env . AZURE_REDIS_SCOPES ,
30+ AZURE_CERT : process . env . AZURE_CERT ,
31+ AZURE_PRIVATE_KEY : process . env . AZURE_PRIVATE_KEY ,
32+ AZURE_USER_ASSIGNED_MANAGED_ID : process . env . AZURE_USER_ASSIGNED_MANAGED_ID ,
33+ REDIS_ENDPOINTS_CONFIG_PATH : process . env . REDIS_ENDPOINTS_CONFIG_PATH
34+ } ;
35+
36+ Object . entries ( requiredEnvVars ) . forEach ( ( [ key , value ] ) => {
37+ console . log ( `key: ${ key } , value: ${ value } ` ) ;
38+ if ( value == undefined ) {
39+ throw new Error ( `${ key } environment variable must be set` ) ;
40+ }
41+ } ) ;
42+
43+ return {
44+ endpoints : loadFromJson ( requiredEnvVars . REDIS_ENDPOINTS_CONFIG_PATH ) ,
45+ clientId : requiredEnvVars . AZURE_CLIENT_ID ,
46+ clientSecret : requiredEnvVars . AZURE_CLIENT_SECRET ,
47+ authority : requiredEnvVars . AZURE_AUTHORITY ,
48+ tenantId : requiredEnvVars . AZURE_TENANT_ID ,
49+ redisScopes : requiredEnvVars . AZURE_REDIS_SCOPES ,
50+ cert : requiredEnvVars . AZURE_CERT ,
51+ privateKey : requiredEnvVars . AZURE_PRIVATE_KEY ,
52+ userAssignedManagedId : requiredEnvVars . AZURE_USER_ASSIGNED_MANAGED_ID
53+ } ;
54+ } ;
55+
56+ it ( 'client configured with with a client secret should be able to authenticate/re-authenticate' , async ( ) => {
57+
58+ const { clientId, clientSecret, tenantId, endpoints } = readConfigFromEnv ( ) ;
59+
60+ const entraidCredentialsProvider = EntraIdCredentialsProviderFactory . createForClientCredentials ( {
61+ clientId : clientId ,
62+ clientSecret : clientSecret ,
63+ authorityConfig : { type : 'multi-tenant' , tenantId : tenantId } ,
64+ tokenManagerConfig : {
65+ expirationRefreshRatio : 0.0001
66+ }
67+ } ) ;
68+
69+ const client = createClient ( {
70+ url : endpoints [ 'standalone-entraid-acl' ] . endpoints [ 0 ] ,
71+ credentialsProvider : entraidCredentialsProvider
72+ } ) ;
73+
74+ const clientInstance = ( client as any ) . _self ;
75+ const reAuthSpy : SinonSpy = spy ( clientInstance , < any > 'reAuthenticate' ) ;
76+
77+ try {
78+ await client . connect ( ) ;
79+
80+ const startTime = Date . now ( ) ;
81+ while ( Date . now ( ) - startTime < 1000 ) {
82+ const key = randomUUID ( ) ;
83+ await client . set ( key , 'value' ) ;
84+ const value = await client . get ( key ) ;
85+ assert . equal ( value , 'value' ) ;
86+ await client . del ( key ) ;
87+ }
88+
89+ assert ( reAuthSpy . callCount >= 1 , `reAuthenticate should have been called at least once, but was called ${ reAuthSpy . callCount } times` ) ;
90+
91+ const tokenDetails = reAuthSpy . getCalls ( ) . map ( call => {
92+ const creds = call . args [ 0 ] as BasicAuth ;
93+ const tokenPayload = JSON . parse (
94+ Buffer . from ( creds . password . split ( '.' ) [ 1 ] , 'base64' ) . toString ( )
95+ ) ;
96+
97+ return {
98+ token : creds . password ,
99+ exp : tokenPayload . exp ,
100+ iat : tokenPayload . iat ,
101+ lifetime : tokenPayload . exp - tokenPayload . iat ,
102+ uti : tokenPayload . uti
103+ } ;
104+ } ) ;
105+
106+ // Verify unique tokens
107+ const uniqueTokens = new Set ( tokenDetails . map ( detail => detail . token ) ) ;
108+ assert . equal (
109+ uniqueTokens . size ,
110+ reAuthSpy . callCount ,
111+ `Expected ${ reAuthSpy . callCount } different tokens, but got ${ uniqueTokens . size } unique tokens`
112+ ) ;
113+
114+ // Verify all tokens are not cached (i.e. have the same lifetime)
115+ const uniqueLifetimes = new Set ( tokenDetails . map ( detail => detail . lifetime ) ) ;
116+ assert . equal (
117+ uniqueLifetimes . size ,
118+ 1 ,
119+ `Expected all tokens to have the same lifetime, but found ${ uniqueLifetimes . size } different lifetimes: ${ [ uniqueLifetimes ] . join ( ', ' ) } seconds`
120+ ) ;
121+
122+ // Verify that all tokens have different uti ( unique token identifier)
123+ const uniqueUti = new Set ( tokenDetails . map ( detail => detail . uti ) ) ;
124+ assert . equal (
125+ uniqueUti . size ,
126+ reAuthSpy . callCount ,
127+ `Expected all tokens to have different uti, but found ${ uniqueUti . size } different uti in: ${ [ uniqueUti ] . join ( ', ' ) } `
128+ ) ;
129+
130+ } finally {
131+ await client . destroy ( ) ;
132+ }
133+ } ) ;
134+ } ) ;
0 commit comments