Skip to content

Commit a31045c

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 a31045c

11 files changed

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

0 commit comments

Comments
 (0)