You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-07 13:22:56 +03:00
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
418 lines
13 KiB
TypeScript
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
|
|
);
|
|
}
|
|
}
|
|
|