@memberjunction/server 2.89.0 → 2.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/auth/AuthProviderFactory.d.ts +24 -0
  2. package/dist/auth/AuthProviderFactory.d.ts.map +1 -0
  3. package/dist/auth/AuthProviderFactory.js +82 -0
  4. package/dist/auth/AuthProviderFactory.js.map +1 -0
  5. package/dist/auth/BaseAuthProvider.d.ts +18 -0
  6. package/dist/auth/BaseAuthProvider.d.ts.map +1 -0
  7. package/dist/auth/BaseAuthProvider.js +42 -0
  8. package/dist/auth/BaseAuthProvider.js.map +1 -0
  9. package/dist/auth/IAuthProvider.d.ts +13 -0
  10. package/dist/auth/IAuthProvider.d.ts.map +1 -0
  11. package/dist/auth/IAuthProvider.js +2 -0
  12. package/dist/auth/IAuthProvider.js.map +1 -0
  13. package/dist/auth/__tests__/backward-compatibility.test.d.ts +2 -0
  14. package/dist/auth/__tests__/backward-compatibility.test.d.ts.map +1 -0
  15. package/dist/auth/__tests__/backward-compatibility.test.js +135 -0
  16. package/dist/auth/__tests__/backward-compatibility.test.js.map +1 -0
  17. package/dist/auth/index.d.ts +22 -7
  18. package/dist/auth/index.d.ts.map +1 -1
  19. package/dist/auth/index.js +65 -32
  20. package/dist/auth/index.js.map +1 -1
  21. package/dist/auth/initializeProviders.d.ts +2 -0
  22. package/dist/auth/initializeProviders.d.ts.map +1 -0
  23. package/dist/auth/initializeProviders.js +23 -0
  24. package/dist/auth/initializeProviders.js.map +1 -0
  25. package/dist/auth/providers/Auth0Provider.d.ts +9 -0
  26. package/dist/auth/providers/Auth0Provider.d.ts.map +1 -0
  27. package/dist/auth/providers/Auth0Provider.js +42 -0
  28. package/dist/auth/providers/Auth0Provider.js.map +1 -0
  29. package/dist/auth/providers/CognitoProvider.d.ts +9 -0
  30. package/dist/auth/providers/CognitoProvider.d.ts.map +1 -0
  31. package/dist/auth/providers/CognitoProvider.js +46 -0
  32. package/dist/auth/providers/CognitoProvider.js.map +1 -0
  33. package/dist/auth/providers/GoogleProvider.d.ts +9 -0
  34. package/dist/auth/providers/GoogleProvider.d.ts.map +1 -0
  35. package/dist/auth/providers/GoogleProvider.js +41 -0
  36. package/dist/auth/providers/GoogleProvider.js.map +1 -0
  37. package/dist/auth/providers/MSALProvider.d.ts +9 -0
  38. package/dist/auth/providers/MSALProvider.d.ts.map +1 -0
  39. package/dist/auth/providers/MSALProvider.js +42 -0
  40. package/dist/auth/providers/MSALProvider.js.map +1 -0
  41. package/dist/auth/providers/OktaProvider.d.ts +9 -0
  42. package/dist/auth/providers/OktaProvider.d.ts.map +1 -0
  43. package/dist/auth/providers/OktaProvider.js +42 -0
  44. package/dist/auth/providers/OktaProvider.js.map +1 -0
  45. package/dist/config.d.ts +97 -21
  46. package/dist/config.d.ts.map +1 -1
  47. package/dist/config.js +13 -6
  48. package/dist/config.js.map +1 -1
  49. package/dist/context.d.ts.map +1 -1
  50. package/dist/context.js +25 -17
  51. package/dist/context.js.map +1 -1
  52. package/dist/generated/generated.d.ts +12 -0
  53. package/dist/generated/generated.d.ts.map +1 -1
  54. package/dist/generated/generated.js +61 -0
  55. package/dist/generated/generated.js.map +1 -1
  56. package/dist/generic/ResolverBase.d.ts +2 -2
  57. package/dist/generic/ResolverBase.d.ts.map +1 -1
  58. package/dist/generic/ResolverBase.js +5 -4
  59. package/dist/generic/ResolverBase.js.map +1 -1
  60. package/dist/generic/RunViewResolver.js.map +1 -1
  61. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  62. package/dist/resolvers/AskSkipResolver.js +3 -0
  63. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  64. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  65. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  66. package/dist/types.d.ts +2 -2
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js.map +1 -1
  69. package/package.json +39 -39
  70. package/src/auth/AuthProviderFactory.ts +152 -0
  71. package/src/auth/BaseAuthProvider.ts +71 -0
  72. package/src/auth/IAuthProvider.ts +49 -0
  73. package/src/auth/__tests__/backward-compatibility.test.ts +183 -0
  74. package/src/auth/index.ts +104 -36
  75. package/src/auth/initializeProviders.ts +31 -0
  76. package/src/auth/providers/Auth0Provider.ts +45 -0
  77. package/src/auth/providers/CognitoProvider.ts +50 -0
  78. package/src/auth/providers/GoogleProvider.ts +45 -0
  79. package/src/auth/providers/MSALProvider.ts +45 -0
  80. package/src/auth/providers/OktaProvider.ts +46 -0
  81. package/src/config.ts +14 -10
  82. package/src/context.ts +40 -17
  83. package/src/generated/generated.ts +37 -0
  84. package/src/generic/ResolverBase.ts +18 -13
  85. package/src/generic/RunViewResolver.ts +4 -4
  86. package/src/resolvers/AskSkipResolver.ts +3 -0
  87. package/src/resolvers/RunAIAgentResolver.ts +3 -3
  88. package/src/resolvers/RunAIPromptResolver.ts +2 -2
  89. package/src/types.ts +2 -4
