@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.
- package/dist/auth/AuthProviderFactory.d.ts +24 -0
- package/dist/auth/AuthProviderFactory.d.ts.map +1 -0
- package/dist/auth/AuthProviderFactory.js +82 -0
- package/dist/auth/AuthProviderFactory.js.map +1 -0
- package/dist/auth/BaseAuthProvider.d.ts +18 -0
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -0
- package/dist/auth/BaseAuthProvider.js +42 -0
- package/dist/auth/BaseAuthProvider.js.map +1 -0
- package/dist/auth/IAuthProvider.d.ts +13 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -0
- package/dist/auth/IAuthProvider.js +2 -0
- package/dist/auth/IAuthProvider.js.map +1 -0
- package/dist/auth/__tests__/backward-compatibility.test.d.ts +2 -0
- package/dist/auth/__tests__/backward-compatibility.test.d.ts.map +1 -0
- package/dist/auth/__tests__/backward-compatibility.test.js +135 -0
- package/dist/auth/__tests__/backward-compatibility.test.js.map +1 -0
- package/dist/auth/index.d.ts +22 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -32
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +2 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -0
- package/dist/auth/initializeProviders.js +23 -0
- package/dist/auth/initializeProviders.js.map +1 -0
- package/dist/auth/providers/Auth0Provider.d.ts +9 -0
- package/dist/auth/providers/Auth0Provider.d.ts.map +1 -0
- package/dist/auth/providers/Auth0Provider.js +42 -0
- package/dist/auth/providers/Auth0Provider.js.map +1 -0
- package/dist/auth/providers/CognitoProvider.d.ts +9 -0
- package/dist/auth/providers/CognitoProvider.d.ts.map +1 -0
- package/dist/auth/providers/CognitoProvider.js +46 -0
- package/dist/auth/providers/CognitoProvider.js.map +1 -0
- package/dist/auth/providers/GoogleProvider.d.ts +9 -0
- package/dist/auth/providers/GoogleProvider.d.ts.map +1 -0
- package/dist/auth/providers/GoogleProvider.js +41 -0
- package/dist/auth/providers/GoogleProvider.js.map +1 -0
- package/dist/auth/providers/MSALProvider.d.ts +9 -0
- package/dist/auth/providers/MSALProvider.d.ts.map +1 -0
- package/dist/auth/providers/MSALProvider.js +42 -0
- package/dist/auth/providers/MSALProvider.js.map +1 -0
- package/dist/auth/providers/OktaProvider.d.ts +9 -0
- package/dist/auth/providers/OktaProvider.d.ts.map +1 -0
- package/dist/auth/providers/OktaProvider.js +42 -0
- package/dist/auth/providers/OktaProvider.js.map +1 -0
- package/dist/config.d.ts +97 -21
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -6
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -17
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +12 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +61 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +2 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +5 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +3 -0
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +39 -39
- package/src/auth/AuthProviderFactory.ts +152 -0
- package/src/auth/BaseAuthProvider.ts +71 -0
- package/src/auth/IAuthProvider.ts +49 -0
- package/src/auth/__tests__/backward-compatibility.test.ts +183 -0
- package/src/auth/index.ts +104 -36
- package/src/auth/initializeProviders.ts +31 -0
- package/src/auth/providers/Auth0Provider.ts +45 -0
- package/src/auth/providers/CognitoProvider.ts +50 -0
- package/src/auth/providers/GoogleProvider.ts +45 -0
- package/src/auth/providers/MSALProvider.ts +45 -0
- package/src/auth/providers/OktaProvider.ts +46 -0
- package/src/config.ts +14 -10
- package/src/context.ts +40 -17
- package/src/generated/generated.ts +37 -0
- package/src/generic/ResolverBase.ts +18 -13
- package/src/generic/RunViewResolver.ts +4 -4
- package/src/resolvers/AskSkipResolver.ts +3 -0
- package/src/resolvers/RunAIAgentResolver.ts +3 -3
- package/src/resolvers/RunAIPromptResolver.ts +2 -2
- 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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
+
}
|