@payez/next-mvp 3.7.0 → 3.7.1
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/README.md +782 -782
- package/dist/lib/idp-client-config.js +75 -7
- package/dist/lib/nextauth-secret.js +1 -6
- package/package.json +1 -1
- package/src/lib/idp-client-config.ts +85 -7
- package/src/lib/nextauth-secret.ts +1 -7
|
@@ -128,19 +128,20 @@ async function getIDPClientConfig(forceRefresh = false) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
// Layer 3: Fetch from IDP
|
|
131
|
+
const internalIdpUrl = process.env.INTERNAL_IDP_URL;
|
|
131
132
|
const idpUrl = process.env.IDP_URL;
|
|
132
133
|
const clientIdStr = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID;
|
|
133
|
-
if (!idpUrl) {
|
|
134
|
-
throw new Error('[IDP_CONFIG] FATAL: IDP_URL must be set');
|
|
135
|
-
}
|
|
136
134
|
if (!clientIdStr) {
|
|
137
135
|
throw new Error('[IDP_CONFIG] FATAL: CLIENT_ID or NEXT_PUBLIC_CLIENT_ID must be set');
|
|
138
136
|
}
|
|
139
|
-
if (!
|
|
140
|
-
throw new Error('[IDP_CONFIG] FATAL:
|
|
137
|
+
if (!internalIdpUrl && !idpUrl) {
|
|
138
|
+
throw new Error('[IDP_CONFIG] FATAL: INTERNAL_IDP_URL or IDP_URL must be set');
|
|
141
139
|
}
|
|
142
140
|
// Start fetch and store promise so concurrent callers wait for same result
|
|
143
|
-
|
|
141
|
+
const fetcher = internalIdpUrl
|
|
142
|
+
? fetchConfigFromInternalIDP(internalIdpUrl, clientIdStr)
|
|
143
|
+
: fetchConfigFromIDP(idpUrl, clientIdStr);
|
|
144
|
+
pendingFetch = fetcher
|
|
144
145
|
.then(async (config) => {
|
|
145
146
|
// Cache with TTL from response (default 5 minutes)
|
|
146
147
|
cachedConfig = config;
|
|
@@ -184,6 +185,74 @@ function getEnabledProviders(config) {
|
|
|
184
185
|
// ============================================================================
|
|
185
186
|
// Internal Functions
|
|
186
187
|
// ============================================================================
|
|
188
|
+
async function fetchConfigFromInternalIDP(internalIdpUrl, clientIdStr) {
|
|
189
|
+
const containersKey = process.env.CONTAINERS_KEY;
|
|
190
|
+
if (!containersKey) {
|
|
191
|
+
throw new Error('[IDP_CONFIG] FATAL: CONTAINERS_KEY is required when using INTERNAL_IDP_URL');
|
|
192
|
+
}
|
|
193
|
+
const url = `${internalIdpUrl.replace(/\/$/, '')}/InternalClientConfig/${encodeURIComponent(clientIdStr)}`;
|
|
194
|
+
console.log(`[IDP_CONFIG] Fetching config from internal IDP: ${url}`);
|
|
195
|
+
const resp = await fetch(url, {
|
|
196
|
+
method: 'GET',
|
|
197
|
+
headers: {
|
|
198
|
+
'Accept': 'application/json',
|
|
199
|
+
'Authorization': `Secret ${containersKey}`,
|
|
200
|
+
},
|
|
201
|
+
cache: 'no-store'
|
|
202
|
+
});
|
|
203
|
+
if (!resp.ok) {
|
|
204
|
+
const txt = await resp.text().catch(() => 'Unknown error');
|
|
205
|
+
throw new Error(`[IDP_CONFIG] FATAL: Internal IDP returned ${resp.status} - ${txt}`);
|
|
206
|
+
}
|
|
207
|
+
const body = await resp.json().catch(() => null);
|
|
208
|
+
if (!body) {
|
|
209
|
+
throw new Error('[IDP_CONFIG] FATAL: Internal IDP returned empty or invalid JSON');
|
|
210
|
+
}
|
|
211
|
+
const configData = body?.data ?? body;
|
|
212
|
+
const rawClientId = configData.clientId ?? configData.client_id;
|
|
213
|
+
if (rawClientId === undefined || rawClientId === null) {
|
|
214
|
+
throw new Error(`[IDP_CONFIG] FATAL: Internal IDP response missing clientId. Got: ${JSON.stringify(Object.keys(configData))}`);
|
|
215
|
+
}
|
|
216
|
+
const config = {
|
|
217
|
+
clientId: String(rawClientId),
|
|
218
|
+
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
219
|
+
nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
|
|
220
|
+
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
221
|
+
oauthProviders: (configData.oauthProviders ?? configData.oauth_providers ?? []).map((p) => ({
|
|
222
|
+
provider: p.provider ?? '',
|
|
223
|
+
enabled: p.enabled ?? false,
|
|
224
|
+
clientId: p.clientId ?? p.client_id ?? '',
|
|
225
|
+
clientSecret: p.clientSecret ?? p.client_secret ?? '',
|
|
226
|
+
scopes: p.scopes,
|
|
227
|
+
additionalParams: p.additionalParams ?? p.additional_params
|
|
228
|
+
})),
|
|
229
|
+
authSettings: {
|
|
230
|
+
require2FA: configData.authSettings?.require2FA ?? configData.auth_settings?.require_2fa ?? true,
|
|
231
|
+
allowed2FAMethods: configData.authSettings?.allowed2FAMethods ?? configData.auth_settings?.allowed_2fa_methods ?? ['email', 'sms'],
|
|
232
|
+
mfaGracePeriodHours: configData.authSettings?.mfaGracePeriodHours ?? configData.auth_settings?.mfa_grace_period_hours ?? 24,
|
|
233
|
+
mfaRememberDeviceDays: configData.authSettings?.mfaRememberDeviceDays ?? configData.auth_settings?.mfa_remember_device_days ?? 30,
|
|
234
|
+
sessionTimeoutMinutes: configData.authSettings?.sessionTimeoutMinutes ?? configData.auth_settings?.session_timeout_minutes ?? 60,
|
|
235
|
+
idleTimeoutMinutes: configData.authSettings?.idleTimeoutMinutes ?? configData.auth_settings?.idle_timeout_minutes ?? 15,
|
|
236
|
+
allowRememberMe: configData.authSettings?.allowRememberMe ?? configData.auth_settings?.allow_remember_me ?? true,
|
|
237
|
+
rememberMeDays: configData.authSettings?.rememberMeDays ?? configData.auth_settings?.remember_me_days ?? 30,
|
|
238
|
+
lockoutThreshold: configData.authSettings?.lockoutThreshold ?? configData.auth_settings?.lockout_threshold ?? 5,
|
|
239
|
+
lockoutDurationMinutes: configData.authSettings?.lockoutDurationMinutes ?? configData.auth_settings?.lockout_duration_minutes ?? 15
|
|
240
|
+
},
|
|
241
|
+
branding: {
|
|
242
|
+
theme: configData.branding?.theme,
|
|
243
|
+
primaryColor: configData.branding?.primaryColor ?? configData.branding?.primary_color,
|
|
244
|
+
secondaryColor: configData.branding?.secondaryColor ?? configData.branding?.secondary_color,
|
|
245
|
+
logoUrl: configData.branding?.logoUrl ?? configData.branding?.logo_url
|
|
246
|
+
},
|
|
247
|
+
baseClientUrl: configData.baseClientUrl ?? configData.base_client_url ?? configData.BaseClientUrl
|
|
248
|
+
};
|
|
249
|
+
if (!config.nextAuthSecret) {
|
|
250
|
+
throw new Error('[IDP_CONFIG] FATAL: Internal IDP did not return nextAuthSecret');
|
|
251
|
+
}
|
|
252
|
+
console.log(`[IDP_CONFIG] Internal IDP config loaded for ${clientIdStr}`);
|
|
253
|
+
consecutiveFailures = 0;
|
|
254
|
+
return config;
|
|
255
|
+
}
|
|
187
256
|
async function fetchConfigFromIDP(idpUrl, clientIdStr) {
|
|
188
257
|
// =========================================================================
|
|
189
258
|
// Circuit Breaker Check
|
|
@@ -223,7 +292,6 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
|
|
|
223
292
|
subject: clientIdStr,
|
|
224
293
|
audience: 'urn:payez:externalauth:clientconfig',
|
|
225
294
|
expires_in: 60,
|
|
226
|
-
client_secret: process.env.PAYEZ_CLIENT_SECRET,
|
|
227
295
|
};
|
|
228
296
|
const signingResp = await fetch(signingUrl, {
|
|
229
297
|
method: 'POST',
|
|
@@ -31,18 +31,13 @@ async function resolveNextAuthSecret() {
|
|
|
31
31
|
const clientIdStr = process.env.CLIENT_ID;
|
|
32
32
|
if (!clientIdStr || clientIdStr.trim() === '')
|
|
33
33
|
throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
|
-
if (!process.env.PAYEZ_CLIENT_SECRET) {
|
|
35
|
-
throw new Error('[NEXTAUTH-SECRET] FATAL: PAYEZ_CLIENT_SECRET is required. Inject via container env or K8s Secret — never .env files.');
|
|
36
|
-
}
|
|
37
34
|
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
38
35
|
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
39
|
-
// Client ID passed via X-Client-Id header, not query string
|
|
40
36
|
const signingPayload = {
|
|
41
37
|
issuer: clientIdStr,
|
|
42
38
|
subject: clientIdStr,
|
|
43
39
|
audience: 'urn:payez:externalauth:nextauthsecret',
|
|
44
40
|
expires_in: 60,
|
|
45
|
-
client_secret: process.env.PAYEZ_CLIENT_SECRET,
|
|
46
41
|
};
|
|
47
42
|
const signingResp = await fetch(signingUrl.toString(), {
|
|
48
43
|
method: 'POST',
|
|
@@ -79,7 +74,7 @@ async function resolveNextAuthSecret() {
|
|
|
79
74
|
'X-Client-Id': clientIdStr,
|
|
80
75
|
'X-Correlation-Id': (0, crypto_1.randomUUID)().replace(/-/g, ''),
|
|
81
76
|
},
|
|
82
|
-
body: JSON.stringify({ client_assertion
|
|
77
|
+
body: JSON.stringify({ client_assertion }),
|
|
83
78
|
cache: 'no-store'
|
|
84
79
|
});
|
|
85
80
|
if (!proxyResp.ok) {
|
package/package.json
CHANGED
|
@@ -187,21 +187,22 @@ export async function getIDPClientConfig(forceRefresh: boolean = false): Promise
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// Layer 3: Fetch from IDP
|
|
190
|
+
const internalIdpUrl = process.env.INTERNAL_IDP_URL;
|
|
190
191
|
const idpUrl = process.env.IDP_URL;
|
|
191
192
|
const clientIdStr = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID;
|
|
192
193
|
|
|
193
|
-
if (!idpUrl) {
|
|
194
|
-
throw new Error('[IDP_CONFIG] FATAL: IDP_URL must be set');
|
|
195
|
-
}
|
|
196
194
|
if (!clientIdStr) {
|
|
197
195
|
throw new Error('[IDP_CONFIG] FATAL: CLIENT_ID or NEXT_PUBLIC_CLIENT_ID must be set');
|
|
198
196
|
}
|
|
199
|
-
if (!
|
|
200
|
-
throw new Error('[IDP_CONFIG] FATAL:
|
|
197
|
+
if (!internalIdpUrl && !idpUrl) {
|
|
198
|
+
throw new Error('[IDP_CONFIG] FATAL: INTERNAL_IDP_URL or IDP_URL must be set');
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
// Start fetch and store promise so concurrent callers wait for same result
|
|
204
|
-
|
|
202
|
+
const fetcher = internalIdpUrl
|
|
203
|
+
? fetchConfigFromInternalIDP(internalIdpUrl, clientIdStr)
|
|
204
|
+
: fetchConfigFromIDP(idpUrl!, clientIdStr);
|
|
205
|
+
pendingFetch = fetcher
|
|
205
206
|
.then(async config => {
|
|
206
207
|
// Cache with TTL from response (default 5 minutes)
|
|
207
208
|
cachedConfig = config;
|
|
@@ -253,6 +254,84 @@ export function getEnabledProviders(config: IDPClientConfig): OAuthProviderConfi
|
|
|
253
254
|
// Internal Functions
|
|
254
255
|
// ============================================================================
|
|
255
256
|
|
|
257
|
+
async function fetchConfigFromInternalIDP(internalIdpUrl: string, clientIdStr: string): Promise<IDPClientConfig> {
|
|
258
|
+
const containersKey = process.env.CONTAINERS_KEY;
|
|
259
|
+
if (!containersKey) {
|
|
260
|
+
throw new Error('[IDP_CONFIG] FATAL: CONTAINERS_KEY is required when using INTERNAL_IDP_URL');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const url = `${internalIdpUrl.replace(/\/$/, '')}/InternalClientConfig/${encodeURIComponent(clientIdStr)}`;
|
|
264
|
+
console.log(`[IDP_CONFIG] Fetching config from internal IDP: ${url}`);
|
|
265
|
+
|
|
266
|
+
const resp = await fetch(url, {
|
|
267
|
+
method: 'GET',
|
|
268
|
+
headers: {
|
|
269
|
+
'Accept': 'application/json',
|
|
270
|
+
'Authorization': `Secret ${containersKey}`,
|
|
271
|
+
},
|
|
272
|
+
cache: 'no-store'
|
|
273
|
+
} as RequestInit);
|
|
274
|
+
|
|
275
|
+
if (!resp.ok) {
|
|
276
|
+
const txt = await resp.text().catch(() => 'Unknown error');
|
|
277
|
+
throw new Error(`[IDP_CONFIG] FATAL: Internal IDP returned ${resp.status} - ${txt}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const body: any = await resp.json().catch(() => null);
|
|
281
|
+
if (!body) {
|
|
282
|
+
throw new Error('[IDP_CONFIG] FATAL: Internal IDP returned empty or invalid JSON');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const configData = body?.data ?? body;
|
|
286
|
+
|
|
287
|
+
const rawClientId = configData.clientId ?? configData.client_id;
|
|
288
|
+
if (rawClientId === undefined || rawClientId === null) {
|
|
289
|
+
throw new Error(`[IDP_CONFIG] FATAL: Internal IDP response missing clientId. Got: ${JSON.stringify(Object.keys(configData))}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const config: IDPClientConfig = {
|
|
293
|
+
clientId: String(rawClientId),
|
|
294
|
+
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
295
|
+
nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
|
|
296
|
+
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
297
|
+
oauthProviders: (configData.oauthProviders ?? configData.oauth_providers ?? []).map((p: any) => ({
|
|
298
|
+
provider: p.provider ?? '',
|
|
299
|
+
enabled: p.enabled ?? false,
|
|
300
|
+
clientId: p.clientId ?? p.client_id ?? '',
|
|
301
|
+
clientSecret: p.clientSecret ?? p.client_secret ?? '',
|
|
302
|
+
scopes: p.scopes,
|
|
303
|
+
additionalParams: p.additionalParams ?? p.additional_params
|
|
304
|
+
})),
|
|
305
|
+
authSettings: {
|
|
306
|
+
require2FA: configData.authSettings?.require2FA ?? configData.auth_settings?.require_2fa ?? true,
|
|
307
|
+
allowed2FAMethods: configData.authSettings?.allowed2FAMethods ?? configData.auth_settings?.allowed_2fa_methods ?? ['email', 'sms'],
|
|
308
|
+
mfaGracePeriodHours: configData.authSettings?.mfaGracePeriodHours ?? configData.auth_settings?.mfa_grace_period_hours ?? 24,
|
|
309
|
+
mfaRememberDeviceDays: configData.authSettings?.mfaRememberDeviceDays ?? configData.auth_settings?.mfa_remember_device_days ?? 30,
|
|
310
|
+
sessionTimeoutMinutes: configData.authSettings?.sessionTimeoutMinutes ?? configData.auth_settings?.session_timeout_minutes ?? 60,
|
|
311
|
+
idleTimeoutMinutes: configData.authSettings?.idleTimeoutMinutes ?? configData.auth_settings?.idle_timeout_minutes ?? 15,
|
|
312
|
+
allowRememberMe: configData.authSettings?.allowRememberMe ?? configData.auth_settings?.allow_remember_me ?? true,
|
|
313
|
+
rememberMeDays: configData.authSettings?.rememberMeDays ?? configData.auth_settings?.remember_me_days ?? 30,
|
|
314
|
+
lockoutThreshold: configData.authSettings?.lockoutThreshold ?? configData.auth_settings?.lockout_threshold ?? 5,
|
|
315
|
+
lockoutDurationMinutes: configData.authSettings?.lockoutDurationMinutes ?? configData.auth_settings?.lockout_duration_minutes ?? 15
|
|
316
|
+
},
|
|
317
|
+
branding: {
|
|
318
|
+
theme: configData.branding?.theme,
|
|
319
|
+
primaryColor: configData.branding?.primaryColor ?? configData.branding?.primary_color,
|
|
320
|
+
secondaryColor: configData.branding?.secondaryColor ?? configData.branding?.secondary_color,
|
|
321
|
+
logoUrl: configData.branding?.logoUrl ?? configData.branding?.logo_url
|
|
322
|
+
},
|
|
323
|
+
baseClientUrl: configData.baseClientUrl ?? configData.base_client_url ?? configData.BaseClientUrl
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (!config.nextAuthSecret) {
|
|
327
|
+
throw new Error('[IDP_CONFIG] FATAL: Internal IDP did not return nextAuthSecret');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(`[IDP_CONFIG] Internal IDP config loaded for ${clientIdStr}`);
|
|
331
|
+
consecutiveFailures = 0;
|
|
332
|
+
return config;
|
|
333
|
+
}
|
|
334
|
+
|
|
256
335
|
async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<IDPClientConfig> {
|
|
257
336
|
// =========================================================================
|
|
258
337
|
// Circuit Breaker Check
|
|
@@ -295,7 +374,6 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
295
374
|
subject: clientIdStr,
|
|
296
375
|
audience: 'urn:payez:externalauth:clientconfig',
|
|
297
376
|
expires_in: 60,
|
|
298
|
-
client_secret: process.env.PAYEZ_CLIENT_SECRET,
|
|
299
377
|
};
|
|
300
378
|
|
|
301
379
|
const signingResp = await fetch(signingUrl, {
|
|
@@ -32,21 +32,15 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
32
32
|
const clientIdStr = process.env.CLIENT_ID;
|
|
33
33
|
if (!clientIdStr || clientIdStr.trim() === '') throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
34
|
|
|
35
|
-
if (!process.env.PAYEZ_CLIENT_SECRET) {
|
|
36
|
-
throw new Error('[NEXTAUTH-SECRET] FATAL: PAYEZ_CLIENT_SECRET is required. Inject via container env or K8s Secret — never .env files.');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
40
36
|
|
|
41
37
|
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
42
|
-
// Client ID passed via X-Client-Id header, not query string
|
|
43
38
|
|
|
44
39
|
const signingPayload = {
|
|
45
40
|
issuer: clientIdStr,
|
|
46
41
|
subject: clientIdStr,
|
|
47
42
|
audience: 'urn:payez:externalauth:nextauthsecret',
|
|
48
43
|
expires_in: 60,
|
|
49
|
-
client_secret: process.env.PAYEZ_CLIENT_SECRET,
|
|
50
44
|
};
|
|
51
45
|
|
|
52
46
|
const signingResp = await fetch(signingUrl.toString(), {
|
|
@@ -92,7 +86,7 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
92
86
|
'X-Client-Id': clientIdStr,
|
|
93
87
|
'X-Correlation-Id': randomUUID().replace(/-/g, ''),
|
|
94
88
|
},
|
|
95
|
-
body: JSON.stringify({ client_assertion
|
|
89
|
+
body: JSON.stringify({ client_assertion }),
|
|
96
90
|
cache: 'no-store'
|
|
97
91
|
} as RequestInit);
|
|
98
92
|
|