@@ -0,0 +1,152 @@
1
+ import { AuthProviderConfig } from '@memberjunction/core';
2
+ import { IAuthProvider } from './IAuthProvider.js';
3
+ import { BaseAuthProvider } from './BaseAuthProvider.js';
4
+ import { MJGlobal } from '@memberjunction/global';
5
+
6
+ // Import providers to ensure they're registered
7
+ import './providers/Auth0Provider.js';
8
+ import './providers/MSALProvider.js';
9
+ import './providers/OktaProvider.js';
10
+ import './providers/CognitoProvider.js';
11
+ import './providers/GoogleProvider.js';
12
+
13
+ /**
14
+ * Factory and registry for managing authentication providers
15
+ * Combines provider creation and lifecycle management in a single class
16
+ */
17
+ export class AuthProviderFactory {
18
+ private static instance: AuthProviderFactory;
19
+ private providers: Map<string, IAuthProvider> = new Map();
20
+ private issuerCache: Map<string, IAuthProvider> = new Map();
21
+
22
+ private constructor() {}
23
+
24
+ /**
25
+ * Gets the singleton instance of the factory
26
+ */
27
+ static getInstance(): AuthProviderFactory {
28
+ if (!AuthProviderFactory.instance) {
29
+ AuthProviderFactory.instance = new AuthProviderFactory();
30
+ }
31
+ return AuthProviderFactory.instance;
32
+ }
33
+
34
+ /**
35
+ * Creates an authentication provider instance based on configuration
36
+ * Uses MJGlobal ClassFactory to instantiate the correct provider class
37
+ */
38
+ static createProvider(config: AuthProviderConfig): IAuthProvider {
39
+ try {
40
+ // Use MJGlobal ClassFactory to create the provider instance
41
+ // The provider type in config should match the key used in @RegisterClass
42
+ // The config is passed as a constructor parameter via the spread operator
43
+ const provider = MJGlobal.Instance.ClassFactory.CreateInstance<BaseAuthProvider>(
44
+ BaseAuthProvider,
45
+ config.type.toLowerCase(),
46
+ config
47
+ );
48
+
49
+ if (!provider) {
50
+ throw new Error(`No provider registered for type: ${config.type}`);
51
+ }
52
+
53
+ return provider;
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ throw new Error(`Failed to create authentication provider for type '${config.type}': ${message}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Registers a new authentication provider
62
+ */
63
+ register(provider: IAuthProvider): void {
64
+ if (!provider.validateConfig()) {
65
+ throw new Error(`Invalid configuration for provider: ${provider.name}`);
66
+ }
67
+
68
+ this.providers.set(provider.name, provider);
69
+
70
+ // Clear issuer cache when registering new provider
71
+ this.issuerCache.clear();
72
+
73
+ console.log(`Registered auth provider: ${provider.name} with issuer: ${provider.issuer}`);
74
+ }
75
+
76
+ /**
77
+ * Gets a provider by its issuer URL
78
+ */
79
+ getByIssuer(issuer: string): IAuthProvider | undefined {
80
+ // Check cache first
81
+ if (this.issuerCache.has(issuer)) {
82
+ return this.issuerCache.get(issuer);
83
+ }
84
+
85
+ // Search through providers
86
+ for (const provider of this.providers.values()) {
87
+ if (provider.matchesIssuer(issuer)) {
88
+ // Cache for future lookups
89
+ this.issuerCache.set(issuer, provider);
90
+ return provider;
91
+ }
92
+ }
93
+
94
+ return undefined;
95
+ }
96
+
97
+ /**
98
+ * Gets a provider by its name
99
+ */
100
+ getByName(name: string): IAuthProvider | undefined {
101
+ return this.providers.get(name);
102
+ }
103
+
104
+ /**
105
+ * Gets all registered providers
106
+ */
107
+ getAllProviders(): IAuthProvider[] {
108
+ return Array.from(this.providers.values());
109
+ }
110
+
111
+ /**
112
+ * Checks if any providers are registered
113
+ */
114
+ hasProviders(): boolean {
115
+ return this.providers.size > 0;
116
+ }
117
+
118
+ /**
119
+ * Clears all registered providers (useful for testing)
120
+ */
121
+ clear(): void {
122
+ this.providers.clear();
123
+ this.issuerCache.clear();
124
+ }
125
+
126
+ /**
127
+ * Gets all registered provider types from the ClassFactory
128
+ */
129
+ static getRegisteredProviderTypes(): string[] {
130
+ // Get all registrations for BaseAuthProvider from ClassFactory
131
+ const registrations = MJGlobal.Instance.ClassFactory.GetAllRegistrations(BaseAuthProvider);
132
+ // Extract unique keys (provider types) from registrations
133
+ const providerTypes = registrations
134
+ .map(reg => reg.Key)
135
+ .filter((key): key is string => key !== null && key !== undefined);
136
+ // Return unique provider types
137
+ return Array.from(new Set(providerTypes));
138
+ }
139
+
140
+ /**
141
+ * Checks if a provider type is registered
142
+ */
143
+ static isProviderTypeRegistered(type: string): boolean {
144
+ try {
145
+ // Try to get the registration for this specific type
146
+ const registration = MJGlobal.Instance.ClassFactory.GetRegistration(BaseAuthProvider, type.toLowerCase());
147
+ return registration !== null && registration !== undefined;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,71 @@
1
+ import { JwtHeader, JwtPayload, SigningKeyCallback } from 'jsonwebtoken';
2
+ import jwksClient from 'jwks-rsa';
3
+ import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
4
+ import { IAuthProvider } from './IAuthProvider.js';
5
+
6
+ /**
7
+ * Base implementation of IAuthProvider with common functionality
8
+ * Concrete providers should extend this class and use @RegisterClass decorator
9
+ * with BaseAuthProvider as the base class
10
+ */
11
+ export abstract class BaseAuthProvider implements IAuthProvider {
12
+ name: string;
13
+ issuer: string;
14
+ audience: string;
15
+ jwksUri: string;
16
+ protected config: AuthProviderConfig;
17
+ protected jwksClient: jwksClient.JwksClient;
18
+
19
+ constructor(config: AuthProviderConfig) {
20
+ this.config = config;
21
+ this.name = config.name;
22
+ this.issuer = config.issuer;
23
+ this.audience = config.audience;
24
+ this.jwksUri = config.jwksUri;
25
+
26
+ // Initialize JWKS client
27
+ this.jwksClient = jwksClient({
28
+ jwksUri: this.jwksUri,
29
+ cache: true,
30
+ cacheMaxEntries: 5,
31
+ cacheMaxAge: 600000 // 10 minutes
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Validates that required configuration is present
37
+ */
38
+ validateConfig(): boolean {
39
+ return !!(this.name && this.issuer && this.audience && this.jwksUri);
40
+ }
41
+
42
+ /**
43
+ * Gets the signing key for token verification
44
+ */
45
+ getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void {
46
+ this.jwksClient.getSigningKey(header.kid)
47
+ .then((key) => {
48
+ const signingKey = 'publicKey' in key ? key.publicKey : key.rsaPublicKey;
49
+ callback(null, signingKey);
50
+ })
51
+ .catch((err) => {
52
+ console.error(`Error getting signing key for provider ${this.name}:`, err);
53
+ callback(err);
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Checks if a given issuer URL belongs to this provider
59
+ */
60
+ matchesIssuer(issuer: string): boolean {
61
+ // Handle trailing slashes and case sensitivity
62
+ const normalizedIssuer = issuer.toLowerCase().replace(/\/$/, '');
63
+ const normalizedProviderIssuer = this.issuer.toLowerCase().replace(/\/$/, '');
64
+ return normalizedIssuer === normalizedProviderIssuer;
65
+ }
66
+
67
+ /**
68
+ * Abstract method for extracting user info - must be implemented by each provider
69
+ */
70
+ abstract extractUserInfo(payload: JwtPayload): AuthUserInfo;
71
+ }
@@ -0,0 +1,49 @@
1
+ import { JwtHeader, JwtPayload, SigningKeyCallback } from 'jsonwebtoken';
2
+ import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
3
+
4
+ /**
5
+ * Interface for authentication providers in MemberJunction
6
+ * Enables support for any OAuth 2.0/OIDC compliant provider
7
+ */
8
+ export interface IAuthProvider {
9
+ /**
10
+ * Unique name identifier for this provider
11
+ */
12
+ name: string;
13
+
14
+ /**
15
+ * The issuer URL for this provider (must match the 'iss' claim in tokens)
16
+ */
17
+ issuer: string;
18
+
19
+ /**
20
+ * The expected audience for tokens from this provider
21
+ */
22
+ audience: string;
23
+
24
+ /**
25
+ * The JWKS endpoint URL for retrieving signing keys
26
+ */
27
+ jwksUri: string;
28
+
29
+ /**
30
+ * Validates that the provider configuration is complete and valid
31
+ */
32
+ validateConfig(): boolean;
33
+
34
+ /**
35
+ * Gets the signing key for token verification
36
+ */
37
+ getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void;
38
+
39
+ /**
40
+ * Extracts user information from the JWT payload
41
+ * Different providers use different claim names
42
+ */
43
+ extractUserInfo(payload: JwtPayload): AuthUserInfo;
44
+
45
+ /**
46
+ * Checks if a given issuer URL belongs to this provider
47
+ */
48
+ matchesIssuer(issuer: string): boolean;
49
+ }
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import { AuthProviderFactory } from '../AuthProviderFactory';
3
+ import { IAuthProvider } from '../IAuthProvider';
4
+ import { initializeAuthProviders } from '../initializeProviders';
5
+
6
+ /**
7
+ * Test suite for backward compatibility of the new auth provider system
8
+ */
9
+ describe('Authentication Provider Backward Compatibility', () => {
10
+ let factory: AuthProviderFactory;
11
+
12
+ beforeEach(() => {
13
+ factory = AuthProviderFactory.getInstance();
14
+ factory.clear();
15
+ });
16
+
17
+ afterEach(() => {
18
+ factory.clear();
19
+ });
20
+
21
+ describe('Legacy Configuration Support', () => {
22
+ it('should create MSAL provider from legacy config', () => {
23
+ // Simulate legacy environment variables
24
+ process.env.TENANT_ID = 'test-tenant-id';
25
+ process.env.WEB_CLIENT_ID = 'test-client-id';
26
+
27
+ // Initialize with legacy config
28
+ initializeAuthProviders();
29
+
30
+ // Check that MSAL provider was created
31
+ const msalProvider = factory.getByName('msal');
32
+ expect(msalProvider).toBeDefined();
33
+ expect(msalProvider?.issuer).toContain('test-tenant-id');
34
+ expect(msalProvider?.audience).toBe('test-client-id');
35
+ });
36
+
37
+ it('should create Auth0 provider from legacy config', () => {
38
+ // Simulate legacy environment variables
39
+ process.env.AUTH0_DOMAIN = 'test.auth0.com';
40
+ process.env.AUTH0_CLIENT_ID = 'auth0-client-id';
41
+ process.env.AUTH0_CLIENT_SECRET = 'auth0-secret';
42
+
43
+ // Initialize with legacy config
44
+ initializeAuthProviders();
45
+
46
+ // Check that Auth0 provider was created
47
+ const auth0Provider = factory.getByName('auth0');
48
+ expect(auth0Provider).toBeDefined();
49
+ expect(auth0Provider?.issuer).toBe('https://test.auth0.com/');
50
+ expect(auth0Provider?.audience).toBe('auth0-client-id');
51
+ });
52
+ });
53
+
54
+
55
+ describe('Provider Registry Functionality', () => {
56
+ it('should find providers by issuer with different formats', () => {
57
+ // Register a test provider
58
+ const testProvider = {
59
+ name: 'test',
60
+ issuer: 'https://test.provider.com/oauth2',
61
+ audience: 'test-audience',
62
+ jwksUri: 'https://test.provider.com/.well-known/jwks.json',
63
+ validateConfig: () => true,
64
+ getSigningKey: jest.fn(),
65
+ extractUserInfo: jest.fn(),
66
+ matchesIssuer: (issuer: string) => {
67
+ const normalized = issuer.toLowerCase().replace(/\/$/, '');
68
+ return normalized === 'https://test.provider.com/oauth2';
69
+ }
70
+ } as IAuthProvider;
71
+
72
+ factory.register(testProvider);
73
+
74
+ // Test with exact match
75
+ expect(factory.getByIssuer('https://test.provider.com/oauth2')).toBe(testProvider);
76
+
77
+ // Test with trailing slash
78
+ expect(factory.getByIssuer('https://test.provider.com/oauth2/')).toBe(testProvider);
79
+
80
+ // Test with different case
81
+ expect(factory.getByIssuer('https://TEST.PROVIDER.COM/oauth2')).toBe(testProvider);
82
+ });
83
+
84
+ it('should cache issuer lookups for performance', () => {
85
+ const testProvider = {
86
+ name: 'test',
87
+ issuer: 'https://test.provider.com',
88
+ audience: 'test',
89
+ jwksUri: 'https://test.provider.com/jwks',
90
+ validateConfig: () => true,
91
+ getSigningKey: jest.fn(),
92
+ extractUserInfo: jest.fn(),
93
+ matchesIssuer: jest.fn((issuer: string): boolean => issuer === 'https://test.provider.com')
94
+ } as IAuthProvider;
95
+
96
+ factory.register(testProvider);
97
+
98
+ // First lookup
99
+ factory.getByIssuer('https://test.provider.com');
100
+ expect(testProvider.matchesIssuer).toHaveBeenCalledTimes(1);
101
+
102
+ // Second lookup should use cache
103
+ factory.getByIssuer('https://test.provider.com');
104
+ expect(testProvider.matchesIssuer).toHaveBeenCalledTimes(1);
105
+ });
106
+ });
107
+
108
+ describe('User Info Extraction', () => {
109
+ it('should extract user info from different token formats', () => {
110
+ // Test MSAL token format
111
+ const msalPayload = {
112
+ iss: 'https://login.microsoftonline.com/tenant/v2.0',
113
+ email: 'user@example.com',
114
+ given_name: 'John',
115
+ family_name: 'Doe',
116
+ name: 'John Doe',
117
+ preferred_username: 'john.doe@example.com'
118
+ };
119
+
120
+ // Test Auth0 token format
121
+ const auth0Payload = {
122
+ iss: 'https://test.auth0.com/',
123
+ email: 'user@example.com',
124
+ given_name: 'Jane',
125
+ family_name: 'Smith',
126
+ name: 'Jane Smith'
127
+ };
128
+
129
+ // Test Okta token format
130
+ const oktaPayload = {
131
+ iss: 'https://test.okta.com/oauth2/default',
132
+ email: 'user@example.com',
133
+ given_name: 'Bob',
134
+ family_name: 'Johnson',
135
+ name: 'Bob Johnson',
136
+ preferred_username: 'bob.johnson'
137
+ };
138
+
139
+ // Initialize providers
140
+ initializeAuthProviders();
141
+
142
+ // Test extraction for each provider type
143
+ const msalProvider = factory.getByIssuer(msalPayload.iss);
144
+ if (msalProvider) {
145
+ const msalUserInfo = msalProvider.extractUserInfo(msalPayload);
146
+ expect(msalUserInfo.email).toBe('user@example.com');
147
+ expect(msalUserInfo.firstName).toBe('John');
148
+ expect(msalUserInfo.lastName).toBe('Doe');
149
+ }
150
+
151
+ const auth0Provider = factory.getByIssuer(auth0Payload.iss);
152
+ if (auth0Provider) {
153
+ const auth0UserInfo = auth0Provider.extractUserInfo(auth0Payload);
154
+ expect(auth0UserInfo.email).toBe('user@example.com');
155
+ expect(auth0UserInfo.firstName).toBe('Jane');
156
+ expect(auth0UserInfo.lastName).toBe('Smith');
157
+ }
158
+ });
159
+ });
160
+
161
+ describe('Error Handling', () => {
162
+ it('should handle missing provider gracefully', () => {
163
+ const unknownIssuer = 'https://unknown.provider.com';
164
+ const provider = factory.getByIssuer(unknownIssuer);
165
+ expect(provider).toBeUndefined();
166
+ });
167
+
168
+ it('should validate provider configuration', () => {
169
+ const invalidProvider = {
170
+ name: 'invalid',
171
+ issuer: '', // Invalid: empty issuer
172
+ audience: 'test',
173
+ jwksUri: 'https://test.com/jwks',
174
+ validateConfig: () => false,
175
+ getSigningKey: jest.fn(),
176
+ extractUserInfo: jest.fn(),
177
+ matchesIssuer: jest.fn()
178
+ } as IAuthProvider;
179
+
180
+ expect(() => factory.register(invalidProvider)).toThrow();
181
+ });
182
+ });
183
+ });
package/src/auth/index.ts CHANGED
@@ -1,33 +1,28 @@
1
- import { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
2
- import jwksClient from 'jwks-rsa';
3
- import { auth0Domain, auth0WebClientID, configInfo, tenantID, webClientID } from '../config.js';
1
+ import { JwtHeader, SigningKeyCallback, JwtPayload } from 'jsonwebtoken';
2
+ import { configInfo } from '../config.js';
4
3
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
4
  import sql from 'mssql';
6
5
  import { Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
7
6
  import { NewUserBase } from './newUsers.js';
8
7
  import { MJGlobal } from '@memberjunction/global';
9
8
  import { UserEntity, UserEntityType } from '@memberjunction/core-entities';
9
+ import { AuthProviderFactory } from './AuthProviderFactory.js';
10
+ import { initializeAuthProviders } from './initializeProviders.js';
10
11
 
11
12
  export { TokenExpiredError } from './tokenExpiredError.js';
12
-
13
- const missingAzureConfig = !tenantID || !webClientID;
14
- const missingAuth0Config = !auth0Domain || !auth0WebClientID;
13
+ export { IAuthProvider } from './IAuthProvider.js';
14
+ export { AuthProviderFactory } from './AuthProviderFactory.js';
15
15
 
16
16
  // This is a hard-coded forever constant due to internal migrations
17
17
  const SYSTEM_USER_ID = 'ecafccec-6a37-ef11-86d4-000d3a4e707e';
18
18
 
19
19
  class MissingAuthError extends Error {
20
20
  constructor() {
21
- super('Could not find authentication configuration for either MSAL or Auth0 in the server environment variables.');
21
+ super('No authentication providers configured. Please configure at least one auth provider in mj.config.cjs');
22
22
  this.name = 'MissingAuthError';
23
23
  }
24
24
  }
25
25
 
26
- const issuers = {
27
- azure: `https://login.microsoftonline.com/${tenantID}/v2.0`,
28
- auth0: `https://${auth0Domain}/`,
29
- };
30
-
31
26
  const refreshUserCache = async (dataSource?: sql.ConnectionPool) => {
32
27
  const startTime: number = Date.now();
33
28
  await UserCache.Instance.Refresh(dataSource);
@@ -51,16 +46,40 @@ const refreshUserCache = async (dataSource?: sql.ConnectionPool) => {
51
46
  );
52
47
  };
53
48
 
54
- export const validationOptions = {
55
- [issuers.auth0]: {
56
- audience: auth0WebClientID,
57
- jwksUri: `https://${auth0Domain}/.well-known/jwks.json`,
49
+ /**
50
+ * Gets validation options for a specific issuer
51
+ * This maintains backward compatibility with the old structure
52
+ */
53
+ export const getValidationOptions = (issuer: string): { audience: string; jwksUri: string } | undefined => {
54
+ const factory = AuthProviderFactory.getInstance();
55
+ const provider = factory.getByIssuer(issuer);
56
+
57
+ if (!provider) {
58
+ return undefined;
59
+ }
60
+
61
+ return {
62
+ audience: provider.audience,
63
+ jwksUri: provider.jwksUri
64
+ };
65
+ };
66
+
67
+ /**
68
+ * Backward compatible validationOptions object
69
+ * @deprecated Use getValidationOptions() or AuthProviderRegistry instead
70
+ */
71
+ export const validationOptions: Record<string, { audience: string; jwksUri: string }> = new Proxy({}, {
72
+ get: (target, prop: string) => {
73
+ return getValidationOptions(prop);
58
74
  },
59
- [issuers.azure]: {
60
- audience: webClientID,
61
- jwksUri: `https://login.microsoftonline.com/${tenantID}/discovery/v2.0/keys`,
75
+ has: (target, prop: string) => {
76
+ return getValidationOptions(prop) !== undefined;
62
77
  },
63
- };
78
+ ownKeys: () => {
79
+ const factory = AuthProviderFactory.getInstance();
80
+ return factory.getAllProviders().map(p => p.issuer);
81
+ }
82
+ });
64
83
 
65
84
  export class UserPayload {
66
85
  aio?: string;
@@ -78,31 +97,77 @@ export class UserPayload {
78
97
  tid?: string;
79
98
  uti?: string;
80
99
  ver?: string;
81
- // what about an array of roles???
100
+ email?: string;
101
+ given_name?: string;
102
+ family_name?: string;
103
+ [key: string]: unknown; // Allow additional claims
82
104
  }
83
105
 
106
+ /**
107
+ * Gets signing keys for JWT validation
108
+ */
84
109
  export const getSigningKeys = (issuer: string) => (header: JwtHeader, cb: SigningKeyCallback) => {
85
- if (!validationOptions[issuer]) {
86
- throw new Error(`No validation options found for issuer ${issuer}`);
110
+ const factory = AuthProviderFactory.getInstance();
111
+
112
+ // Initialize providers if not already done
113
+ if (!factory.hasProviders()) {
114
+ initializeAuthProviders();
87
115
  }
88
116
 
89
- const jwksUri = validationOptions[issuer].jwksUri;
90
- if (missingAuth0Config && missingAzureConfig) {
91
- throw new MissingAuthError();
117
+ const provider = factory.getByIssuer(issuer);
118
+
119
+ if (!provider) {
120
+ // Check if we have any providers at all
121
+ if (!factory.hasProviders()) {
122
+ throw new MissingAuthError();
123
+ }
124
+ throw new Error(`No authentication provider found for issuer: ${issuer}`);
92
125
  }
93
- if (missingAuth0Config) {
94
- console.warn('Auth0 configuration not found in environment variables');
126
+
127
+ provider.getSigningKey(header, cb);
128
+ };
129
+
130
+ /**
131
+ * Extracts user information from JWT payload using the appropriate provider
132
+ */
133
+ export const extractUserInfoFromPayload = (payload: JwtPayload): {
134
+ email?: string;
135
+ firstName?: string;
136
+ lastName?: string;
137
+ fullName?: string;
138
+ preferredUsername?: string;
139
+ } => {
140
+ const factory = AuthProviderFactory.getInstance();
141
+ const issuer = payload.iss;
142
+
143
+ if (!issuer) {
144
+ // Fallback to default extraction
145
+ const preferredUsername = payload.preferred_username as string | undefined;
146
+ return {
147
+ email: payload.email as string | undefined || preferredUsername,
148
+ firstName: payload.given_name as string | undefined,
149
+ lastName: payload.family_name as string | undefined,
150
+ fullName: payload.name as string | undefined,
151
+ preferredUsername
152
+ };
95
153
  }
96
- if (missingAzureConfig) {
97
- console.warn('MSAL configuration not found in environment variables');
154
+
155
+ const provider = factory.getByIssuer(issuer);
156
+
157
+ if (!provider) {
158
+ // Fallback to default extraction
159
+ const fullName = payload.name as string | undefined;
160
+ const preferredUsername = payload.preferred_username as string | undefined;
161
+ return {
162
+ email: payload.email as string | undefined || preferredUsername,
163
+ firstName: payload.given_name as string | undefined || fullName?.split(' ')[0],
164
+ lastName: payload.family_name as string | undefined || fullName?.split(' ')[1] || fullName?.split(' ')[0],
165
+ fullName,
166
+ preferredUsername
167
+ };
98
168
  }
99
169
 
100
- jwksClient({ jwksUri })
101
- .getSigningKey(header.kid)
102
- .then((key) => {
103
- cb(null, 'publicKey' in key ? key.publicKey : key.rsaPublicKey);
104
- })
105
- .catch((err) => console.error(err));
170
+ return provider.extractUserInfo(payload);
106
171
  };
107
172
 
108
173
  export const getSystemUser = async (dataSource?: sql.ConnectionPool, attemptCacheUpdateIfNeeded: boolean = true): Promise<UserInfo> => {
@@ -200,3 +265,6 @@ export const verifyUserRecord = async (
200
265
 
201
266
  return user;
202
267
  };
268
+
269
+ // Initialize providers on module load
270
+ initializeAuthProviders();
@@ -0,0 +1,31 @@
1
+ import { configInfo } from '../config.js';
2
+ import { AuthProviderConfig, LogError, LogStatus } from '@memberjunction/core';
3
+ import { AuthProviderFactory } from './AuthProviderFactory.js';
4
+
5
+ /**
6
+ * Initialize authentication providers from configuration
7
+ */
8
+ export function initializeAuthProviders(): void {
9
+ const factory = AuthProviderFactory.getInstance();
10
+
11
+ // Clear any existing providers
12
+ factory.clear();
13
+
14
+ // Initialize providers from authProviders config
15
+ if (configInfo.authProviders && configInfo.authProviders.length > 0) {
16
+ for (const providerConfig of configInfo.authProviders) {
17
+ try {
18
+ const provider = AuthProviderFactory.createProvider(providerConfig as AuthProviderConfig);
19
+ factory.register(provider);
20
+ LogStatus(`Registered auth provider: ${provider.name} (type: ${providerConfig.type})`);
21
+ } catch (error) {
22
+ LogError(`Failed to initialize auth provider ${providerConfig.name}: ${error}`);
23
+ }
24
+ }
25
+ }
26
+
27
+ // Validate we have at least one provider
28
+ if (!factory.hasProviders()) {
29
+ LogError('No authentication providers configured. Please configure authProviders array in mj.config.cjs');
30
+ }
31
+ }