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
+ interface TestConfig {
44
+ clientId : string ;
45
+ clientSecret : string ;
46
+ authority : string ;
47
+ tenantId : string ;
48
+ redisScopes : string ;
49
+ cert : string ;
50
+ privateKey : string ;
51
+ userAssignedManagedId : string ;
52
+ endpoints : RedisEndpointsConfig ;
53
+ }
54
+
55
+ const readConfigFromEnv = ( ) : TestConfig => {
56
+ const requiredEnvVars = {
57
+ AZURE_CLIENT_ID : process . env . AZURE_CLIENT_ID ,
58
+ AZURE_CLIENT_SECRET : process . env . AZURE_CLIENT_SECRET ,
59
+ AZURE_AUTHORITY : process . env . AZURE_AUTHORITY ,
60
+ AZURE_TENANT_ID : process . env . AZURE_TENANT_ID ,
61
+ AZURE_REDIS_SCOPES : process . env . AZURE_REDIS_SCOPES ,
62
+ AZURE_CERT : process . env . AZURE_CERT ,
63
+ AZURE_PRIVATE_KEY : process . env . AZURE_PRIVATE_KEY ,
64
+ AZURE_USER_ASSIGNED_MANAGED_ID : process . env . AZURE_USER_ASSIGNED_MANAGED_ID ,
65
+ REDIS_ENDPOINTS_CONFIG_PATH : process . env . REDIS_ENDPOINTS_CONFIG_PATH
66
+ } ;
67
+
68
+ Object . entries ( requiredEnvVars ) . forEach ( ( [ key , value ] ) => {
69
+ if ( value == undefined ) {
70
+ throw new Error ( `${ key } environment variable must be set` ) ;
71
+ }
72
+ } ) ;
73
+
74
+ return {
75
+ endpoints : loadFromJson ( requiredEnvVars . REDIS_ENDPOINTS_CONFIG_PATH ) ,
76
+ clientId : requiredEnvVars . AZURE_CLIENT_ID ,
77
+ clientSecret : requiredEnvVars . AZURE_CLIENT_SECRET ,
78
+ authority : requiredEnvVars . AZURE_AUTHORITY ,
79
+ tenantId : requiredEnvVars . AZURE_TENANT_ID ,
80
+ redisScopes : requiredEnvVars . AZURE_REDIS_SCOPES ,
81
+ cert : requiredEnvVars . AZURE_CERT ,
82
+ privateKey : requiredEnvVars . AZURE_PRIVATE_KEY ,
83
+ userAssignedManagedId : requiredEnvVars . AZURE_USER_ASSIGNED_MANAGED_ID
84
+ } ;
85
+ } ;
86
+
87
+ interface TokenDetail {
88
+ token : string ;
89
+ exp : number ;
90
+ iat : number ;
91
+ lifetime : number ;
92
+ uti : string ;
93
+ }
94
+
95
+ const setupTestClient = ( credentialsProvider : EntraidCredentialsProvider ) => {
96
+ const config = readConfigFromEnv ( ) ;
97
+ const client = createClient ( {
98
+ url : config . endpoints [ 'standalone-entraid-acl' ] . endpoints [ 0 ] ,
99
+ credentialsProvider
100
+ } ) ;
101
+
102
+ const clientInstance = ( client as any ) . _self ;
103
+ const reAuthSpy : SinonSpy = spy ( clientInstance , 'reAuthenticate' ) ;
104
+
105
+ return { client, reAuthSpy } ;
106
+ } ;
107
+
108
+ const runClientOperations = async ( client : any ) => {
109
+ const startTime = Date . now ( ) ;
110
+ while ( Date . now ( ) - startTime < 1000 ) {
111
+ const key = randomUUID ( ) ;
112
+ await client . set ( key , 'value' ) ;
113
+ const value = await client . get ( key ) ;
114
+ assert . equal ( value , 'value' ) ;
115
+ await client . del ( key ) ;
116
+ }
117
+ } ;
118
+
119
+ const validateTokens = ( reAuthSpy : SinonSpy ) => {
120
+ assert ( reAuthSpy . callCount >= 1 ,
121
+ `reAuthenticate should have been called at least once, but was called ${ reAuthSpy . callCount } times` ) ;
122
+
123
+ const tokenDetails : TokenDetail [ ] = reAuthSpy . getCalls ( ) . map ( call => {
124
+ const creds = call . args [ 0 ] as BasicAuth ;
125
+ const tokenPayload = JSON . parse (
126
+ Buffer . from ( creds . password . split ( '.' ) [ 1 ] , 'base64' ) . toString ( )
127
+ ) ;
128
+
129
+ return {
130
+ token : creds . password ,
131
+ exp : tokenPayload . exp ,
132
+ iat : tokenPayload . iat ,
133
+ lifetime : tokenPayload . exp - tokenPayload . iat ,
134
+ uti : tokenPayload . uti
135
+ } ;
136
+ } ) ;
137
+
138
+ // Verify unique tokens
139
+ const uniqueTokens = new Set ( tokenDetails . map ( detail => detail . token ) ) ;
140
+ assert . equal (
141
+ uniqueTokens . size ,
142
+ reAuthSpy . callCount ,
143
+ `Expected ${ reAuthSpy . callCount } different tokens, but got ${ uniqueTokens . size } unique tokens`
144
+ ) ;
145
+
146
+ // Verify all tokens are not cached (i.e. have the same lifetime)
147
+ const uniqueLifetimes = new Set ( tokenDetails . map ( detail => detail . lifetime ) ) ;
148
+ assert . equal (
149
+ uniqueLifetimes . size ,
150
+ 1 ,
151
+ `Expected all tokens to have the same lifetime, but found ${ uniqueLifetimes . size } different lifetimes: ${ [ uniqueLifetimes ] . join ( ', ' ) } seconds`
152
+ ) ;
153
+
154
+ // Verify that all tokens have different uti (unique token identifier)
155
+ const uniqueUti = new Set ( tokenDetails . map ( detail => detail . uti ) ) ;
156
+ assert . equal (
157
+ uniqueUti . size ,
158
+ reAuthSpy . callCount ,
159
+ `Expected all tokens to have different uti, but found ${ uniqueUti . size } different uti in: ${ [ uniqueUti ] . join ( ', ' ) } `
160
+ ) ;
161
+ } ;
162
+
163
+ const runAuthenticationTest = async ( setupCredentialsProvider : ( ) => any ) => {
164
+ const { client, reAuthSpy } = setupTestClient ( setupCredentialsProvider ( ) ) ;
165
+
166
+ try {
167
+ await client . connect ( ) ;
168
+ await runClientOperations ( client ) ;
169
+ validateTokens ( reAuthSpy ) ;
170
+ } finally {
171
+ await client . destroy ( ) ;
172
+ }
173
+ } ;
174
+
175
+ } ) ;
0 commit comments