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