@payez/next-mvp 4.0.34 → 4.0.36
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/api/auth-handler.js +3 -0
- package/dist/lib/api-handler.js +3 -1
- package/dist/lib/nextauth-secret.d.ts +10 -0
- package/dist/lib/nextauth-secret.js +100 -0
- package/package.json +6 -1
- package/src/api/auth-handler.ts +2 -0
- package/src/lib/api-handler.ts +3 -1
- package/src/lib/nextauth-secret.ts +121 -0
package/dist/api/auth-handler.js
CHANGED
|
@@ -235,6 +235,9 @@ function createAuthHandler(options = {}) {
|
|
|
235
235
|
function needsRefresh(auth) {
|
|
236
236
|
if (!autoRefresh)
|
|
237
237
|
return false;
|
|
238
|
+
// No refresh token = nothing to refresh with, skip entirely
|
|
239
|
+
if (!auth.refreshToken)
|
|
240
|
+
return false;
|
|
238
241
|
// Check if we have token expiry information
|
|
239
242
|
const token = auth.token;
|
|
240
243
|
const expiresAt = token.accessTokenExpires || token.exp;
|
package/dist/lib/api-handler.js
CHANGED
|
@@ -184,9 +184,11 @@ class ApiHandler {
|
|
|
184
184
|
}
|
|
185
185
|
catch { /* ignore */ }
|
|
186
186
|
// Check if token needs refresh
|
|
187
|
+
// Skip entirely if there's no refresh token — nothing to refresh with
|
|
187
188
|
const thresholdMs = 5 * 60 * 1000;
|
|
188
189
|
const expires = sessionData.idpAccessTokenExpires || 0;
|
|
189
|
-
const
|
|
190
|
+
const hasRefreshToken = !!sessionData.idpRefreshToken;
|
|
191
|
+
const needsRefresh = hasRefreshToken && (!accessToken || (expires - Date.now()) <= thresholdMs);
|
|
190
192
|
if (needsRefresh) {
|
|
191
193
|
const refreshResult = await this.handleCoordinatedRefresh(req, token, sessionData, ctx);
|
|
192
194
|
if (refreshResult.blocked) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the NextAuth secret (server-only).
|
|
4
|
+
*
|
|
5
|
+
* Priority:
|
|
6
|
+
* 1) Use process.env.NEXTAUTH_SECRET if present (allows overrides/production)
|
|
7
|
+
* 2) Fetch from IDP broker endpoint - IDP handles all Key Vault/signing
|
|
8
|
+
* 3) Cache result in-memory and set process.env.NEXTAUTH_SECRET for subsequent calls
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveNextAuthSecret(): Promise<string>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveNextAuthSecret = resolveNextAuthSecret;
|
|
4
|
+
require("server-only");
|
|
5
|
+
const secret_validation_1 = require("./secret-validation");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
let cachedSecret = null;
|
|
8
|
+
let lastFetchedAt = 0;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the NextAuth secret (server-only).
|
|
11
|
+
*
|
|
12
|
+
* Priority:
|
|
13
|
+
* 1) Use process.env.NEXTAUTH_SECRET if present (allows overrides/production)
|
|
14
|
+
* 2) Fetch from IDP broker endpoint - IDP handles all Key Vault/signing
|
|
15
|
+
* 3) Cache result in-memory and set process.env.NEXTAUTH_SECRET for subsequent calls
|
|
16
|
+
*/
|
|
17
|
+
async function resolveNextAuthSecret() {
|
|
18
|
+
// Check if already in environment
|
|
19
|
+
if (process.env.NEXTAUTH_SECRET && process.env.NEXTAUTH_SECRET.trim() !== '') {
|
|
20
|
+
// Silent - already configured
|
|
21
|
+
return process.env.NEXTAUTH_SECRET;
|
|
22
|
+
}
|
|
23
|
+
// Check if cached and fresh (within 5 minutes)
|
|
24
|
+
if (cachedSecret && Date.now() - lastFetchedAt < 5 * 60 * 1000) {
|
|
25
|
+
return cachedSecret;
|
|
26
|
+
}
|
|
27
|
+
// Broker mode: fetch from IDP (IDP handles all Key Vault/signing)
|
|
28
|
+
const base = process.env.IDP_URL;
|
|
29
|
+
if (!base)
|
|
30
|
+
throw new Error('IDP_URL environment variable is required');
|
|
31
|
+
const clientIdStr = process.env.CLIENT_ID;
|
|
32
|
+
if (!clientIdStr || clientIdStr.trim() === '')
|
|
33
|
+
throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
|
+
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
35
|
+
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
36
|
+
const signingPayload = {
|
|
37
|
+
issuer: clientIdStr,
|
|
38
|
+
subject: clientIdStr,
|
|
39
|
+
audience: 'urn:payez:externalauth:nextauthsecret',
|
|
40
|
+
expires_in: 60,
|
|
41
|
+
};
|
|
42
|
+
const signingResp = await fetch(signingUrl.toString(), {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Accept': 'application/json',
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'X-Client-Id': clientIdStr,
|
|
48
|
+
'X-Correlation-Id': (0, crypto_1.randomUUID)().replace(/-/g, ''),
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(signingPayload),
|
|
51
|
+
cache: 'no-store'
|
|
52
|
+
});
|
|
53
|
+
if (!signingResp.ok) {
|
|
54
|
+
const txt = await signingResp.text().catch(() => 'Unknown error');
|
|
55
|
+
throw new Error(`Failed to sign client assertion: ${signingResp.status} ${signingResp.statusText} - ${txt}`);
|
|
56
|
+
}
|
|
57
|
+
const signingBody = await signingResp.json().catch(() => ({}));
|
|
58
|
+
const client_assertion = (signingBody?.data?.client_assertion ??
|
|
59
|
+
signingBody?.data?.clientAssertion ??
|
|
60
|
+
signingBody?.client_assertion ??
|
|
61
|
+
signingBody?.clientAssertion ??
|
|
62
|
+
signingBody?.data?.ClientAssertion ??
|
|
63
|
+
signingBody?.ClientAssertion);
|
|
64
|
+
if (!client_assertion || typeof client_assertion !== 'string') {
|
|
65
|
+
throw new Error('IDP did not return a valid signed client assertion');
|
|
66
|
+
}
|
|
67
|
+
// Step 2: Use the signed assertion to fetch the NextAuth secret
|
|
68
|
+
const proxyUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/next-auth/secret`);
|
|
69
|
+
const proxyResp = await fetch(proxyUrl.toString(), {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Accept': 'application/json',
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
'X-Client-Id': clientIdStr,
|
|
75
|
+
'X-Correlation-Id': (0, crypto_1.randomUUID)().replace(/-/g, ''),
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ client_assertion }),
|
|
78
|
+
cache: 'no-store'
|
|
79
|
+
});
|
|
80
|
+
if (!proxyResp.ok) {
|
|
81
|
+
const txt = await proxyResp.text().catch(() => 'Unknown error');
|
|
82
|
+
throw new Error(`Proxy error: ${proxyResp.status} ${proxyResp.statusText} - ${txt}`);
|
|
83
|
+
}
|
|
84
|
+
const proxyBody = await proxyResp.json().catch(() => ({}));
|
|
85
|
+
const secret = (proxyBody?.data?.secret ?? proxyBody?.secret);
|
|
86
|
+
const configuration = (proxyBody?.data?.configuration ?? proxyBody?.configuration);
|
|
87
|
+
// Configuration is available but we don't log it verbosely
|
|
88
|
+
if (!secret || typeof secret !== 'string') {
|
|
89
|
+
throw new Error('Proxy did not return a valid NextAuth secret');
|
|
90
|
+
}
|
|
91
|
+
const validation = (0, secret_validation_1.validateNextAuthSecret)(secret);
|
|
92
|
+
if (!validation.valid) {
|
|
93
|
+
throw new Error(`Fetched NextAuth secret failed validation: ${validation.reason}`);
|
|
94
|
+
}
|
|
95
|
+
cachedSecret = secret;
|
|
96
|
+
lastFetchedAt = Date.now();
|
|
97
|
+
process.env.NEXTAUTH_SECRET = secret;
|
|
98
|
+
console.log('[NEXTAUTH-SECRET] Resolved from IDP (length:', secret.length + ')');
|
|
99
|
+
return secret;
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.36",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -107,6 +107,11 @@
|
|
|
107
107
|
"require": "./dist/lib/auth.js",
|
|
108
108
|
"default": "./dist/lib/auth.js"
|
|
109
109
|
},
|
|
110
|
+
"./lib/nextauth-secret": {
|
|
111
|
+
"types": "./dist/lib/nextauth-secret.d.ts",
|
|
112
|
+
"require": "./dist/lib/nextauth-secret.js",
|
|
113
|
+
"default": "./dist/lib/nextauth-secret.js"
|
|
114
|
+
},
|
|
110
115
|
"./lib/session-store": {
|
|
111
116
|
"types": "./dist/lib/session-store.d.ts",
|
|
112
117
|
"require": "./dist/lib/session-store.js",
|
package/src/api/auth-handler.ts
CHANGED
|
@@ -337,6 +337,8 @@ export function createAuthHandler(options: AuthHandlerOptions = {}) {
|
|
|
337
337
|
*/
|
|
338
338
|
function needsRefresh(auth: AuthContext): boolean {
|
|
339
339
|
if (!autoRefresh) return false;
|
|
340
|
+
// No refresh token = nothing to refresh with, skip entirely
|
|
341
|
+
if (!auth.refreshToken) return false;
|
|
340
342
|
|
|
341
343
|
// Check if we have token expiry information
|
|
342
344
|
const token = auth.token as any;
|
package/src/lib/api-handler.ts
CHANGED
|
@@ -279,9 +279,11 @@ export class ApiHandler {
|
|
|
279
279
|
} catch { /* ignore */ }
|
|
280
280
|
|
|
281
281
|
// Check if token needs refresh
|
|
282
|
+
// Skip entirely if there's no refresh token — nothing to refresh with
|
|
282
283
|
const thresholdMs = 5 * 60 * 1000;
|
|
283
284
|
const expires = sessionData.idpAccessTokenExpires || 0;
|
|
284
|
-
const
|
|
285
|
+
const hasRefreshToken = !!sessionData.idpRefreshToken;
|
|
286
|
+
const needsRefresh = hasRefreshToken && (!accessToken || (expires - Date.now()) <= thresholdMs);
|
|
285
287
|
|
|
286
288
|
if (needsRefresh) {
|
|
287
289
|
const refreshResult = await this.handleCoordinatedRefresh(req, token, sessionData, ctx);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { validateNextAuthSecret } from './secret-validation';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
let cachedSecret: string | null = null;
|
|
6
|
+
let lastFetchedAt = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the NextAuth secret (server-only).
|
|
10
|
+
*
|
|
11
|
+
* Priority:
|
|
12
|
+
* 1) Use process.env.NEXTAUTH_SECRET if present (allows overrides/production)
|
|
13
|
+
* 2) Fetch from IDP broker endpoint - IDP handles all Key Vault/signing
|
|
14
|
+
* 3) Cache result in-memory and set process.env.NEXTAUTH_SECRET for subsequent calls
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveNextAuthSecret(): Promise<string> {
|
|
17
|
+
// Check if already in environment
|
|
18
|
+
if (process.env.NEXTAUTH_SECRET && process.env.NEXTAUTH_SECRET.trim() !== '') {
|
|
19
|
+
// Silent - already configured
|
|
20
|
+
return process.env.NEXTAUTH_SECRET;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if cached and fresh (within 5 minutes)
|
|
24
|
+
if (cachedSecret && Date.now() - lastFetchedAt < 5 * 60 * 1000) {
|
|
25
|
+
return cachedSecret;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Broker mode: fetch from IDP (IDP handles all Key Vault/signing)
|
|
29
|
+
const base = process.env.IDP_URL;
|
|
30
|
+
if (!base) throw new Error('IDP_URL environment variable is required');
|
|
31
|
+
|
|
32
|
+
const clientIdStr = process.env.CLIENT_ID;
|
|
33
|
+
if (!clientIdStr || clientIdStr.trim() === '') throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
|
+
|
|
35
|
+
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
36
|
+
|
|
37
|
+
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
38
|
+
|
|
39
|
+
const signingPayload = {
|
|
40
|
+
issuer: clientIdStr,
|
|
41
|
+
subject: clientIdStr,
|
|
42
|
+
audience: 'urn:payez:externalauth:nextauthsecret',
|
|
43
|
+
expires_in: 60,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const signingResp = await fetch(signingUrl.toString(), {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Accept': 'application/json',
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'X-Client-Id': clientIdStr,
|
|
52
|
+
'X-Correlation-Id': randomUUID().replace(/-/g, ''),
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(signingPayload),
|
|
55
|
+
cache: 'no-store'
|
|
56
|
+
} as RequestInit);
|
|
57
|
+
|
|
58
|
+
if (!signingResp.ok) {
|
|
59
|
+
const txt = await signingResp.text().catch(() => 'Unknown error');
|
|
60
|
+
throw new Error(`Failed to sign client assertion: ${signingResp.status} ${signingResp.statusText} - ${txt}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const signingBody: any = await signingResp.json().catch(() => ({}));
|
|
64
|
+
const client_assertion = (
|
|
65
|
+
signingBody?.data?.client_assertion ??
|
|
66
|
+
signingBody?.data?.clientAssertion ??
|
|
67
|
+
signingBody?.client_assertion ??
|
|
68
|
+
signingBody?.clientAssertion ??
|
|
69
|
+
signingBody?.data?.ClientAssertion ??
|
|
70
|
+
signingBody?.ClientAssertion
|
|
71
|
+
) as string | undefined;
|
|
72
|
+
|
|
73
|
+
if (!client_assertion || typeof client_assertion !== 'string') {
|
|
74
|
+
throw new Error('IDP did not return a valid signed client assertion');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Step 2: Use the signed assertion to fetch the NextAuth secret
|
|
78
|
+
|
|
79
|
+
const proxyUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/next-auth/secret`);
|
|
80
|
+
|
|
81
|
+
const proxyResp = await fetch(proxyUrl.toString(), {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Accept': 'application/json',
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
'X-Client-Id': clientIdStr,
|
|
87
|
+
'X-Correlation-Id': randomUUID().replace(/-/g, ''),
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({ client_assertion }),
|
|
90
|
+
cache: 'no-store'
|
|
91
|
+
} as RequestInit);
|
|
92
|
+
|
|
93
|
+
if (!proxyResp.ok) {
|
|
94
|
+
const txt = await proxyResp.text().catch(() => 'Unknown error');
|
|
95
|
+
throw new Error(`Proxy error: ${proxyResp.status} ${proxyResp.statusText} - ${txt}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const proxyBody: any = await proxyResp.json().catch(() => ({}));
|
|
99
|
+
|
|
100
|
+
const secret = (proxyBody?.data?.secret ?? proxyBody?.secret) as string | undefined;
|
|
101
|
+
const configuration = (proxyBody?.data?.configuration ?? proxyBody?.configuration) as any | undefined;
|
|
102
|
+
|
|
103
|
+
// Configuration is available but we don't log it verbosely
|
|
104
|
+
|
|
105
|
+
if (!secret || typeof secret !== 'string') {
|
|
106
|
+
throw new Error('Proxy did not return a valid NextAuth secret');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const validation = validateNextAuthSecret(secret);
|
|
110
|
+
if (!validation.valid) {
|
|
111
|
+
throw new Error(`Fetched NextAuth secret failed validation: ${validation.reason}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cachedSecret = secret;
|
|
115
|
+
lastFetchedAt = Date.now();
|
|
116
|
+
process.env.NEXTAUTH_SECRET = secret;
|
|
117
|
+
|
|
118
|
+
console.log('[NEXTAUTH-SECRET] Resolved from IDP (length:', secret.length + ')');
|
|
119
|
+
|
|
120
|
+
return secret;
|
|
121
|
+
}
|