-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth): add EntraId integration tests
- Add integration tests for token renewal and re-authentication flows - Update credentials provider to use uniqueId as username instead of account username - Add test utilities for loading Redis endpoint configurations - Split TypeScript configs into separate files for samples and integration tests
- Loading branch information
1 parent
ac972bd
commit b326302
Showing
9 changed files
with
240 additions
and
12 deletions.
There are no files selected for viewing
175 changes: 175 additions & 0 deletions
175
packages/entraid/integration-tests/entraid-integration.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { BasicAuth } from '@redis/authx'; | ||
import { createClient } from '@redis/client'; | ||
import { EntraIdCredentialsProviderFactory } from '../lib/entra-id-credentials-provider-factory'; | ||
import { strict as assert } from 'node:assert'; | ||
import { spy, SinonSpy } from 'sinon'; | ||
import { randomUUID } from 'crypto'; | ||
import { loadFromJson, RedisEndpointsConfig } from '@redis/test-utils/lib/cae-client-testing'; | ||
import { EntraidCredentialsProvider } from '../lib/entraid-credentials-provider'; | ||
|
||
describe('EntraID Integration Tests', () => { | ||
|
||
it('client configured with client secret should be able to authenticate/re-authenticate', async () => { | ||
const config = readConfigFromEnv(); | ||
await runAuthenticationTest(() => | ||
EntraIdCredentialsProviderFactory.createForClientCredentials({ | ||
clientId: config.clientId, | ||
clientSecret: config.clientSecret, | ||
authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId }, | ||
tokenManagerConfig: { | ||
expirationRefreshRatio: 0.0001 | ||
} | ||
}) | ||
); | ||
}); | ||
|
||
it('client configured with client certificate should be able to authenticate/re-authenticate', async () => { | ||
const config = readConfigFromEnv(); | ||
await runAuthenticationTest(() => | ||
EntraIdCredentialsProviderFactory.createForClientCredentialsWithCertificate({ | ||
clientId: config.clientId, | ||
certificate: { | ||
privateKey: config.privateKey, | ||
thumbprint: config.cert | ||
}, | ||
authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId }, | ||
tokenManagerConfig: { | ||
expirationRefreshRatio: 0.0001 | ||
} | ||
}) | ||
); | ||
}); | ||
|
||
interface TestConfig { | ||
clientId: string; | ||
clientSecret: string; | ||
authority: string; | ||
tenantId: string; | ||
redisScopes: string; | ||
cert: string; | ||
privateKey: string; | ||
userAssignedManagedId: string; | ||
endpoints: RedisEndpointsConfig; | ||
} | ||
|
||
const readConfigFromEnv = (): TestConfig => { | ||
const requiredEnvVars = { | ||
AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID, | ||
AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET, | ||
AZURE_AUTHORITY: process.env.AZURE_AUTHORITY, | ||
AZURE_TENANT_ID: process.env.AZURE_TENANT_ID, | ||
AZURE_REDIS_SCOPES: process.env.AZURE_REDIS_SCOPES, | ||
AZURE_CERT: process.env.AZURE_CERT, | ||
AZURE_PRIVATE_KEY: process.env.AZURE_PRIVATE_KEY, | ||
AZURE_USER_ASSIGNED_MANAGED_ID: process.env.AZURE_USER_ASSIGNED_MANAGED_ID, | ||
REDIS_ENDPOINTS_CONFIG_PATH: process.env.REDIS_ENDPOINTS_CONFIG_PATH | ||
}; | ||
|
||
Object.entries(requiredEnvVars).forEach(([key, value]) => { | ||
if (value == undefined) { | ||
throw new Error(`${key} environment variable must be set`); | ||
} | ||
}); | ||
|
||
return { | ||
endpoints: loadFromJson(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH), | ||
clientId: requiredEnvVars.AZURE_CLIENT_ID, | ||
clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET, | ||
authority: requiredEnvVars.AZURE_AUTHORITY, | ||
tenantId: requiredEnvVars.AZURE_TENANT_ID, | ||
redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES, | ||
cert: requiredEnvVars.AZURE_CERT, | ||
privateKey: requiredEnvVars.AZURE_PRIVATE_KEY, | ||
userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID | ||
}; | ||
}; | ||
|
||
interface TokenDetail { | ||
token: string; | ||
exp: number; | ||
iat: number; | ||
lifetime: number; | ||
uti: string; | ||
} | ||
|
||
const setupTestClient = (credentialsProvider: EntraidCredentialsProvider) => { | ||
const config = readConfigFromEnv(); | ||
const client = createClient({ | ||
url: config.endpoints['standalone-entraid-acl'].endpoints[0], | ||
credentialsProvider | ||
}); | ||
|
||
const clientInstance = (client as any)._self; | ||
const reAuthSpy: SinonSpy = spy(clientInstance, 'reAuthenticate'); | ||
|
||
return { client, reAuthSpy }; | ||
}; | ||
|
||
const runClientOperations = async (client: any) => { | ||
const startTime = Date.now(); | ||
while (Date.now() - startTime < 1000) { | ||
const key = randomUUID(); | ||
await client.set(key, 'value'); | ||
const value = await client.get(key); | ||
assert.equal(value, 'value'); | ||
await client.del(key); | ||
} | ||
}; | ||
|
||
const validateTokens = (reAuthSpy: SinonSpy) => { | ||
assert(reAuthSpy.callCount >= 1, | ||
`reAuthenticate should have been called at least once, but was called ${reAuthSpy.callCount} times`); | ||
|
||
const tokenDetails: TokenDetail[] = reAuthSpy.getCalls().map(call => { | ||
const creds = call.args[0] as BasicAuth; | ||
const tokenPayload = JSON.parse( | ||
Buffer.from(creds.password.split('.')[1], 'base64').toString() | ||
); | ||
|
||
return { | ||
token: creds.password, | ||
exp: tokenPayload.exp, | ||
iat: tokenPayload.iat, | ||
lifetime: tokenPayload.exp - tokenPayload.iat, | ||
uti: tokenPayload.uti | ||
}; | ||
}); | ||
|
||
// Verify unique tokens | ||
const uniqueTokens = new Set(tokenDetails.map(detail => detail.token)); | ||
assert.equal( | ||
uniqueTokens.size, | ||
reAuthSpy.callCount, | ||
`Expected ${reAuthSpy.callCount} different tokens, but got ${uniqueTokens.size} unique tokens` | ||
); | ||
|
||
// Verify all tokens are not cached (i.e. have the same lifetime) | ||
const uniqueLifetimes = new Set(tokenDetails.map(detail => detail.lifetime)); | ||
assert.equal( | ||
uniqueLifetimes.size, | ||
1, | ||
`Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${[uniqueLifetimes].join(', ')} seconds` | ||
); | ||
|
||
// Verify that all tokens have different uti (unique token identifier) | ||
const uniqueUti = new Set(tokenDetails.map(detail => detail.uti)); | ||
assert.equal( | ||
uniqueUti.size, | ||
reAuthSpy.callCount, | ||
`Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${[uniqueUti].join(', ')}` | ||
); | ||
}; | ||
|
||
const runAuthenticationTest = async (setupCredentialsProvider: () => any) => { | ||
const { client, reAuthSpy } = setupTestClient(setupCredentialsProvider()); | ||
|
||
try { | ||
await client.connect(); | ||
await runClientOperations(client); | ||
validateTokens(reAuthSpy); | ||
} finally { | ||
await client.destroy(); | ||
} | ||
}; | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
"include": [ | ||
"./integration-tests/**/*.ts", | ||
"./lib/**/*.ts" | ||
], | ||
"compilerOptions": { | ||
"noEmit": true | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
"include": [ | ||
"./samples/**/*.ts", | ||
"./lib/**/*.ts" | ||
], | ||
"compilerOptions": { | ||
"noEmit": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
interface RawRedisEndpoint { | ||
username?: string; | ||
password?: string; | ||
tls: boolean; | ||
endpoints: string[]; | ||
} | ||
|
||
export type RedisEndpointsConfig = Record<string, RawRedisEndpoint>; | ||
|
||
export function loadFromJson(jsonString: string): RedisEndpointsConfig { | ||
try { | ||
return JSON.parse(jsonString) as RedisEndpointsConfig; | ||
} catch (error) { | ||
throw new Error(`Invalid JSON configuration: ${error}`); | ||
} | ||
} |