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