1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00
Files
node-redis/packages/entraid/lib/entra-id-credentials-provider-factory.ts
Bobby I. 8b4ed0059a feat(entraid): add support for azure identity (#2901)
This PR adds support for using Azure Identity's credential classes with Redis Enterprise Entra ID authentication.
The main changes include:

- Add a new factory method createForDefaultAzureCredential to enable using Azure Identity credentials
- Add @azure/identity as a dependency to support the new authentication flow
- Add support for DefaultAzureCredential, EnvironmentCredential, and any other TokenCredential implementation
- Create a new AzureIdentityProvider to support DefaultAzureCredential
- Update documentation and README with usage examples for DefaultAzureCredential
- Add integration tests for the new authentication methods
- Include a sample application demonstrating interactive browser authentication
- Export constants for Redis scopes / credential mappers to simplify authentication configuration
2025-03-05 14:47:18 +02:00

418 lines
13 KiB
TypeScript

import type { GetTokenOptions, TokenCredential } from '@azure/core-auth';
import { NetworkError } from '@azure/msal-common';
import {
LogLevel,
ManagedIdentityApplication,
ManagedIdentityConfiguration,
AuthenticationResult,
PublicClientApplication,
ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo
} from '@azure/msal-node';
import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError, BasicAuth } from '@redis/client/dist/lib/authx';
import { AzureIdentityProvider } from './azure-identity-provider';
import { AuthenticationResponse, DEFAULT_CREDENTIALS_MAPPER, EntraidCredentialsProvider, OID_CREDENTIALS_MAPPER } from './entraid-credentials-provider';
import { MSALIdentityProvider } from './msal-identity-provider';
/**
* This class is used to create credentials providers for different types of authentication flows.
*/
export class EntraIdCredentialsProviderFactory {
/**
* This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities.
*
* @param params
* @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID,
* full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication.
*
*/
public static createManagedIdentityProvider(
params: CredentialParams, userAssignedClientId?: string
): EntraidCredentialsProvider {
const config: ManagedIdentityConfiguration = {
// For user-assigned identity, include the client ID
...(userAssignedClientId && {
managedIdentityIdParams: {
userAssignedClientId
}
}),
system: {
loggerOptions
}
};
const client = new ManagedIdentityApplication(config);
const idp = new MSALIdentityProvider(
() => client.acquireToken({
resource: params.scopes?.[0] ?? REDIS_SCOPE,
forceRefresh: true
}).then(x => x === null ? Promise.reject('Token is null') : x)
);
return new EntraidCredentialsProvider(
new TokenManager(idp, params.tokenManagerConfig),
idp,
{
onReAuthenticationError: params.onReAuthenticationError,
credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER,
onRetryableError: params.onRetryableError
}
);
}
/**
* This method is used to create a credentials provider for system-assigned managed identities.
* @param params
*/
static createForSystemAssignedManagedIdentity(
params: CredentialParams
): EntraidCredentialsProvider {
return this.createManagedIdentityProvider(params);
}
/**
* This method is used to create a credentials provider for user-assigned managed identities.
* It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration.
* @param params
*/
static createForUserAssignedManagedIdentity(
params: CredentialParams & { userAssignedClientId: string }
): EntraidCredentialsProvider {
return this.createManagedIdentityProvider(params, params.userAssignedClientId);
}
static #createForClientCredentials(
authConfig: NodeAuthOptions,
params: CredentialParams
): EntraidCredentialsProvider {
const config: Configuration = {
auth: {
...authConfig,
authority: this.getAuthority(params.authorityConfig ?? { type: 'default' })
},
system: {
loggerOptions
}
};
const client = new ConfidentialClientApplication(config);
const idp = new MSALIdentityProvider(
() => client.acquireTokenByClientCredential({
skipCache: true,
scopes: params.scopes ?? [REDIS_SCOPE_DEFAULT]
}).then(x => x === null ? Promise.reject('Token is null') : x)
);
return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp,
{
onReAuthenticationError: params.onReAuthenticationError,
credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER,
onRetryableError: params.onRetryableError
});
}
/**
* This method is used to create a credentials provider for service principals using certificate.
* @param params
*/
static createForClientCredentialsWithCertificate(
params: ClientCredentialsWithCertificateParams
): EntraidCredentialsProvider {
return this.#createForClientCredentials(
{
clientId: params.clientId,
clientCertificate: params.certificate
},
params
);
}
/**
* This method is used to create a credentials provider for service principals using client secret.
* @param params
*/
static createForClientCredentials(
params: ClientSecretCredentialsParams
): EntraidCredentialsProvider {
return this.#createForClientCredentials(
{
clientId: params.clientId,
clientSecret: params.clientSecret
},
params
);
}
/**
* This method is used to create a credentials provider using DefaultAzureCredential.
*
* The user needs to create a configured instance of DefaultAzureCredential ( or any other class that implements TokenCredential )and pass it to this method.
*
* The default credentials mapper for this method is OID_CREDENTIALS_MAPPER which extracts the object ID from JWT
* encoded token.
*
* Depending on the actual flow that DefaultAzureCredential uses, the user may need to provide different
* credential mapper via the credentialsMapper parameter.
*
*/
static createForDefaultAzureCredential(
{
credential,
scopes,
options,
tokenManagerConfig,
onReAuthenticationError,
credentialsMapper,
onRetryableError
}: DefaultAzureCredentialsParams
): EntraidCredentialsProvider {
const idp = new AzureIdentityProvider(
() => credential.getToken(scopes, options).then(x => x === null ? Promise.reject('Token is null') : x)
);
return new EntraidCredentialsProvider(new TokenManager(idp, tokenManagerConfig), idp,
{
onReAuthenticationError: onReAuthenticationError,
credentialsMapper: credentialsMapper ?? OID_CREDENTIALS_MAPPER,
onRetryableError: onRetryableError
});
}
/**
* This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
* @param params
*/
static createForAuthorizationCodeWithPKCE(
params: AuthCodePKCEParams
): {
getPKCECodes: () => Promise<{
verifier: string;
challenge: string;
challengeMethod: string;
}>;
getAuthCodeUrl: (
pkceCodes: { challenge: string; challengeMethod: string }
) => Promise<string>;
createCredentialsProvider: (
params: PKCEParams
) => EntraidCredentialsProvider;
} {
const requiredScopes = ['user.read', 'offline_access'];
const scopes = [...new Set([...(params.scopes || []), ...requiredScopes])];
const authFlow = AuthCodeFlowHelper.create({
clientId: params.clientId,
redirectUri: params.redirectUri,
scopes: scopes,
authorityConfig: params.authorityConfig
});
return {
getPKCECodes: AuthCodeFlowHelper.generatePKCE,
getAuthCodeUrl: (pkceCodes) => authFlow.getAuthCodeUrl(pkceCodes),
createCredentialsProvider: (pkceParams) => {
// This is used to store the initial credentials account to be used
// for silent token acquisition after the initial token acquisition.
let initialCredentialsAccount: AccountInfo | null = null;
const idp = new MSALIdentityProvider(
async () => {
if (!initialCredentialsAccount) {
let authResult = await authFlow.acquireTokenByCode(pkceParams);
initialCredentialsAccount = authResult.account;
return authResult;
} else {
return authFlow.client.acquireTokenSilent({
forceRefresh: true,
account: initialCredentialsAccount,
scopes
});
}
}
);
const tm = new TokenManager(idp, params.tokenManagerConfig);
return new EntraidCredentialsProvider(tm, idp, {
onReAuthenticationError: params.onReAuthenticationError,
credentialsMapper: params.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER,
onRetryableError: params.onRetryableError
});
}
};
}
static getAuthority(config: AuthorityConfig): string {
switch (config.type) {
case 'multi-tenant':
return `https://login.microsoftonline.com/${config.tenantId}`;
case 'custom':
return config.authorityUrl;
case 'default':
return 'https://login.microsoftonline.com/common';
default:
throw new Error('Invalid authority configuration');
}
}
}
export const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default';
export const REDIS_SCOPE = 'https://redis.azure.com'
export type AuthorityConfig =
| { type: 'multi-tenant'; tenantId: string }
| { type: 'custom'; authorityUrl: string }
| { type: 'default' };
export type PKCEParams = {
code: string;
verifier: string;
clientInfo?: string;
}
export type CredentialParams = {
clientId: string;
scopes?: string[];
authorityConfig?: AuthorityConfig;
tokenManagerConfig: TokenManagerConfig
onReAuthenticationError?: (error: ReAuthenticationError) => void
credentialsMapper?: (token: AuthenticationResponse) => BasicAuth
onRetryableError?: (error: string) => void
}
export type DefaultAzureCredentialsParams = {
scopes: string | string[],
options?: GetTokenOptions,
credential: TokenCredential
tokenManagerConfig: TokenManagerConfig
onReAuthenticationError?: (error: ReAuthenticationError) => void
credentialsMapper?: (token: AuthenticationResponse) => BasicAuth
onRetryableError?: (error: string) => void
}
export type AuthCodePKCEParams = CredentialParams & {
redirectUri: string;
};
export type ClientSecretCredentialsParams = CredentialParams & {
clientSecret: string;
};
export type ClientCredentialsWithCertificateParams = CredentialParams & {
certificate: {
thumbprint: string;
privateKey: string;
x5c?: string;
};
};
const loggerOptions = {
loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) {
if (!containsPii) console.log(message);
},
piiLoggingEnabled: false,
logLevel: LogLevel.Error
}
/**
* The most important part of the RetryPolicy is the `isRetryable` function. This function is used to determine if a request should be retried based
* on the error returned from the identity provider. The default for is to retry on network errors only.
*/
export const DEFAULT_RETRY_POLICY: RetryPolicy = {
// currently only retry on network errors
isRetryable: (error: unknown) => error instanceof NetworkError,
maxAttempts: 10,
initialDelayMs: 100,
maxDelayMs: 100000,
backoffMultiplier: 2,
jitterPercentage: 0.1
};
export const DEFAULT_TOKEN_MANAGER_CONFIG: TokenManagerConfig = {
retry: DEFAULT_RETRY_POLICY,
expirationRefreshRatio: 0.7 // Refresh token when 70% of the token has expired
}
/**
* This class is used to help with the Authorization Code Flow with PKCE.
* It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider.
*/
export class AuthCodeFlowHelper {
private constructor(
readonly client: PublicClientApplication,
readonly scopes: string[],
readonly redirectUri: string
) {}
async getAuthCodeUrl(pkceCodes: {
challenge: string;
challengeMethod: string;
}): Promise<string> {
const authCodeUrlParameters: AuthorizationUrlRequest = {
scopes: this.scopes,
redirectUri: this.redirectUri,
codeChallenge: pkceCodes.challenge,
codeChallengeMethod: pkceCodes.challengeMethod
};
return this.client.getAuthCodeUrl(authCodeUrlParameters);
}
async acquireTokenByCode(params: PKCEParams): Promise<AuthenticationResult> {
const tokenRequest: AuthorizationCodeRequest = {
code: params.code,
scopes: this.scopes,
redirectUri: this.redirectUri,
codeVerifier: params.verifier,
clientInfo: params.clientInfo
};
return this.client.acquireTokenByCode(tokenRequest);
}
static async generatePKCE(): Promise<{
verifier: string;
challenge: string;
challengeMethod: string;
}> {
const cryptoProvider = new CryptoProvider();
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
return {
verifier,
challenge,
challengeMethod: 'S256'
};
}
static create(params: {
clientId: string;
redirectUri: string;
scopes?: string[];
authorityConfig?: AuthorityConfig;
}): AuthCodeFlowHelper {
const config = {
auth: {
clientId: params.clientId,
authority: EntraIdCredentialsProviderFactory.getAuthority(params.authorityConfig ?? { type: 'default' })
},
system: {
loggerOptions
}
};
return new AuthCodeFlowHelper(
new PublicClientApplication(config),
params.scopes ?? ['user.read'],
params.redirectUri
);
}
}