@payez/next-mvp 4.0.45 → 4.0.47
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.d.ts +0 -2
- package/dist/api/auth-handler.js +1 -1
- package/dist/api-handlers/auth/refresh.d.ts +4 -6
- package/dist/api-handlers/auth/refresh.js +5 -7
- package/dist/api-handlers/auth/signout.d.ts +6 -15
- package/dist/api-handlers/auth/signout.js +9 -16
- package/dist/api-handlers/auth/update-session.d.ts +6 -15
- package/dist/api-handlers/auth/update-session.js +7 -15
- package/dist/api-handlers/auth/verify-code.d.ts +6 -15
- package/dist/api-handlers/auth/verify-code.js +7 -15
- package/dist/api-handlers/session/viability.js +2 -2
- package/dist/auth/better-auth.d.ts +3 -19
- package/dist/auth/better-auth.js +7 -13
- package/dist/client/better-auth-client.d.ts +7 -8
- package/dist/client/better-auth-client.js +3 -4
- package/dist/lib/auth-secret.d.ts +17 -0
- package/dist/lib/{nextauth-secret.js → auth-secret.js} +31 -15
- package/dist/lib/demo-mode.js +3 -1
- package/dist/lib/idp-client-config.d.ts +6 -2
- package/dist/lib/idp-client-config.js +35 -21
- package/dist/lib/secret-validation.d.ts +1 -1
- package/dist/lib/secret-validation.js +2 -2
- package/dist/lib/startup-init.d.ts +3 -3
- package/dist/lib/startup-init.js +23 -18
- package/dist/lib/test-aware-get-token.js +2 -2
- package/dist/routes/account/masked-info.d.ts +1 -1
- package/dist/routes/account/masked-info.js +1 -1
- package/dist/routes/account/send-code.d.ts +1 -1
- package/dist/routes/account/send-code.js +1 -1
- package/dist/routes/account/verify-email.d.ts +1 -1
- package/dist/routes/account/verify-email.js +1 -1
- package/dist/routes/account/verify-sms.d.ts +1 -1
- package/dist/routes/account/verify-sms.js +1 -1
- package/dist/routes/auth/refresh.js +3 -8
- package/dist/server/auth.d.ts +4 -7
- package/dist/server/auth.js +3 -6
- package/dist/server/decode-session.js +2 -2
- package/dist/vibe/hooks/index.d.ts +1 -1
- package/package.json +888 -893
- package/src/api/auth-handler.ts +0 -4
- package/src/api-handlers/auth/refresh.ts +5 -8
- package/src/api-handlers/auth/signout.ts +9 -21
- package/src/api-handlers/auth/update-session.ts +7 -20
- package/src/api-handlers/auth/verify-code.ts +7 -20
- package/src/api-handlers/session/viability.ts +2 -2
- package/src/auth/better-auth.ts +7 -32
- package/src/client/better-auth-client.ts +3 -4
- package/src/lib/{nextauth-secret.ts → auth-secret.ts} +32 -16
- package/src/lib/demo-mode.ts +5 -1
- package/src/lib/idp-client-config.ts +42 -22
- package/src/lib/secret-validation.ts +1 -1
- package/src/lib/startup-init.ts +23 -18
- package/src/lib/test-aware-get-token.ts +2 -2
- package/src/routes/account/masked-info.ts +1 -1
- package/src/routes/account/send-code.ts +1 -1
- package/src/routes/account/verify-email.ts +1 -1
- package/src/routes/account/verify-sms.ts +1 -1
- package/src/routes/auth/refresh.ts +3 -8
- package/src/server/auth.ts +3 -6
- package/src/server/decode-session.ts +2 -2
- package/dist/lib/nextauth-secret.d.ts +0 -10
package/src/api/auth-handler.ts
CHANGED
|
@@ -50,9 +50,6 @@ export interface AuthHandlerOptions {
|
|
|
50
50
|
/** Maximum number of retry attempts on 401 (default: 1) */
|
|
51
51
|
maxRetries?: number;
|
|
52
52
|
|
|
53
|
-
/** NextAuth secret for JWT decoding */
|
|
54
|
-
nextAuthSecret?: string;
|
|
55
|
-
|
|
56
53
|
/** IDP base URL for refresh requests */
|
|
57
54
|
idpBaseUrl?: string;
|
|
58
55
|
|
|
@@ -99,7 +96,6 @@ export function createAuthHandler(options: AuthHandlerOptions = {}) {
|
|
|
99
96
|
refreshBuffer = 60, // 60 seconds - matches website-membership proven threshold
|
|
100
97
|
retryOn401 = true,
|
|
101
98
|
maxRetries = 1,
|
|
102
|
-
nextAuthSecret = process.env.NEXTAUTH_SECRET,
|
|
103
99
|
idpBaseUrl = process.env.IDP_URL,
|
|
104
100
|
clientId = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_IDP_CLIENT_ID
|
|
105
101
|
} = options;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ASK BEFORE EDITING - TESTED AND WORKING SYSTEM
|
|
5
5
|
*
|
|
6
6
|
* This handler manages the server-side refresh token cycle with:
|
|
7
|
-
* -
|
|
7
|
+
* - Better Auth session extraction
|
|
8
8
|
* - Session token fallback for internal calls
|
|
9
9
|
* - PayEz IDP refresh token exchange
|
|
10
10
|
* - Session state updates with new tokens
|
|
@@ -23,14 +23,13 @@ import { extractKidFromToken } from '../../auth/utils/token-utils';
|
|
|
23
23
|
interface RefreshConfig {
|
|
24
24
|
idpBaseUrl: string;
|
|
25
25
|
clientId: string;
|
|
26
|
-
nextAuthSecret: string;
|
|
27
26
|
refreshEndpoint?: string;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Creates a refresh token handler for Next.js API routes
|
|
32
31
|
*
|
|
33
|
-
* @param config
|
|
32
|
+
* @param config IDP connection settings (Better Auth handles session crypto)
|
|
34
33
|
* @returns Next.js POST handler function
|
|
35
34
|
*
|
|
36
35
|
* @example
|
|
@@ -41,13 +40,12 @@ interface RefreshConfig {
|
|
|
41
40
|
* export const POST = createRefreshHandler({
|
|
42
41
|
* idpBaseUrl: process.env.IDP_URL!,
|
|
43
42
|
* clientId: process.env.CLIENT_ID!,
|
|
44
|
-
* nextAuthSecret: process.env.NEXTAUTH_SECRET!,
|
|
45
43
|
* refreshEndpoint: '/api/ExternalAuth/refresh'
|
|
46
44
|
* });
|
|
47
45
|
* ```
|
|
48
46
|
*/
|
|
49
47
|
export function createRefreshHandler(config: RefreshConfig) {
|
|
50
|
-
const { idpBaseUrl, clientId,
|
|
48
|
+
const { idpBaseUrl, clientId, refreshEndpoint = '/api/ExternalAuth/refresh' } = config;
|
|
51
49
|
|
|
52
50
|
return async function POST(req: NextRequest) {
|
|
53
51
|
try {
|
|
@@ -674,12 +672,11 @@ export function createRefreshHandler(config: RefreshConfig) {
|
|
|
674
672
|
}
|
|
675
673
|
|
|
676
674
|
/**
|
|
677
|
-
* Default export for
|
|
678
|
-
* Requires environment variables: IDP_URL, CLIENT_ID
|
|
675
|
+
* Default POST export — drop-in for `app/api/auth/refresh/route.ts`.
|
|
676
|
+
* Requires environment variables: IDP_URL, CLIENT_ID
|
|
679
677
|
*/
|
|
680
678
|
export const POST = createRefreshHandler({
|
|
681
679
|
idpBaseUrl: process.env.IDP_URL!,
|
|
682
680
|
clientId: process.env.CLIENT_ID || 'payez_default_client',
|
|
683
|
-
nextAuthSecret: process.env.NEXTAUTH_SECRET || '',
|
|
684
681
|
refreshEndpoint: '/api/ExternalAuth/refresh'
|
|
685
682
|
});
|
|
@@ -57,29 +57,19 @@ interface SignoutResponse {
|
|
|
57
57
|
chunkCookiesDeleted?: number;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
interface SignoutConfig {
|
|
61
|
-
nextAuthSecret: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
/**
|
|
65
|
-
* Creates a signout handler for Next.js API routes
|
|
61
|
+
* Creates a signout handler for Next.js API routes.
|
|
66
62
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
63
|
+
* Better Auth resolves its session from cookies, so this handler takes no
|
|
64
|
+
* configuration. Use the default `POST` export below for typical usage.
|
|
69
65
|
*
|
|
70
66
|
* @example
|
|
71
67
|
* ```typescript
|
|
72
68
|
* // In your app's /app/api/auth/signout/route.ts
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* export const POST = createSignoutHandler({
|
|
76
|
-
* nextAuthSecret: process.env.NEXTAUTH_SECRET!
|
|
77
|
-
* });
|
|
69
|
+
* export { POST } from '@payez/next-mvp/api-handlers/auth/signout';
|
|
78
70
|
* ```
|
|
79
71
|
*/
|
|
80
|
-
export function createSignoutHandler(
|
|
81
|
-
const { nextAuthSecret } = config;
|
|
82
|
-
|
|
72
|
+
export function createSignoutHandler() {
|
|
83
73
|
return async function POST(req: NextRequest) {
|
|
84
74
|
const cookieStore = await cookies();
|
|
85
75
|
|
|
@@ -113,7 +103,8 @@ export function createSignoutHandler(config: SignoutConfig) {
|
|
|
113
103
|
const chunkCookies = cookieStore.getAll()
|
|
114
104
|
.filter(cookie => cookie.name.startsWith(`${sessionCookieName}.`));
|
|
115
105
|
|
|
116
|
-
// Decode
|
|
106
|
+
// Decode the Better Auth session JWT to extract the Redis session UUID
|
|
107
|
+
// before deletion.
|
|
117
108
|
let redisSessionToken: string | null = null;
|
|
118
109
|
|
|
119
110
|
// First attempt: Better Auth getSession
|
|
@@ -203,9 +194,6 @@ export function createSignoutHandler(config: SignoutConfig) {
|
|
|
203
194
|
}
|
|
204
195
|
|
|
205
196
|
/**
|
|
206
|
-
* Default export for
|
|
207
|
-
* Requires environment variable: NEXTAUTH_SECRET
|
|
197
|
+
* Default POST export — drop-in for `app/api/auth/signout/route.ts`.
|
|
208
198
|
*/
|
|
209
|
-
export const POST = createSignoutHandler(
|
|
210
|
-
nextAuthSecret: process.env.NEXTAUTH_SECRET || ''
|
|
211
|
-
});
|
|
199
|
+
export const POST = createSignoutHandler();
|
|
@@ -38,29 +38,19 @@ interface UpdateSessionResponse {
|
|
|
38
38
|
twoFactorMethod?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
interface UpdateSessionConfig {
|
|
42
|
-
nextAuthSecret?: string; // Legacy - no longer used by Better Auth
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
/**
|
|
46
|
-
* Creates an update-session handler for Next.js API routes
|
|
42
|
+
* Creates an update-session handler for Next.js API routes.
|
|
47
43
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* Better Auth resolves its session from cookies, so this handler takes no
|
|
45
|
+
* configuration. Use the default `POST` export below for typical usage.
|
|
50
46
|
*
|
|
51
47
|
* @example
|
|
52
48
|
* ```typescript
|
|
53
49
|
* // In your app's /app/api/auth/update-session/route.ts
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* export const POST = createUpdateSessionHandler({
|
|
57
|
-
* nextAuthSecret: process.env.NEXTAUTH_SECRET!
|
|
58
|
-
* });
|
|
50
|
+
* export { POST } from '@payez/next-mvp/api-handlers/auth/update-session';
|
|
59
51
|
* ```
|
|
60
52
|
*/
|
|
61
|
-
export function createUpdateSessionHandler(
|
|
62
|
-
const { nextAuthSecret } = config;
|
|
63
|
-
|
|
53
|
+
export function createUpdateSessionHandler() {
|
|
64
54
|
return async function POST(req: NextRequest) {
|
|
65
55
|
try {
|
|
66
56
|
let body: UpdateSessionRequest;
|
|
@@ -115,9 +105,6 @@ export function createUpdateSessionHandler(config: UpdateSessionConfig) {
|
|
|
115
105
|
}
|
|
116
106
|
|
|
117
107
|
/**
|
|
118
|
-
* Default export for
|
|
119
|
-
* Requires environment variable: NEXTAUTH_SECRET
|
|
108
|
+
* Default POST export — drop-in for `app/api/auth/update-session/route.ts`.
|
|
120
109
|
*/
|
|
121
|
-
export const POST = createUpdateSessionHandler(
|
|
122
|
-
nextAuthSecret: process.env.NEXTAUTH_SECRET || ''
|
|
123
|
-
});
|
|
110
|
+
export const POST = createUpdateSessionHandler();
|
|
@@ -20,29 +20,19 @@ interface VerifyCodeRequest {
|
|
|
20
20
|
refreshTokenExpires?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
interface VerifyCodeConfig {
|
|
24
|
-
nextAuthSecret?: string; // Legacy - no longer used by Better Auth
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
/**
|
|
28
|
-
* Creates a verify-code/complete-2FA handler for Next.js API routes
|
|
24
|
+
* Creates a verify-code/complete-2FA handler for Next.js API routes.
|
|
29
25
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Better Auth resolves its session from cookies, so this handler takes no
|
|
27
|
+
* configuration. Use the default `POST` export below for typical usage.
|
|
32
28
|
*
|
|
33
29
|
* @example
|
|
34
30
|
* ```typescript
|
|
35
31
|
* // In your app's /app/api/auth/verify-code/route.ts
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* export const POST = createVerifyCodeHandler({
|
|
39
|
-
* nextAuthSecret: process.env.NEXTAUTH_SECRET!
|
|
40
|
-
* });
|
|
32
|
+
* export { POST } from '@payez/next-mvp/api-handlers/auth/verify-code';
|
|
41
33
|
* ```
|
|
42
34
|
*/
|
|
43
|
-
export function createVerifyCodeHandler(
|
|
44
|
-
const { nextAuthSecret } = config;
|
|
45
|
-
|
|
35
|
+
export function createVerifyCodeHandler() {
|
|
46
36
|
return async function POST(req: NextRequest) {
|
|
47
37
|
try {
|
|
48
38
|
let body: VerifyCodeRequest;
|
|
@@ -117,9 +107,6 @@ export function createVerifyCodeHandler(config: VerifyCodeConfig) {
|
|
|
117
107
|
}
|
|
118
108
|
|
|
119
109
|
/**
|
|
120
|
-
* Default export for
|
|
121
|
-
* Requires environment variable: NEXTAUTH_SECRET
|
|
110
|
+
* Default POST export — drop-in for `app/api/auth/verify-code/route.ts`.
|
|
122
111
|
*/
|
|
123
|
-
export const POST = createVerifyCodeHandler(
|
|
124
|
-
nextAuthSecret: process.env.NEXTAUTH_SECRET || ''
|
|
125
|
-
});
|
|
112
|
+
export const POST = createVerifyCodeHandler();
|
|
@@ -12,8 +12,8 @@ import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
|
12
12
|
|
|
13
13
|
export async function GET(req: NextRequest) {
|
|
14
14
|
try {
|
|
15
|
-
// Ensure initialization is complete
|
|
16
|
-
if (!process.env.NEXTAUTH_SECRET) {
|
|
15
|
+
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
16
|
+
if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
|
|
17
17
|
try {
|
|
18
18
|
await ensureInitialized();
|
|
19
19
|
} catch (error) {
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -48,49 +48,25 @@ export function buildBetterAuthProviders(
|
|
|
48
48
|
return providers;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* Optional extra plugins for createBetterAuthInstance.
|
|
53
|
-
* Use to add credentials/custom signin endpoints from the host app.
|
|
54
|
-
*/
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
-
export type BetterAuthExtraPlugins = any[];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Optional overrides for createBetterAuthInstance.
|
|
60
|
-
*/
|
|
61
|
-
export interface BetterAuthInstanceOptions {
|
|
62
|
-
/** Path suffix for the BA mount (default: '/api/auth'). Use '/api/ba-auth' for migration scenarios. */
|
|
63
|
-
basePath?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
51
|
/**
|
|
67
52
|
* Create Better Auth instance from IDP config.
|
|
68
53
|
*
|
|
69
54
|
* No database — runs in stateless mode with JWE cookie cache.
|
|
70
55
|
* Call after getIDPClientConfig() resolves.
|
|
71
|
-
*
|
|
72
|
-
* @param idpConfig IDP client config (from getIDPClientConfig)
|
|
73
|
-
* @param extraPlugins Optional plugins to add (e.g., credentials plugin from host app)
|
|
74
|
-
* @param options Optional overrides (e.g., basePath for migration scenarios)
|
|
75
56
|
*/
|
|
76
|
-
export function createBetterAuthInstance(
|
|
77
|
-
idpConfig: IDPClientConfig,
|
|
78
|
-
extraPlugins: BetterAuthExtraPlugins = [],
|
|
79
|
-
options: BetterAuthInstanceOptions = {}
|
|
80
|
-
) {
|
|
57
|
+
export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
81
58
|
const appSlug = idpConfig.clientSlug || getAppSlug();
|
|
82
|
-
const basePath = options.basePath || '/api/auth';
|
|
83
59
|
|
|
84
60
|
// Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
|
|
85
|
-
//
|
|
61
|
+
// Must include /api/auth since that's where the catch-all route is mounted
|
|
86
62
|
const rawBaseURL = process.env.BETTER_AUTH_URL
|
|
87
63
|
|| idpConfig.baseClientUrl
|
|
88
64
|
|| `http://localhost:${process.env.PORT || '3000'}`;
|
|
89
|
-
const baseURL = rawBaseURL.replace(/\/+$/, '') +
|
|
65
|
+
const baseURL = rawBaseURL.replace(/\/+$/, '') + '/api/auth';
|
|
90
66
|
|
|
91
67
|
return betterAuth({
|
|
92
68
|
baseURL,
|
|
93
|
-
secret: idpConfig.
|
|
69
|
+
secret: idpConfig.authSecret as string,
|
|
94
70
|
|
|
95
71
|
socialProviders: buildBetterAuthProviders(idpConfig),
|
|
96
72
|
|
|
@@ -147,7 +123,6 @@ export function createBetterAuthInstance(
|
|
|
147
123
|
},
|
|
148
124
|
|
|
149
125
|
plugins: [
|
|
150
|
-
...extraPlugins,
|
|
151
126
|
nextCookies(),
|
|
152
127
|
],
|
|
153
128
|
});
|
|
@@ -173,12 +148,12 @@ let initPromise: Promise<any> | null = null;
|
|
|
173
148
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
149
|
export { cachedInstance as __betterAuthInstance };
|
|
175
150
|
|
|
176
|
-
export async function getBetterAuthInstance(
|
|
151
|
+
export async function getBetterAuthInstance() {
|
|
177
152
|
if (cachedInstance) return cachedInstance;
|
|
178
153
|
|
|
179
154
|
if (!initPromise) {
|
|
180
155
|
initPromise = getIDPClientConfig(true).then(config => {
|
|
181
|
-
const instance = createBetterAuthInstance(config
|
|
156
|
+
const instance = createBetterAuthInstance(config);
|
|
182
157
|
cachedInstance = instance;
|
|
183
158
|
console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
|
|
184
159
|
return instance;
|
|
@@ -287,7 +262,7 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
287
262
|
}
|
|
288
263
|
|
|
289
264
|
// Build IDP token data
|
|
290
|
-
const requiresTwoFactor = result.
|
|
265
|
+
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
291
266
|
const idpTokenData = {
|
|
292
267
|
idpAccessToken: result.access_token,
|
|
293
268
|
idpRefreshToken: result.refresh_token,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Better Auth Client
|
|
2
|
+
* Better Auth Client.
|
|
3
3
|
*
|
|
4
|
-
* Drop-in replacement for next-auth/react hooks and functions.
|
|
5
4
|
* Import from '@payez/next-mvp/client/better-auth-client'.
|
|
6
5
|
*
|
|
7
|
-
* Includes useSessionCompat() — returns
|
|
8
|
-
*
|
|
6
|
+
* Includes useSessionCompat() — returns a { data, status } shape so existing
|
|
7
|
+
* components written against the legacy hook don't need destructure changes.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { createAuthClient } from 'better-auth/react';
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
import {
|
|
2
|
+
import { validateAuthSecret } from './secret-validation';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
|
|
5
5
|
let cachedSecret: string | null = null;
|
|
6
6
|
let lastFetchedAt = 0;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Resolve the
|
|
9
|
+
* Resolve the Better Auth signing secret (server-only).
|
|
10
10
|
*
|
|
11
11
|
* Priority:
|
|
12
|
-
* 1) Use process.env.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 1) Use process.env.BETTER_AUTH_SECRET (preferred) or NEXTAUTH_SECRET (legacy)
|
|
13
|
+
* if present — allows overrides/production via env.
|
|
14
|
+
* 2) Fetch from IDP broker endpoint — IDP handles all Key Vault/signing.
|
|
15
|
+
* 3) Cache result in-memory and set process.env.BETTER_AUTH_SECRET for
|
|
16
|
+
* subsequent calls.
|
|
17
|
+
*
|
|
18
|
+
* NOTE on naming: this secret is the cryptographic key Better Auth uses to
|
|
19
|
+
* sign session JWTs. The IDP backend still names the broker endpoint and
|
|
20
|
+
* response field with the legacy "next-auth" / "nextAuthSecret" names; we
|
|
21
|
+
* read both new and legacy on the wire during the migration window.
|
|
15
22
|
*/
|
|
16
|
-
export async function
|
|
17
|
-
// Check if already in environment
|
|
18
|
-
|
|
23
|
+
export async function resolveAuthSecret(): Promise<string> {
|
|
24
|
+
// Check if already in environment (prefer new name, fall back to legacy)
|
|
25
|
+
const envSecret =
|
|
26
|
+
(process.env.BETTER_AUTH_SECRET && process.env.BETTER_AUTH_SECRET.trim() !== ''
|
|
27
|
+
? process.env.BETTER_AUTH_SECRET
|
|
28
|
+
: undefined) ||
|
|
29
|
+
(process.env.NEXTAUTH_SECRET && process.env.NEXTAUTH_SECRET.trim() !== ''
|
|
30
|
+
? process.env.NEXTAUTH_SECRET
|
|
31
|
+
: undefined);
|
|
32
|
+
if (envSecret) {
|
|
19
33
|
// Silent - already configured
|
|
20
|
-
return
|
|
34
|
+
return envSecret;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
// Check if cached and fresh (within 5 minutes)
|
|
@@ -74,7 +88,8 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
74
88
|
throw new Error('IDP did not return a valid signed client assertion');
|
|
75
89
|
}
|
|
76
90
|
|
|
77
|
-
// Step 2: Use the signed assertion to fetch the
|
|
91
|
+
// Step 2: Use the signed assertion to fetch the auth secret
|
|
92
|
+
// (Endpoint is still served at /next-auth/secret on the IDP — legacy path.)
|
|
78
93
|
|
|
79
94
|
const proxyUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/next-auth/secret`);
|
|
80
95
|
|
|
@@ -98,24 +113,25 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
98
113
|
const proxyBody: any = await proxyResp.json().catch(() => ({}));
|
|
99
114
|
|
|
100
115
|
const secret = (proxyBody?.data?.secret ?? proxyBody?.secret) as string | undefined;
|
|
101
|
-
const configuration = (proxyBody?.data?.configuration ?? proxyBody?.configuration) as any | undefined;
|
|
102
|
-
|
|
103
116
|
// Configuration is available but we don't log it verbosely
|
|
104
117
|
|
|
105
118
|
if (!secret || typeof secret !== 'string') {
|
|
106
|
-
throw new Error('Proxy did not return a valid
|
|
119
|
+
throw new Error('Proxy did not return a valid auth secret');
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
const validation =
|
|
122
|
+
const validation = validateAuthSecret(secret);
|
|
110
123
|
if (!validation.valid) {
|
|
111
|
-
throw new Error(`Fetched
|
|
124
|
+
throw new Error(`Fetched auth secret failed validation: ${validation.reason}`);
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
cachedSecret = secret;
|
|
115
128
|
lastFetchedAt = Date.now();
|
|
129
|
+
process.env.BETTER_AUTH_SECRET = secret;
|
|
130
|
+
// Also set legacy name during transition so any consumer still reading
|
|
131
|
+
// process.env.NEXTAUTH_SECRET keeps working until they upgrade.
|
|
116
132
|
process.env.NEXTAUTH_SECRET = secret;
|
|
117
133
|
|
|
118
|
-
console.log('[
|
|
134
|
+
console.log('[AUTH-SECRET] Resolved from IDP (length:', secret.length + ')');
|
|
119
135
|
|
|
120
136
|
return secret;
|
|
121
137
|
}
|
package/src/lib/demo-mode.ts
CHANGED
|
@@ -9,5 +9,9 @@ export function isDemoMode(): boolean {
|
|
|
9
9
|
|
|
10
10
|
export function isAuthConfigured(): boolean {
|
|
11
11
|
if (isDemoMode()) return false;
|
|
12
|
-
return !!(
|
|
12
|
+
return !!(
|
|
13
|
+
process.env.BETTER_AUTH_SECRET ||
|
|
14
|
+
process.env.NEXTAUTH_SECRET ||
|
|
15
|
+
(process.env.NEXT_CLIENT_ID && process.env.NEXT_CLIENT_PRIVATE_KEY_PEM)
|
|
16
|
+
);
|
|
13
17
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - OAuth provider credentials (from Key Vault)
|
|
6
6
|
* - 2FA/MFA settings
|
|
7
7
|
* - Session configuration
|
|
8
|
-
* -
|
|
8
|
+
* - Better Auth signing secret
|
|
9
9
|
* - Branding
|
|
10
10
|
*
|
|
11
11
|
* CACHING STRATEGY:
|
|
@@ -58,7 +58,11 @@ export interface BrandingConfig {
|
|
|
58
58
|
export interface IDPClientConfig {
|
|
59
59
|
clientId: string;
|
|
60
60
|
clientSlug: string;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Cryptographic secret used by Better Auth to sign session JWTs.
|
|
63
|
+
* Historically named "nextAuthSecret" — kept under the new name now.
|
|
64
|
+
*/
|
|
65
|
+
authSecret: string;
|
|
62
66
|
configCacheTtlSeconds: number;
|
|
63
67
|
oauthProviders: OAuthProviderConfig[];
|
|
64
68
|
authSettings: AuthSettings;
|
|
@@ -170,14 +174,15 @@ export async function getIDPClientConfig(forceRefresh: boolean = false): Promise
|
|
|
170
174
|
cachedConfig = redisConfig;
|
|
171
175
|
cacheExpiry = Date.now() + ((redisConfig.configCacheTtlSeconds || 300) * 1000);
|
|
172
176
|
|
|
173
|
-
// Set
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
// Set BETTER_AUTH_SECRET from cached config (also set legacy
|
|
178
|
+
// NEXTAUTH_SECRET during the rename transition).
|
|
179
|
+
if (redisConfig.authSecret) {
|
|
180
|
+
process.env.BETTER_AUTH_SECRET = redisConfig.authSecret;
|
|
181
|
+
process.env.NEXTAUTH_SECRET = redisConfig.authSecret;
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from cached config
|
|
179
|
-
//
|
|
180
|
-
// Only set if not already defined (allows deployment override for beta/staging)
|
|
184
|
+
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from cached config.
|
|
185
|
+
// Only set if not already defined (allows deployment override for beta/staging).
|
|
181
186
|
if (redisConfig.baseClientUrl && !process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL) {
|
|
182
187
|
process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL = redisConfig.baseClientUrl;
|
|
183
188
|
}
|
|
@@ -211,16 +216,17 @@ export async function getIDPClientConfig(forceRefresh: boolean = false): Promise
|
|
|
211
216
|
// Store in Redis for persistence across module reloads
|
|
212
217
|
await setConfigInRedis(config);
|
|
213
218
|
|
|
214
|
-
// Set
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
// Set BETTER_AUTH_SECRET from config (also set legacy
|
|
220
|
+
// NEXTAUTH_SECRET during the rename transition).
|
|
221
|
+
if (config.authSecret) {
|
|
222
|
+
process.env.BETTER_AUTH_SECRET = config.authSecret;
|
|
223
|
+
process.env.NEXTAUTH_SECRET = config.authSecret;
|
|
217
224
|
} else {
|
|
218
|
-
throw new Error('[IDP_CONFIG] FATAL: IDP did not return
|
|
225
|
+
throw new Error('[IDP_CONFIG] FATAL: IDP did not return authSecret');
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from config
|
|
222
|
-
//
|
|
223
|
-
// Only set if not already defined (allows deployment override for beta/staging)
|
|
228
|
+
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from config.
|
|
229
|
+
// Only set if not already defined (allows deployment override for beta/staging).
|
|
224
230
|
if (config.baseClientUrl && !process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL) {
|
|
225
231
|
process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL = config.baseClientUrl;
|
|
226
232
|
console.log("[IDP_CONFIG] Set IDENTITY_CLIENT_BASE_EXTERNAL_URL:", config.baseClientUrl);
|
|
@@ -305,7 +311,14 @@ async function fetchConfigFromInternalIDP(internalIdpUrl: string, clientIdStr: s
|
|
|
305
311
|
const config: IDPClientConfig = {
|
|
306
312
|
clientId: String(rawClientId),
|
|
307
313
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
308
|
-
|
|
314
|
+
// Wire compatibility: accept new authSecret first, fall back to legacy
|
|
315
|
+
// nextAuthSecret/next_auth_secret while IDP rename rolls out.
|
|
316
|
+
authSecret:
|
|
317
|
+
configData.authSecret ??
|
|
318
|
+
configData.auth_secret ??
|
|
319
|
+
configData.nextAuthSecret ??
|
|
320
|
+
configData.next_auth_secret ??
|
|
321
|
+
'',
|
|
309
322
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
310
323
|
oauthProviders: (configData.oauthProviders ?? configData.oauth_providers ?? []).map((p: any) => ({
|
|
311
324
|
provider: p.provider ?? '',
|
|
@@ -336,8 +349,8 @@ async function fetchConfigFromInternalIDP(internalIdpUrl: string, clientIdStr: s
|
|
|
336
349
|
baseClientUrl: configData.baseClientUrl ?? configData.base_client_url ?? configData.BaseClientUrl
|
|
337
350
|
};
|
|
338
351
|
|
|
339
|
-
if (!config.
|
|
340
|
-
throw new Error('[IDP_CONFIG] FATAL: Internal IDP did not return
|
|
352
|
+
if (!config.authSecret) {
|
|
353
|
+
throw new Error('[IDP_CONFIG] FATAL: Internal IDP did not return authSecret');
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
console.log(`[IDP_CONFIG] Internal IDP config loaded for ${clientIdStr}`);
|
|
@@ -464,11 +477,18 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
464
477
|
throw new Error(`[IDP_CONFIG] FATAL: IDP response missing clientId/client_id. Got: ${JSON.stringify(Object.keys(configData))}`);
|
|
465
478
|
}
|
|
466
479
|
|
|
467
|
-
// Map response to our interface (IDP
|
|
480
|
+
// Map response to our interface (IDP returns camelCase or snake_case).
|
|
481
|
+
// Wire compatibility: accept new authSecret first, fall back to legacy
|
|
482
|
+
// nextAuthSecret/next_auth_secret while IDP rename rolls out.
|
|
468
483
|
const config: IDPClientConfig = {
|
|
469
484
|
clientId: String(rawClientId),
|
|
470
485
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
471
|
-
|
|
486
|
+
authSecret:
|
|
487
|
+
configData.authSecret ??
|
|
488
|
+
configData.auth_secret ??
|
|
489
|
+
configData.nextAuthSecret ??
|
|
490
|
+
configData.next_auth_secret ??
|
|
491
|
+
'',
|
|
472
492
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
473
493
|
oauthProviders: (configData.oauthProviders ?? configData.oauth_providers ?? []).map((p: any) => ({
|
|
474
494
|
provider: p.provider ?? '',
|
|
@@ -517,8 +537,8 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
517
537
|
if (!config.clientId) {
|
|
518
538
|
throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
|
|
519
539
|
}
|
|
520
|
-
if (!config.
|
|
521
|
-
throw new Error('[IDP_CONFIG] FATAL:
|
|
540
|
+
if (!config.authSecret) {
|
|
541
|
+
throw new Error('[IDP_CONFIG] FATAL: authSecret is empty after parsing');
|
|
522
542
|
}
|
|
523
543
|
|
|
524
544
|
// Success - reset failure tracking
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function validateAuthSecret(secret: string): { valid: boolean; reason?: string } {
|
|
2
2
|
if (!secret || typeof secret !== 'string') return { valid: false, reason: 'missing' };
|
|
3
3
|
if (secret.length < 32) return { valid: false, reason: 'too_short' };
|
|
4
4
|
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^a-zA-Z0-9]/];
|