Skip to content

Commit cb2e186

Browse files
committed
feat(auth): add Entra ID identity provider integration
Introduces Entra ID (former Azure AD) authentication support with multiple authentication flows and automated token lifecycle management. Key additions: - Add EntraIdCredentialsProvider for handling Entra ID authentication flows - Implement MSALIdentityProvider to integrate with MSAL/EntraID authentication library - Add support for multiple authentication methods: - Managed identities (system and user-assigned) - Client credentials with certificate - Client credentials with secret - Authorization Code flow with PKCE - Add factory class with builder methods for each authentication flow - Include sample Express server implementation for Authorization Code flow - Export core auth types from client package for reuse - Add comprehensive configuration options for authority and token management Package exports (@redis/client): - Added package.json "exports" field to enable auth folder barrel exports - Included additional export paths ("./index" and "./dist/*") to maintain compatibility with existing deep imports across the monorepo packages This change enables Azure-based authentication flows while maintaining consistent token lifecycle management patterns established in the core authentication system.
1 parent 0d78fb0 commit cb2e186

11 files changed

+1736
-43
lines changed

package-lock.json

+1,020-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { TokenManager, TokenManagerConfig, TokenStreamListener, RetryPolicy, IDPError } from './token-manager';
2+
export { TokenManager, TokenManagerConfig, TokenStreamListener, RetryPolicy, IDPError };
3+
import { Disposable } from './types';
4+
export { Disposable };
5+
6+
import { CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, CredentialsError, StreamingCredentialsListener, AsyncCredentialsProvider, ReAuthenticationError, BasicAuth } from './credentials-provider';
7+
export { CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, CredentialsError, StreamingCredentialsListener, AsyncCredentialsProvider, ReAuthenticationError, BasicAuth };
8+
9+
import { Token } from './token';
10+
export { Token };
11+
12+
import { IdentityProvider, TokenResponse } from './identity-provider';
13+
export { IdentityProvider, TokenResponse };

packages/client/lib/client/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import COMMANDS from '../commands';
2-
import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError } from './authx/credentials-provider';
3-
import {Disposable} from './authx/types';
2+
import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, Disposable } from './authx/';
43
import RedisSocket, { RedisSocketOptions } from './socket';
54
import RedisCommandsQueue, { CommandOptions } from './commands-queue';
65
import { EventEmitter } from 'node:events';

