najm-auth 2.0.6 → 2.0.10
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 +801 -774
- package/dist/client/edge.d.ts +26 -1
- package/dist/client/edge.js +122 -16
- package/dist/client/server/index.d.ts +11 -2
- package/dist/client/server/index.js +135 -20
- package/dist/index.d.ts +22 -3
- package/dist/index.js +78 -36
- package/package.json +3 -1
package/dist/client/edge.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as next_server from 'next/server';
|
|
2
2
|
|
|
3
|
+
type SessionRecoveryFailureReason = 'invalid-cookie-name' | 'invalid-refresh-cookie' | 'invalid-endpoint' | 'fetch-error' | 'http-status' | 'missing-set-cookie' | 'session-cookie-parse' | 'session-cookie-hmac' | 'session-cookie-payload';
|
|
4
|
+
interface SessionRecoveryErrorDetails {
|
|
5
|
+
name: string;
|
|
6
|
+
message: string;
|
|
7
|
+
code?: string;
|
|
8
|
+
cause?: {
|
|
9
|
+
name: string;
|
|
10
|
+
message: string;
|
|
11
|
+
code?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface SessionRecoveryFailure {
|
|
15
|
+
reason: SessionRecoveryFailureReason;
|
|
16
|
+
httpStatus?: number;
|
|
17
|
+
error?: SessionRecoveryErrorDetails;
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
interface AuthMiddlewareConfig {
|
|
4
21
|
/** Routes that require authentication (glob patterns) */
|
|
5
22
|
protectedRoutes?: string[];
|
|
@@ -34,6 +51,14 @@ interface AuthMiddlewareConfig {
|
|
|
34
51
|
* Set to false to disable automatic recovery.
|
|
35
52
|
*/
|
|
36
53
|
recoveryURL?: string | false;
|
|
54
|
+
/**
|
|
55
|
+
* Optional loopback-only endpoint for self-hosted apps whose public origin
|
|
56
|
+
* cannot be reached from the app container. Takes precedence over
|
|
57
|
+
* `recoveryURL` for the server-side recovery request.
|
|
58
|
+
*/
|
|
59
|
+
internalRecoveryURL?: string;
|
|
60
|
+
/** Secret-free diagnostic hook for failed authoritative recovery attempts. */
|
|
61
|
+
onRecoveryFailure?: (failure: SessionRecoveryFailure) => void;
|
|
37
62
|
}
|
|
38
63
|
/**
|
|
39
64
|
* Create a Next.js middleware function that protects routes based on auth state.
|
|
@@ -57,4 +82,4 @@ interface AuthMiddlewareConfig {
|
|
|
57
82
|
*/
|
|
58
83
|
declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
|
|
59
84
|
|
|
60
|
-
export { type AuthMiddlewareConfig, withAuthMiddleware };
|
|
85
|
+
export { type AuthMiddlewareConfig, type SessionRecoveryErrorDetails, type SessionRecoveryFailure, type SessionRecoveryFailureReason, withAuthMiddleware };
|
package/dist/client/edge.js
CHANGED
|
@@ -36,22 +36,36 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
36
36
|
}
|
|
37
37
|
__name(parseSessionCookiePayload, "parseSessionCookiePayload");
|
|
38
38
|
async function verifySessionCookie(rawCookieValue, options) {
|
|
39
|
-
|
|
39
|
+
const result = await verifySessionCookieDetailed(rawCookieValue, options);
|
|
40
|
+
return result.status === "valid" ? result.claims : null;
|
|
41
|
+
}
|
|
42
|
+
__name(verifySessionCookie, "verifySessionCookie");
|
|
43
|
+
async function verifySessionCookieDetailed(rawCookieValue, options) {
|
|
44
|
+
if (!rawCookieValue || !options.secret || !globalThis.crypto?.subtle) {
|
|
45
|
+
return { status: "invalid", reason: "format" };
|
|
46
|
+
}
|
|
47
|
+
let sawSignedFormat = false;
|
|
48
|
+
let sawValidHmac = false;
|
|
40
49
|
for (const signedValue of cookieValueCandidates(rawCookieValue)) {
|
|
41
50
|
const lastDot = signedValue.lastIndexOf(".");
|
|
42
51
|
if (lastDot <= 0 || lastDot === signedValue.length - 1) continue;
|
|
52
|
+
sawSignedFormat = true;
|
|
43
53
|
const payload = signedValue.slice(0, lastDot);
|
|
44
54
|
const signature = signedValue.slice(lastDot + 1);
|
|
45
55
|
if (!await verifyHmac(payload, signature, options.secret)) continue;
|
|
46
|
-
|
|
56
|
+
sawValidHmac = true;
|
|
57
|
+
const claims = parseSessionCookiePayload(
|
|
47
58
|
payload,
|
|
48
59
|
options.maxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
49
60
|
options.now
|
|
50
61
|
);
|
|
62
|
+
if (claims) return { status: "valid", claims };
|
|
51
63
|
}
|
|
52
|
-
return
|
|
64
|
+
if (sawValidHmac) return { status: "invalid", reason: "payload" };
|
|
65
|
+
if (sawSignedFormat) return { status: "invalid", reason: "hmac" };
|
|
66
|
+
return { status: "invalid", reason: "format" };
|
|
53
67
|
}
|
|
54
|
-
__name(
|
|
68
|
+
__name(verifySessionCookieDetailed, "verifySessionCookieDetailed");
|
|
55
69
|
function readCookieValue(cookieHeader, name) {
|
|
56
70
|
for (const part of cookieHeader.split(";")) {
|
|
57
71
|
const separator = part.indexOf("=");
|
|
@@ -132,18 +146,28 @@ function isStringArray(value) {
|
|
|
132
146
|
__name(isStringArray, "isStringArray");
|
|
133
147
|
|
|
134
148
|
// src/client/sessionRecovery.ts
|
|
149
|
+
function resolveInternalRecoveryURL(explicit) {
|
|
150
|
+
if (explicit !== void 0) return explicit || void 0;
|
|
151
|
+
if (typeof process === "undefined") return void 0;
|
|
152
|
+
return process.env.NAJM_AUTH_INTERNAL_URL || void 0;
|
|
153
|
+
}
|
|
154
|
+
__name(resolveInternalRecoveryURL, "resolveInternalRecoveryURL");
|
|
135
155
|
async function requestSessionRecovery(options) {
|
|
136
156
|
if (!isCookieName(options.refreshCookieName) || !isCookieName(options.sessionCookieName)) {
|
|
157
|
+
reportFailure(options, { reason: "invalid-cookie-name" });
|
|
137
158
|
return { status: "unavailable" };
|
|
138
159
|
}
|
|
139
160
|
if (!isCookieValue(options.refreshCookieValue)) {
|
|
161
|
+
reportFailure(options, { reason: "invalid-refresh-cookie" });
|
|
140
162
|
return { status: "invalid" };
|
|
141
163
|
}
|
|
142
164
|
const endpoint = sameOriginRecoveryEndpoint(
|
|
143
165
|
options.endpoint,
|
|
144
|
-
options.requestOrigin
|
|
166
|
+
options.requestOrigin,
|
|
167
|
+
options.allowLoopbackEndpoint
|
|
145
168
|
);
|
|
146
169
|
if (!endpoint) {
|
|
170
|
+
reportFailure(options, { reason: "invalid-endpoint" });
|
|
147
171
|
return { status: "unavailable" };
|
|
148
172
|
}
|
|
149
173
|
let response;
|
|
@@ -158,30 +182,56 @@ async function requestSessionRecovery(options) {
|
|
|
158
182
|
cache: "no-store",
|
|
159
183
|
redirect: "manual"
|
|
160
184
|
});
|
|
161
|
-
} catch {
|
|
185
|
+
} catch (error) {
|
|
186
|
+
reportFailure(options, {
|
|
187
|
+
reason: "fetch-error",
|
|
188
|
+
error: safeErrorDetails(error, [
|
|
189
|
+
options.refreshCookieValue,
|
|
190
|
+
options.sessionSecret,
|
|
191
|
+
endpoint,
|
|
192
|
+
options.requestOrigin
|
|
193
|
+
])
|
|
194
|
+
});
|
|
162
195
|
return { status: "unavailable" };
|
|
163
196
|
}
|
|
164
197
|
if (!response.ok) {
|
|
165
198
|
const status = response.status;
|
|
199
|
+
reportFailure(options, { reason: "http-status", httpStatus: status });
|
|
166
200
|
return {
|
|
167
201
|
status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
|
|
168
202
|
httpStatus: status
|
|
169
203
|
};
|
|
170
204
|
}
|
|
171
205
|
const setCookie = response.headers.get("set-cookie");
|
|
172
|
-
if (!setCookie)
|
|
206
|
+
if (!setCookie) {
|
|
207
|
+
reportFailure(options, {
|
|
208
|
+
reason: "missing-set-cookie",
|
|
209
|
+
httpStatus: response.status
|
|
210
|
+
});
|
|
211
|
+
return { status: "unavailable", httpStatus: response.status };
|
|
212
|
+
}
|
|
173
213
|
const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
|
|
174
214
|
if (!sessionCookieValue) {
|
|
215
|
+
reportFailure(options, {
|
|
216
|
+
reason: "session-cookie-parse",
|
|
217
|
+
httpStatus: response.status
|
|
218
|
+
});
|
|
175
219
|
return { status: "unavailable", httpStatus: response.status };
|
|
176
220
|
}
|
|
177
|
-
const
|
|
221
|
+
const verification = await verifySessionCookieDetailed(sessionCookieValue, {
|
|
178
222
|
secret: options.sessionSecret,
|
|
179
223
|
maxAgeSeconds: options.sessionMaxAge
|
|
180
224
|
});
|
|
181
|
-
if (
|
|
225
|
+
if (verification.status === "invalid") {
|
|
226
|
+
reportFailure(options, {
|
|
227
|
+
reason: verification.reason === "hmac" ? "session-cookie-hmac" : verification.reason === "payload" ? "session-cookie-payload" : "session-cookie-parse",
|
|
228
|
+
httpStatus: response.status
|
|
229
|
+
});
|
|
230
|
+
return { status: "unavailable", httpStatus: response.status };
|
|
231
|
+
}
|
|
182
232
|
return {
|
|
183
233
|
status: "recovered",
|
|
184
|
-
claims,
|
|
234
|
+
claims: verification.claims,
|
|
185
235
|
setCookie,
|
|
186
236
|
sessionCookieValue
|
|
187
237
|
};
|
|
@@ -215,7 +265,7 @@ function isCookieValue(value) {
|
|
|
215
265
|
return value.length > 0 && /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/.test(value);
|
|
216
266
|
}
|
|
217
267
|
__name(isCookieValue, "isCookieValue");
|
|
218
|
-
function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
|
|
268
|
+
function sameOriginRecoveryEndpoint(endpoint, requestOrigin, allowLoopback = false) {
|
|
219
269
|
try {
|
|
220
270
|
const trusted = new URL(requestOrigin);
|
|
221
271
|
if (trusted.protocol !== "https:" && trusted.protocol !== "http:" || trusted.username || trusted.password) {
|
|
@@ -223,13 +273,64 @@ function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
|
|
|
223
273
|
}
|
|
224
274
|
const resolved = new URL(endpoint, trusted.origin);
|
|
225
275
|
if (resolved.username || resolved.password) return void 0;
|
|
226
|
-
if (resolved.origin !== trusted.origin
|
|
276
|
+
if (resolved.origin !== trusted.origin && (!allowLoopback || !isLoopbackURL(resolved))) {
|
|
277
|
+
return void 0;
|
|
278
|
+
}
|
|
227
279
|
return resolved.toString();
|
|
228
280
|
} catch {
|
|
229
281
|
return void 0;
|
|
230
282
|
}
|
|
231
283
|
}
|
|
232
284
|
__name(sameOriginRecoveryEndpoint, "sameOriginRecoveryEndpoint");
|
|
285
|
+
function isLoopbackURL(url) {
|
|
286
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return false;
|
|
287
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
288
|
+
}
|
|
289
|
+
__name(isLoopbackURL, "isLoopbackURL");
|
|
290
|
+
function reportFailure(options, failure) {
|
|
291
|
+
try {
|
|
292
|
+
options.onFailure?.(failure);
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
__name(reportFailure, "reportFailure");
|
|
297
|
+
function safeErrorDetails(value, sensitiveValues) {
|
|
298
|
+
const error = isRecord2(value) ? value : {};
|
|
299
|
+
const details = {
|
|
300
|
+
name: safeText(error.name, "Error", sensitiveValues),
|
|
301
|
+
message: safeText(error.message, "Session recovery fetch failed", sensitiveValues)
|
|
302
|
+
};
|
|
303
|
+
const code = safeOptionalText(error.code, sensitiveValues);
|
|
304
|
+
if (code) details.code = code;
|
|
305
|
+
if (isRecord2(error.cause)) {
|
|
306
|
+
const causeCode = safeOptionalText(error.cause.code, sensitiveValues);
|
|
307
|
+
details.cause = {
|
|
308
|
+
name: safeText(error.cause.name, "Error", sensitiveValues),
|
|
309
|
+
message: safeText(error.cause.message, "Session recovery fetch failed", sensitiveValues),
|
|
310
|
+
...causeCode ? { code: causeCode } : {}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return details;
|
|
314
|
+
}
|
|
315
|
+
__name(safeErrorDetails, "safeErrorDetails");
|
|
316
|
+
function safeText(value, fallback, sensitiveValues) {
|
|
317
|
+
if (typeof value !== "string" || !value) return fallback;
|
|
318
|
+
let safe = value.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\b(authorization|cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
|
|
319
|
+
for (const sensitive of sensitiveValues) {
|
|
320
|
+
if (sensitive) safe = safe.split(sensitive).join("[redacted]");
|
|
321
|
+
}
|
|
322
|
+
return safe.slice(0, 300);
|
|
323
|
+
}
|
|
324
|
+
__name(safeText, "safeText");
|
|
325
|
+
function safeOptionalText(value, sensitiveValues) {
|
|
326
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
327
|
+
return safeText(String(value), "", sensitiveValues);
|
|
328
|
+
}
|
|
329
|
+
__name(safeOptionalText, "safeOptionalText");
|
|
330
|
+
function isRecord2(value) {
|
|
331
|
+
return typeof value === "object" && value !== null;
|
|
332
|
+
}
|
|
333
|
+
__name(isRecord2, "isRecord");
|
|
233
334
|
|
|
234
335
|
// src/client/server/withAuthMiddleware.ts
|
|
235
336
|
function withAuthMiddleware(config) {
|
|
@@ -245,8 +346,11 @@ function withAuthMiddleware(config) {
|
|
|
245
346
|
sessionSecret,
|
|
246
347
|
sessionMaxAge,
|
|
247
348
|
verifyAlways = false,
|
|
248
|
-
recoveryURL
|
|
349
|
+
recoveryURL,
|
|
350
|
+
internalRecoveryURL,
|
|
351
|
+
onRecoveryFailure
|
|
249
352
|
} = config;
|
|
353
|
+
const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
|
|
250
354
|
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
251
355
|
const { NextResponse } = await import("next/server");
|
|
252
356
|
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
@@ -282,18 +386,20 @@ function withAuthMiddleware(config) {
|
|
|
282
386
|
let recovery = null;
|
|
283
387
|
if (!session || verifyAlways) {
|
|
284
388
|
const refreshCookie = readCookieValue(cookie, cookieName);
|
|
285
|
-
if (!refreshCookie || recoveryURL === false) {
|
|
389
|
+
if (!refreshCookie || recoveryURL === false && !resolvedInternalRecoveryURL) {
|
|
286
390
|
return redirectToLogin(returnPath, ["refresh", "session"]);
|
|
287
391
|
}
|
|
288
|
-
const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
|
|
392
|
+
const endpoint = resolvedInternalRecoveryURL ?? (recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url));
|
|
289
393
|
recovery = await requestSessionRecovery({
|
|
290
394
|
endpoint,
|
|
291
395
|
requestOrigin: url.origin,
|
|
396
|
+
allowLoopbackEndpoint: resolvedInternalRecoveryURL !== void 0,
|
|
292
397
|
refreshCookieName: cookieName,
|
|
293
398
|
refreshCookieValue: refreshCookie,
|
|
294
399
|
sessionCookieName,
|
|
295
400
|
sessionSecret: secret,
|
|
296
|
-
sessionMaxAge
|
|
401
|
+
sessionMaxAge,
|
|
402
|
+
onFailure: onRecoveryFailure
|
|
297
403
|
});
|
|
298
404
|
if (recovery.status !== "recovered") {
|
|
299
405
|
return redirectToLogin(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-0yzFb9CR.js';
|
|
2
|
-
|
|
2
|
+
import { SessionRecoveryFailure } from '../edge.js';
|
|
3
|
+
export { SessionRecoveryErrorDetails, SessionRecoveryFailureReason, withAuthMiddleware } from '../edge.js';
|
|
3
4
|
import 'next/server';
|
|
4
5
|
|
|
5
6
|
interface GetServerSessionOptions {
|
|
@@ -96,12 +97,16 @@ interface GetSessionConfig {
|
|
|
96
97
|
* `${baseURL}${authPrefix}/session/recover`. Set to false to disable fallback.
|
|
97
98
|
*/
|
|
98
99
|
recoveryURL?: string | false;
|
|
100
|
+
/** Loopback-only recovery endpoint for self-hosted reverse-proxy setups. */
|
|
101
|
+
internalRecoveryURL?: string;
|
|
99
102
|
/**
|
|
100
103
|
* Error handling mode:
|
|
101
104
|
* - 'nullable' (default): returns null on any failure
|
|
102
105
|
* - 'strict': throws typed errors for debugging
|
|
103
106
|
*/
|
|
104
107
|
mode?: 'nullable' | 'strict';
|
|
108
|
+
/** Secret-free diagnostic hook for failed recovery attempts. */
|
|
109
|
+
onRecoveryFailure?: (failure: SessionRecoveryFailure) => void;
|
|
105
110
|
}
|
|
106
111
|
declare class NoSessionError extends Error {
|
|
107
112
|
readonly code = "NO_SESSION";
|
|
@@ -181,6 +186,8 @@ interface DefineAuthConfig {
|
|
|
181
186
|
* `${apiBaseURL}${authPrefix}/session/recover`; false disables recovery.
|
|
182
187
|
*/
|
|
183
188
|
recoveryURL?: string | false;
|
|
189
|
+
/** Loopback-only recovery endpoint for self-hosted reverse-proxy setups. */
|
|
190
|
+
internalRecoveryURL?: string;
|
|
184
191
|
/** Next.js middleware matcher (default: exclude _next, favicon, api) */
|
|
185
192
|
matcher?: string[];
|
|
186
193
|
/**
|
|
@@ -188,6 +195,8 @@ interface DefineAuthConfig {
|
|
|
188
195
|
* Recovery reissues the signed cookie without rotating refresh tokens.
|
|
189
196
|
*/
|
|
190
197
|
verifyAlways?: boolean;
|
|
198
|
+
/** Secret-free diagnostic hook for failed server or proxy recovery. */
|
|
199
|
+
onRecoveryFailure?: (failure: SessionRecoveryFailure) => void;
|
|
191
200
|
}
|
|
192
201
|
interface AuthKit {
|
|
193
202
|
/**
|
|
@@ -223,4 +232,4 @@ interface AuthKit {
|
|
|
223
232
|
}
|
|
224
233
|
declare function defineAuth(authConfig?: DefineAuthConfig): AuthKit;
|
|
225
234
|
|
|
226
|
-
export { AuthConfigError, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type ServerSession, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getServerSession, getSession, withAuth };
|
|
235
|
+
export { AuthConfigError, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getServerSession, getSession, withAuth };
|
|
@@ -38,20 +38,33 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
async function verifySessionCookie(rawCookieValue, options) {
|
|
41
|
-
|
|
41
|
+
const result = await verifySessionCookieDetailed(rawCookieValue, options);
|
|
42
|
+
return result.status === "valid" ? result.claims : null;
|
|
43
|
+
}
|
|
44
|
+
async function verifySessionCookieDetailed(rawCookieValue, options) {
|
|
45
|
+
if (!rawCookieValue || !options.secret || !globalThis.crypto?.subtle) {
|
|
46
|
+
return { status: "invalid", reason: "format" };
|
|
47
|
+
}
|
|
48
|
+
let sawSignedFormat = false;
|
|
49
|
+
let sawValidHmac = false;
|
|
42
50
|
for (const signedValue of cookieValueCandidates(rawCookieValue)) {
|
|
43
51
|
const lastDot = signedValue.lastIndexOf(".");
|
|
44
52
|
if (lastDot <= 0 || lastDot === signedValue.length - 1) continue;
|
|
53
|
+
sawSignedFormat = true;
|
|
45
54
|
const payload = signedValue.slice(0, lastDot);
|
|
46
55
|
const signature = signedValue.slice(lastDot + 1);
|
|
47
56
|
if (!await verifyHmac(payload, signature, options.secret)) continue;
|
|
48
|
-
|
|
57
|
+
sawValidHmac = true;
|
|
58
|
+
const claims = parseSessionCookiePayload(
|
|
49
59
|
payload,
|
|
50
60
|
options.maxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
51
61
|
options.now
|
|
52
62
|
);
|
|
63
|
+
if (claims) return { status: "valid", claims };
|
|
53
64
|
}
|
|
54
|
-
return
|
|
65
|
+
if (sawValidHmac) return { status: "invalid", reason: "payload" };
|
|
66
|
+
if (sawSignedFormat) return { status: "invalid", reason: "hmac" };
|
|
67
|
+
return { status: "invalid", reason: "format" };
|
|
55
68
|
}
|
|
56
69
|
function readCookieValue(cookieHeader, name) {
|
|
57
70
|
for (const part of cookieHeader.split(";")) {
|
|
@@ -134,6 +147,7 @@ var init_sessionCookie = __esm({
|
|
|
134
147
|
__name(resolveSessionSecret, "resolveSessionSecret");
|
|
135
148
|
__name(parseSessionCookiePayload, "parseSessionCookiePayload");
|
|
136
149
|
__name(verifySessionCookie, "verifySessionCookie");
|
|
150
|
+
__name(verifySessionCookieDetailed, "verifySessionCookieDetailed");
|
|
137
151
|
__name(readCookieValue, "readCookieValue");
|
|
138
152
|
__name(verifyHmac, "verifyHmac");
|
|
139
153
|
__name(cookieValueCandidates, "cookieValueCandidates");
|
|
@@ -145,18 +159,27 @@ var init_sessionCookie = __esm({
|
|
|
145
159
|
});
|
|
146
160
|
|
|
147
161
|
// src/client/sessionRecovery.ts
|
|
162
|
+
function resolveInternalRecoveryURL(explicit) {
|
|
163
|
+
if (explicit !== void 0) return explicit || void 0;
|
|
164
|
+
if (typeof process === "undefined") return void 0;
|
|
165
|
+
return process.env.NAJM_AUTH_INTERNAL_URL || void 0;
|
|
166
|
+
}
|
|
148
167
|
async function requestSessionRecovery(options) {
|
|
149
168
|
if (!isCookieName(options.refreshCookieName) || !isCookieName(options.sessionCookieName)) {
|
|
169
|
+
reportFailure(options, { reason: "invalid-cookie-name" });
|
|
150
170
|
return { status: "unavailable" };
|
|
151
171
|
}
|
|
152
172
|
if (!isCookieValue(options.refreshCookieValue)) {
|
|
173
|
+
reportFailure(options, { reason: "invalid-refresh-cookie" });
|
|
153
174
|
return { status: "invalid" };
|
|
154
175
|
}
|
|
155
176
|
const endpoint = sameOriginRecoveryEndpoint(
|
|
156
177
|
options.endpoint,
|
|
157
|
-
options.requestOrigin
|
|
178
|
+
options.requestOrigin,
|
|
179
|
+
options.allowLoopbackEndpoint
|
|
158
180
|
);
|
|
159
181
|
if (!endpoint) {
|
|
182
|
+
reportFailure(options, { reason: "invalid-endpoint" });
|
|
160
183
|
return { status: "unavailable" };
|
|
161
184
|
}
|
|
162
185
|
let response;
|
|
@@ -171,30 +194,56 @@ async function requestSessionRecovery(options) {
|
|
|
171
194
|
cache: "no-store",
|
|
172
195
|
redirect: "manual"
|
|
173
196
|
});
|
|
174
|
-
} catch {
|
|
197
|
+
} catch (error) {
|
|
198
|
+
reportFailure(options, {
|
|
199
|
+
reason: "fetch-error",
|
|
200
|
+
error: safeErrorDetails(error, [
|
|
201
|
+
options.refreshCookieValue,
|
|
202
|
+
options.sessionSecret,
|
|
203
|
+
endpoint,
|
|
204
|
+
options.requestOrigin
|
|
205
|
+
])
|
|
206
|
+
});
|
|
175
207
|
return { status: "unavailable" };
|
|
176
208
|
}
|
|
177
209
|
if (!response.ok) {
|
|
178
210
|
const status = response.status;
|
|
211
|
+
reportFailure(options, { reason: "http-status", httpStatus: status });
|
|
179
212
|
return {
|
|
180
213
|
status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
|
|
181
214
|
httpStatus: status
|
|
182
215
|
};
|
|
183
216
|
}
|
|
184
217
|
const setCookie = response.headers.get("set-cookie");
|
|
185
|
-
if (!setCookie)
|
|
218
|
+
if (!setCookie) {
|
|
219
|
+
reportFailure(options, {
|
|
220
|
+
reason: "missing-set-cookie",
|
|
221
|
+
httpStatus: response.status
|
|
222
|
+
});
|
|
223
|
+
return { status: "unavailable", httpStatus: response.status };
|
|
224
|
+
}
|
|
186
225
|
const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
|
|
187
226
|
if (!sessionCookieValue) {
|
|
227
|
+
reportFailure(options, {
|
|
228
|
+
reason: "session-cookie-parse",
|
|
229
|
+
httpStatus: response.status
|
|
230
|
+
});
|
|
188
231
|
return { status: "unavailable", httpStatus: response.status };
|
|
189
232
|
}
|
|
190
|
-
const
|
|
233
|
+
const verification = await verifySessionCookieDetailed(sessionCookieValue, {
|
|
191
234
|
secret: options.sessionSecret,
|
|
192
235
|
maxAgeSeconds: options.sessionMaxAge
|
|
193
236
|
});
|
|
194
|
-
if (
|
|
237
|
+
if (verification.status === "invalid") {
|
|
238
|
+
reportFailure(options, {
|
|
239
|
+
reason: verification.reason === "hmac" ? "session-cookie-hmac" : verification.reason === "payload" ? "session-cookie-payload" : "session-cookie-parse",
|
|
240
|
+
httpStatus: response.status
|
|
241
|
+
});
|
|
242
|
+
return { status: "unavailable", httpStatus: response.status };
|
|
243
|
+
}
|
|
195
244
|
return {
|
|
196
245
|
status: "recovered",
|
|
197
|
-
claims,
|
|
246
|
+
claims: verification.claims,
|
|
198
247
|
setCookie,
|
|
199
248
|
sessionCookieValue
|
|
200
249
|
};
|
|
@@ -222,7 +271,7 @@ function isCookieName(value) {
|
|
|
222
271
|
function isCookieValue(value) {
|
|
223
272
|
return value.length > 0 && /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/.test(value);
|
|
224
273
|
}
|
|
225
|
-
function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
|
|
274
|
+
function sameOriginRecoveryEndpoint(endpoint, requestOrigin, allowLoopback = false) {
|
|
226
275
|
try {
|
|
227
276
|
const trusted = new URL(requestOrigin);
|
|
228
277
|
if (trusted.protocol !== "https:" && trusted.protocol !== "http:" || trusted.username || trusted.password) {
|
|
@@ -230,15 +279,61 @@ function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
|
|
|
230
279
|
}
|
|
231
280
|
const resolved = new URL(endpoint, trusted.origin);
|
|
232
281
|
if (resolved.username || resolved.password) return void 0;
|
|
233
|
-
if (resolved.origin !== trusted.origin
|
|
282
|
+
if (resolved.origin !== trusted.origin && (!allowLoopback || !isLoopbackURL(resolved))) {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
234
285
|
return resolved.toString();
|
|
235
286
|
} catch {
|
|
236
287
|
return void 0;
|
|
237
288
|
}
|
|
238
289
|
}
|
|
290
|
+
function isLoopbackURL(url) {
|
|
291
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return false;
|
|
292
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
293
|
+
}
|
|
294
|
+
function reportFailure(options, failure) {
|
|
295
|
+
try {
|
|
296
|
+
options.onFailure?.(failure);
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function safeErrorDetails(value, sensitiveValues) {
|
|
301
|
+
const error = isRecord2(value) ? value : {};
|
|
302
|
+
const details = {
|
|
303
|
+
name: safeText(error.name, "Error", sensitiveValues),
|
|
304
|
+
message: safeText(error.message, "Session recovery fetch failed", sensitiveValues)
|
|
305
|
+
};
|
|
306
|
+
const code = safeOptionalText(error.code, sensitiveValues);
|
|
307
|
+
if (code) details.code = code;
|
|
308
|
+
if (isRecord2(error.cause)) {
|
|
309
|
+
const causeCode = safeOptionalText(error.cause.code, sensitiveValues);
|
|
310
|
+
details.cause = {
|
|
311
|
+
name: safeText(error.cause.name, "Error", sensitiveValues),
|
|
312
|
+
message: safeText(error.cause.message, "Session recovery fetch failed", sensitiveValues),
|
|
313
|
+
...causeCode ? { code: causeCode } : {}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return details;
|
|
317
|
+
}
|
|
318
|
+
function safeText(value, fallback, sensitiveValues) {
|
|
319
|
+
if (typeof value !== "string" || !value) return fallback;
|
|
320
|
+
let safe = value.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\b(authorization|cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
|
|
321
|
+
for (const sensitive of sensitiveValues) {
|
|
322
|
+
if (sensitive) safe = safe.split(sensitive).join("[redacted]");
|
|
323
|
+
}
|
|
324
|
+
return safe.slice(0, 300);
|
|
325
|
+
}
|
|
326
|
+
function safeOptionalText(value, sensitiveValues) {
|
|
327
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
328
|
+
return safeText(String(value), "", sensitiveValues);
|
|
329
|
+
}
|
|
330
|
+
function isRecord2(value) {
|
|
331
|
+
return typeof value === "object" && value !== null;
|
|
332
|
+
}
|
|
239
333
|
var init_sessionRecovery = __esm({
|
|
240
334
|
"src/client/sessionRecovery.ts"() {
|
|
241
335
|
init_sessionCookie();
|
|
336
|
+
__name(resolveInternalRecoveryURL, "resolveInternalRecoveryURL");
|
|
242
337
|
__name(requestSessionRecovery, "requestSessionRecovery");
|
|
243
338
|
__name(authEndpoint, "authEndpoint");
|
|
244
339
|
__name(replaceCookieValue, "replaceCookieValue");
|
|
@@ -246,6 +341,12 @@ var init_sessionRecovery = __esm({
|
|
|
246
341
|
__name(isCookieName, "isCookieName");
|
|
247
342
|
__name(isCookieValue, "isCookieValue");
|
|
248
343
|
__name(sameOriginRecoveryEndpoint, "sameOriginRecoveryEndpoint");
|
|
344
|
+
__name(isLoopbackURL, "isLoopbackURL");
|
|
345
|
+
__name(reportFailure, "reportFailure");
|
|
346
|
+
__name(safeErrorDetails, "safeErrorDetails");
|
|
347
|
+
__name(safeText, "safeText");
|
|
348
|
+
__name(safeOptionalText, "safeOptionalText");
|
|
349
|
+
__name(isRecord2, "isRecord");
|
|
249
350
|
}
|
|
250
351
|
});
|
|
251
352
|
|
|
@@ -286,6 +387,7 @@ async function getSession(config = {}) {
|
|
|
286
387
|
const baseURL = config.baseURL ?? defaultBaseURL();
|
|
287
388
|
const prefix = config.authPrefix ?? "/auth";
|
|
288
389
|
const strict = config.mode === "strict";
|
|
390
|
+
const internalRecoveryURL = resolveInternalRecoveryURL(config.internalRecoveryURL);
|
|
289
391
|
let sessionCookieValue;
|
|
290
392
|
let refreshCookieValue;
|
|
291
393
|
let requestOrigin;
|
|
@@ -319,7 +421,7 @@ async function getSession(config = {}) {
|
|
|
319
421
|
if (strict) throw new AuthConfigError("Session cookie secret is not configured");
|
|
320
422
|
return null;
|
|
321
423
|
}
|
|
322
|
-
if (!refreshCookieValue || config.recoveryURL === false) {
|
|
424
|
+
if (!refreshCookieValue || config.recoveryURL === false && !internalRecoveryURL) {
|
|
323
425
|
if (strict) throw new NoSessionError("No recoverable refresh session");
|
|
324
426
|
return null;
|
|
325
427
|
}
|
|
@@ -327,15 +429,17 @@ async function getSession(config = {}) {
|
|
|
327
429
|
if (strict) throw new AuthConfigError("Incoming request origin is unavailable");
|
|
328
430
|
return null;
|
|
329
431
|
}
|
|
330
|
-
const endpoint = config.recoveryURL ? new URL(config.recoveryURL, requestOrigin).toString() : authEndpoint(baseURL, prefix, "/session/recover", requestOrigin);
|
|
432
|
+
const endpoint = internalRecoveryURL ?? (config.recoveryURL ? new URL(config.recoveryURL, requestOrigin).toString() : authEndpoint(baseURL, prefix, "/session/recover", requestOrigin));
|
|
331
433
|
const recovery = await requestSessionRecovery({
|
|
332
434
|
endpoint,
|
|
333
435
|
requestOrigin,
|
|
436
|
+
allowLoopbackEndpoint: internalRecoveryURL !== void 0,
|
|
334
437
|
refreshCookieName: cookieName,
|
|
335
438
|
refreshCookieValue,
|
|
336
439
|
sessionCookieName,
|
|
337
440
|
sessionSecret: secret,
|
|
338
|
-
sessionMaxAge: config.sessionMaxAge
|
|
441
|
+
sessionMaxAge: config.sessionMaxAge,
|
|
442
|
+
onFailure: config.onRecoveryFailure
|
|
339
443
|
});
|
|
340
444
|
if (recovery.status === "recovered") {
|
|
341
445
|
return {
|
|
@@ -603,8 +707,11 @@ function withAuthMiddleware(config) {
|
|
|
603
707
|
sessionSecret,
|
|
604
708
|
sessionMaxAge,
|
|
605
709
|
verifyAlways = false,
|
|
606
|
-
recoveryURL
|
|
710
|
+
recoveryURL,
|
|
711
|
+
internalRecoveryURL,
|
|
712
|
+
onRecoveryFailure
|
|
607
713
|
} = config;
|
|
714
|
+
const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
|
|
608
715
|
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
609
716
|
const { NextResponse } = await import("next/server");
|
|
610
717
|
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
@@ -640,18 +747,20 @@ function withAuthMiddleware(config) {
|
|
|
640
747
|
let recovery = null;
|
|
641
748
|
if (!session || verifyAlways) {
|
|
642
749
|
const refreshCookie = readCookieValue(cookie, cookieName);
|
|
643
|
-
if (!refreshCookie || recoveryURL === false) {
|
|
750
|
+
if (!refreshCookie || recoveryURL === false && !resolvedInternalRecoveryURL) {
|
|
644
751
|
return redirectToLogin(returnPath, ["refresh", "session"]);
|
|
645
752
|
}
|
|
646
|
-
const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
|
|
753
|
+
const endpoint = resolvedInternalRecoveryURL ?? (recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url));
|
|
647
754
|
recovery = await requestSessionRecovery({
|
|
648
755
|
endpoint,
|
|
649
756
|
requestOrigin: url.origin,
|
|
757
|
+
allowLoopbackEndpoint: resolvedInternalRecoveryURL !== void 0,
|
|
650
758
|
refreshCookieName: cookieName,
|
|
651
759
|
refreshCookieValue: refreshCookie,
|
|
652
760
|
sessionCookieName,
|
|
653
761
|
sessionSecret: secret,
|
|
654
|
-
sessionMaxAge
|
|
762
|
+
sessionMaxAge,
|
|
763
|
+
onFailure: onRecoveryFailure
|
|
655
764
|
});
|
|
656
765
|
if (recovery.status !== "recovered") {
|
|
657
766
|
return redirectToLogin(
|
|
@@ -1212,8 +1321,10 @@ function defineAuth(authConfig = {}) {
|
|
|
1212
1321
|
sessionSecret,
|
|
1213
1322
|
sessionMaxAge,
|
|
1214
1323
|
recoveryURL,
|
|
1324
|
+
internalRecoveryURL,
|
|
1215
1325
|
matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
|
|
1216
1326
|
verifyAlways = false,
|
|
1327
|
+
onRecoveryFailure,
|
|
1217
1328
|
refreshThreshold,
|
|
1218
1329
|
tabSync,
|
|
1219
1330
|
channelName,
|
|
@@ -1227,7 +1338,9 @@ function defineAuth(authConfig = {}) {
|
|
|
1227
1338
|
sessionCookieName,
|
|
1228
1339
|
sessionSecret,
|
|
1229
1340
|
sessionMaxAge,
|
|
1230
|
-
recoveryURL
|
|
1341
|
+
recoveryURL,
|
|
1342
|
+
internalRecoveryURL,
|
|
1343
|
+
onRecoveryFailure
|
|
1231
1344
|
};
|
|
1232
1345
|
let _client = null;
|
|
1233
1346
|
const getClient = /* @__PURE__ */ __name(() => {
|
|
@@ -1281,7 +1394,9 @@ function defineAuth(authConfig = {}) {
|
|
|
1281
1394
|
sessionSecret,
|
|
1282
1395
|
sessionMaxAge,
|
|
1283
1396
|
recoveryURL,
|
|
1284
|
-
|
|
1397
|
+
internalRecoveryURL,
|
|
1398
|
+
verifyAlways,
|
|
1399
|
+
onRecoveryFailure
|
|
1285
1400
|
});
|
|
1286
1401
|
const protect = /* @__PURE__ */ __name((Page, options) => {
|
|
1287
1402
|
return /* @__PURE__ */ __name(async function ProtectedPage(props) {
|