packages/entraid/README.md

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import { NetworkError } from '@azure/msal-common';
2+
import {
3+
LogLevel,
4+
ManagedIdentityApplication,
5+
ManagedIdentityConfiguration,
6+
AuthenticationResult,
7+
PublicClientApplication,
8+
ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo
9+
} from '@azure/msal-node';
10+
import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError } from '@redis/client/lib/client/authx';
11+
import { EntraidCredentialsProvider } from './entraid-credentials-provider';
12+
import { MSALIdentityProvider } from './msal-identity-provider';
13+
14+
const FALLBACK_SCOPE = 'https://redis.azure.com/.default';
15+
16+
export type AuthorityConfig =
17+
| { type: 'multi-tenant'; tenantId: string }
18+
| { type: 'custom'; authorityUrl: string }
19+
| { type: 'default' };
20+
21+
export type PKCEParams = {
22+
code: string;
23+
verifier: string;
24+
clientInfo?: string;
25+
}
26+
27+
export type CredentialParams = {
28+
clientId: string;
29+
scopes?: string[];
30+
authorityConfig?: AuthorityConfig;
31+
32+
tokenManagerConfig: TokenManagerConfig
33+
onReAuthenticationError?: (error: ReAuthenticationError) => void;
34+
}
35+
36+
export type AuthCodePKCEParams = CredentialParams & {
37+
redirectUri: string;
38+
};
39+
40+
export type ClientSecretCredentialsParams = CredentialParams & {
41+
clientSecret: string;
42+
};
43+
44+
export type ClientCredentialsWithCertificateParams = CredentialParams & {
45+
certificate: {
46+
thumbprint: string;
47+
privateKey: string;
48+
x5c?: string;
49+
};
50+
};
51+
52+
const loggerOptions = {
53+
loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) {
54+
if (!containsPii) console.log(message);
55+
},
56+
piiLoggingEnabled: false,
57+
logLevel: LogLevel.Verbose
58+
}
59+
60+
/**
61+
* The most imporant part of the RetryPolicy is the shouldRetry function. This function is used to determine if a request should be retried based
62+
* on the error returned from the identity provider. The defaultRetryPolicy is used to retry on network errors only.
63+
*/
64+
export const DEFAULT_RETRY_POLICY: RetryPolicy = {
65+
// currently only retry on network errors
66+
shouldRetry: (error: unknown) => error instanceof NetworkError,
67+
maxAttempts: 10,
68+
initialDelayMs: 100,
69+
maxDelayMs: 100000,
70+
backoffMultiplier: 2,
71+
jitterPercentage: 0.1
72+
73+
};
74+
75+
export const DEFAULT_TOKEN_MANAGER_CONFIG: TokenManagerConfig = {
76+
retry: DEFAULT_RETRY_POLICY,
77+
expirationRefreshRatio: 0.7 // Refresh token when 70% of the token has expired
78+
}
79+
80+
/**
81+
* This class is used to help with the Authorization Code Flow with PKCE.
82+
* It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider.
83+
*/
84+
export class AuthCodeFlowHelper {
85+
private constructor(
86+
readonly client: PublicClientApplication,
87+
readonly scopes: string[],
88+
readonly redirectUri: string
89+
) {}
90+
91+
async getAuthCodeUrl(pkceCodes: {
92+
challenge: string;
93+
challengeMethod: string;
94+
}): Promise<string> {
95+
const authCodeUrlParameters: AuthorizationUrlRequest = {
96+
scopes: this.scopes,
97+
redirectUri: this.redirectUri,
98+
codeChallenge: pkceCodes.challenge,
99+
codeChallengeMethod: pkceCodes.challengeMethod
100+
};
101+
102+
return this.client.getAuthCodeUrl(authCodeUrlParameters);
103+
}
104+
105+
async acquireTokenByCode(params: PKCEParams): Promise<AuthenticationResult> {
106+
const tokenRequest: AuthorizationCodeRequest = {
107+
code: params.code,
108+
scopes: this.scopes,
109+
redirectUri: this.redirectUri,
110+
codeVerifier: params.verifier,
111+
clientInfo: params.clientInfo
112+
};
113+
114+
return this.client.acquireTokenByCode(tokenRequest);
115+
}
116+
117+
static async generatePKCE(): Promise<{
118+
verifier: string;
119+
challenge: string;
120+
challengeMethod: string;
121+
}> {
122+
const cryptoProvider = new CryptoProvider();
123+
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
124+
return {
125+
verifier,
126+
challenge,
127+
challengeMethod: 'S256'
128+
};
129+
}
130+
131+
static create(params: {
132+
clientId: string;
133+
redirectUri: string;
134+
scopes?: string[];
135+
authorityConfig?: AuthorityConfig;
136+
}): AuthCodeFlowHelper {
137+
const config = {
138+
auth: {
139+
clientId: params.clientId,
140+
authority: EntraIdCredentialsProviderFactory.getAuthority(params.authorityConfig ?? { type: 'default' })
141+
},
142+
system: {
143+
loggerOptions
144+
}
145+
};
146+
147+
return new AuthCodeFlowHelper(
148+
new PublicClientApplication(config),
149+
params.scopes ?? ['user.read'],
150+
params.redirectUri
151+
);
152+
}
153+
}
154+
155+
/**
156+
* This class is used to create credentials providers for different types of authentication flows.
157+
*/
158+
export class EntraIdCredentialsProviderFactory {
159+
160+
static getAuthority(config: AuthorityConfig): string {
161+
switch (config.type) {
162+
case 'multi-tenant':
163+
return `https://login.microsoftonline.com/${config.tenantId}`;
164+
case 'custom':
165+
return config.authorityUrl;
166+
case 'default':
167+
return 'https://login.microsoftonline.com/common';
168+
default:
169+
throw new Error('Invalid authority configuration');
170+
}
171+
}
172+
173+
/**
174+
* This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities.
175+
* for user-assigned managed identities, the developer needs to pass either the client ID, full resource identifier,
176+
* or the object ID of the managed identity when creating ManagedIdentityApplication.
177+
*
178+
* @param params
179+
* @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID,
180+
* full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication.
181+
*
182+
* @private
183+
*/
184+
public static createManagedIdentityProvider(
185+
params: CredentialParams, userAssignedClientId?: string
186+
): EntraidCredentialsProvider {
187+
const config: ManagedIdentityConfiguration = {
188+
// For user-assigned identity, include the client ID
189+
...(userAssignedClientId && {
190+
managedIdentityIdParams: {
191+
userAssignedClientId
192+
}
193+
}),
194+
system: {
195+
loggerOptions
196+
}
197+
};
198+
199+
const client = new ManagedIdentityApplication(config);
200+
201+
const idp = new MSALIdentityProvider(
202+
() => client.acquireToken({
203+
resource: params.scopes?.[0] ?? FALLBACK_SCOPE
204+
}).then(x => x === null ? Promise.reject('Token is null') : x)
205+
);
206+
207+
return new EntraidCredentialsProvider(
208+
new TokenManager(idp, params.tokenManagerConfig),
209+
idp,
210+
{ onReAuthenticationError: params.onReAuthenticationError }
211+
);
212+
}
213+
214+
/**
215+
* This method is used to create a credentials provider for system-assigned managed identities.
216+
* @param params
217+
*/
218+
static createForSystemAssignedManagedIdentity(
219+
params: CredentialParams
220+
): EntraidCredentialsProvider {
221+
return this.createManagedIdentityProvider(params);
222+
}
223+
224+
/**
225+
* This method is used to create a credentials provider for user-assigned managed identities.
226+
* It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration.
227+
* @param params
228+
*/
229+
static createForUserAssignedManagedIdentity(
230+
params: CredentialParams
231+
): EntraidCredentialsProvider {
232+
return this.createManagedIdentityProvider(params, params.clientId);
233+
}
234+
235+
private static _createForClientCredentials(
236+
authConfig: NodeAuthOptions,
237+
params: CredentialParams
238+
): EntraidCredentialsProvider {
239+
const config: Configuration = {
240+
auth: {
241+
...authConfig,
242+
authority: this.getAuthority(params.authorityConfig ?? { type: 'default' })
243+
},
244+
system: {
245+
loggerOptions
246+
}
247+
};
248+
249+
const client = new ConfidentialClientApplication(config);
250+
251+
const idp = new MSALIdentityProvider(
252+
() => client.acquireTokenByClientCredential({
253+
scopes: params.scopes ?? [FALLBACK_SCOPE]
254+
}).then(x => x === null ? Promise.reject('Token is null') : x)
255+
);
256+
257+
return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp,
258+
{ onReAuthenticationError: params.onReAuthenticationError });
259+
}
260+
261+
/**
262+
* This method is used to create a credentials provider for service principals using certificate.
263+
* @param params
264+
*/
265+
static createForClientCredentialsWithCertificate(
266+
params: ClientCredentialsWithCertificateParams
267+
): EntraidCredentialsProvider {
268+
return this._createForClientCredentials(
269+
{
270+
clientId: params.clientId,
271+
clientCertificate: params.certificate
272+
},
273+
params
274+
);
275+
}
276+
277+
/**
278+
* This method is used to create a credentials provider for service principals using client secret.
279+
* @param params
280+
*/
281+
static createForClientCredentials(
282+
params: ClientSecretCredentialsParams
283+
): EntraidCredentialsProvider {
284+
return this._createForClientCredentials(
285+
{
286+
clientId: params.clientId,
287+
clientSecret: params.clientSecret
288+
},
289+
params
290+
);
291+
}
292+
293+
/**
294+
* This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
295+
* @param params
296+
*/
297+
static createForAuthorizationCodeWithPKCE(
298+
params: AuthCodePKCEParams
299+
): {
300+
getPKCECodes: () => Promise<{
301+
verifier: string;
302+
challenge: string;
303+
challengeMethod: string;
304+
}>;
305+
getAuthCodeUrl: (
306+
pkceCodes: { challenge: string; challengeMethod: string }
307+
) => Promise<string>;
308+
createCredentialsProvider: (
309+
params: PKCEParams
310+
) => EntraidCredentialsProvider;
311+
} {
312+
313+
const requiredScopes = ['user.read', 'offline_access'];
314+
const scopes = [...new Set([...(params.scopes || []), ...requiredScopes])];
315+
316+
const authFlow = AuthCodeFlowHelper.create({
317+
clientId: params.clientId,
318+
redirectUri: params.redirectUri,
319+
scopes: scopes,
320+
authorityConfig: params.authorityConfig
321+
});
322+
323+
return {
324+
getPKCECodes: AuthCodeFlowHelper.generatePKCE,
325+
getAuthCodeUrl: (pkceCodes) => authFlow.getAuthCodeUrl(pkceCodes),
326+
createCredentialsProvider: (pkceParams) => {
327+
328+
// This is used to store the initial credentials account to be used
329+
// for silent token acquisition after the initial token acquisition.
330+
let initialCredentialsAccount: AccountInfo | null = null;
331+
332+
const idp = new MSALIdentityProvider(
333+
async () => {
334+
if (!initialCredentialsAccount) {
335+
let authResult = await authFlow.acquireTokenByCode(pkceParams);
336+
initialCredentialsAccount = authResult.account;
337+
return authResult;
338+
} else {
339+
return authFlow.client.acquireTokenSilent({
340+
account: initialCredentialsAccount,
341+
scopes
342+
});
343+
}
344+
345+
}
346+
);
347+
const tm = new TokenManager(idp, params.tokenManagerConfig);
348+
return new EntraidCredentialsProvider(tm, idp, { onReAuthenticationError: params.onReAuthenticationError });
349+
}
350+
};
351+
}
352+
}

0 commit comments

Comments
 (0)