najm-auth 3.4.0 → 4.0.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 +100 -47
- package/dist/client/edge.d.ts +15 -2
- package/dist/client/edge.js +25 -7
- package/dist/client/server/index.d.ts +4 -4
- package/dist/client/server/index.js +25 -7
- package/dist/index.d.ts +312 -97
- package/dist/index.js +1232 -767
- package/dist/schema/pg.d.ts +2 -2
- package/dist/schema/sqlite.d.ts +4 -4
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -97,11 +97,11 @@ FRONTEND_URL=https://app.example.com
|
|
|
97
97
|
GOOGLE_CLIENT_ID=<google-web-client-id>
|
|
98
98
|
GOOGLE_CLIENT_SECRET=<google-web-client-secret>
|
|
99
99
|
# Optional for a split frontend/API deployment. Otherwise FRONTEND_URL is used.
|
|
100
|
-
GOOGLE_CALLBACK_URL=https://app.example.com/api/auth/oauth/google/callback
|
|
101
|
-
# Optional GitHub sign-in
|
|
102
|
-
GITHUB_CLIENT_ID=<github-oauth-app-client-id>
|
|
103
|
-
GITHUB_CLIENT_SECRET=<github-oauth-app-client-secret>
|
|
104
|
-
GITHUB_CALLBACK_URL=https://app.example.com/api/auth/oauth/github/callback
|
|
100
|
+
GOOGLE_CALLBACK_URL=https://app.example.com/api/auth/oauth/google/callback
|
|
101
|
+
# Optional GitHub sign-in
|
|
102
|
+
GITHUB_CLIENT_ID=<github-oauth-app-client-id>
|
|
103
|
+
GITHUB_CLIENT_SECRET=<github-oauth-app-client-secret>
|
|
104
|
+
GITHUB_CALLBACK_URL=https://app.example.com/api/auth/oauth/github/callback
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
> ⚠️ **Security:** Generate secrets with `openssl rand -base64 32`
|
|
@@ -156,7 +156,7 @@ auth({
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
// Optional external identity providers
|
|
159
|
+
// Optional external identity providers
|
|
160
160
|
oauth?: {
|
|
161
161
|
google?: true | {
|
|
162
162
|
clientId?: string // Or GOOGLE_CLIENT_ID
|
|
@@ -166,17 +166,17 @@ auth({
|
|
|
166
166
|
errorRedirectPath?: string // Default: /login
|
|
167
167
|
allowSignup?: boolean // Default: true
|
|
168
168
|
autoLinkVerifiedEmail?: boolean // Default: false
|
|
169
|
-
allowedHostedDomains?: string[] // Validates the Google hd claim
|
|
170
|
-
}
|
|
171
|
-
github?: true | {
|
|
172
|
-
clientId?: string // Or GITHUB_CLIENT_ID
|
|
173
|
-
clientSecret?: string // Or GITHUB_CLIENT_SECRET
|
|
174
|
-
callbackUrl?: string // Or GITHUB_CALLBACK_URL
|
|
175
|
-
frontendCallbackPath?: string // Default: /auth/oauth/callback
|
|
176
|
-
errorRedirectPath?: string // Default: /login
|
|
177
|
-
allowSignup?: boolean // Default: true
|
|
178
|
-
autoLinkVerifiedEmail?: boolean // Default: false
|
|
179
|
-
}
|
|
169
|
+
allowedHostedDomains?: string[] // Validates the Google hd claim
|
|
170
|
+
}
|
|
171
|
+
github?: true | {
|
|
172
|
+
clientId?: string // Or GITHUB_CLIENT_ID
|
|
173
|
+
clientSecret?: string // Or GITHUB_CLIENT_SECRET
|
|
174
|
+
callbackUrl?: string // Or GITHUB_CALLBACK_URL
|
|
175
|
+
frontendCallbackPath?: string // Default: /auth/oauth/callback
|
|
176
|
+
errorRedirectPath?: string // Default: /login
|
|
177
|
+
allowSignup?: boolean // Default: true
|
|
178
|
+
autoLinkVerifiedEmail?: boolean // Default: false
|
|
179
|
+
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// Dependencies (forwarded to plugins)
|
|
@@ -346,22 +346,22 @@ import { GoogleLoginButton } from 'najm-auth/client/react';
|
|
|
346
346
|
Google accounts are keyed by Google's stable `sub` claim. If an existing Najm
|
|
347
347
|
user has the same email but is not linked, sign-in fails with
|
|
348
348
|
`oauth_account_link_required` by default. After password login, call
|
|
349
|
-
`client.linkOAuthAccount('google')` to prove control of both accounts. Setting
|
|
350
|
-
`autoLinkVerifiedEmail: true` opts into verified-email linking.
|
|
351
|
-
|
|
352
|
-
### GitHub Sign-In
|
|
353
|
-
|
|
354
|
-
Enable GitHub with `oauth: { github: true }`. Credentials come from
|
|
355
|
-
`GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`; the callback defaults to
|
|
356
|
-
`${FRONTEND_URL}/api/auth/oauth/github/callback`. Register that exact callback
|
|
357
|
-
on the GitHub OAuth App, and set `GITHUB_CALLBACK_URL` only for a split-origin
|
|
358
|
-
deployment. GitHub login uses authorization code plus PKCE, requests
|
|
359
|
-
`user:email`, requires a verified primary email, and keys the durable provider
|
|
360
|
-
link by GitHub's numeric user ID.
|
|
361
|
-
|
|
362
|
-
The client exposes `loginWithGitHub()`, `useGitHubLogin()`, and the headless
|
|
363
|
-
`GitHubLoginButton`; the generic `linkOAuthAccount('github')` method links an
|
|
364
|
-
authenticated Najm user.
|
|
349
|
+
`client.linkOAuthAccount('google')` to prove control of both accounts. Setting
|
|
350
|
+
`autoLinkVerifiedEmail: true` opts into verified-email linking.
|
|
351
|
+
|
|
352
|
+
### GitHub Sign-In
|
|
353
|
+
|
|
354
|
+
Enable GitHub with `oauth: { github: true }`. Credentials come from
|
|
355
|
+
`GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`; the callback defaults to
|
|
356
|
+
`${FRONTEND_URL}/api/auth/oauth/github/callback`. Register that exact callback
|
|
357
|
+
on the GitHub OAuth App, and set `GITHUB_CALLBACK_URL` only for a split-origin
|
|
358
|
+
deployment. GitHub login uses authorization code plus PKCE, requests
|
|
359
|
+
`user:email`, requires a verified primary email, and keys the durable provider
|
|
360
|
+
link by GitHub's numeric user ID.
|
|
361
|
+
|
|
362
|
+
The client exposes `loginWithGitHub()`, `useGitHubLogin()`, and the headless
|
|
363
|
+
`GitHubLoginButton`; the generic `linkOAuthAccount('github')` method links an
|
|
364
|
+
authenticated Najm user.
|
|
365
365
|
|
|
366
366
|
### Admin Routes (all require `@isAdmin()`)
|
|
367
367
|
|
|
@@ -664,7 +664,7 @@ credential_setup_requirements
|
|
|
664
664
|
Existing databases must generate and run a migration after upgrading so the
|
|
665
665
|
new `oauth_accounts`, `credential_setup_sessions`, and
|
|
666
666
|
`credential_setup_requirements` tables exist. Custom `AuthSchema` objects may
|
|
667
|
-
omit `oauthAccounts` while OAuth is disabled, but provider configuration fails
|
|
667
|
+
omit `oauthAccounts` while OAuth is disabled, but provider configuration fails
|
|
668
668
|
fast unless the custom schema supplies it. Both credential-setup tables are
|
|
669
669
|
required of a custom schema, because the setup flow is always mounted.
|
|
670
670
|
|
|
@@ -849,6 +849,31 @@ Next.js 16 requires the exported Proxy `config` to be a statically analyzable
|
|
|
849
849
|
object literal. Turbopack rejects `export const config = auth.config`, so the
|
|
850
850
|
matcher is the one integration value that cannot be composed at runtime.
|
|
851
851
|
|
|
852
|
+
When a Proxy generates request-scoped headers for the downstream render, pass
|
|
853
|
+
only the overrides as the optional second argument. Najm merges them over the
|
|
854
|
+
incoming request and preserves them when session recovery replaces the cookie.
|
|
855
|
+
The application still owns any matching response header:
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
export default async function proxy(request: Request) {
|
|
859
|
+
const nonce = btoa(globalThis.crypto.randomUUID());
|
|
860
|
+
const policy = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`;
|
|
861
|
+
const response = await auth.proxy(request, {
|
|
862
|
+
requestHeaders: {
|
|
863
|
+
'content-security-policy': policy,
|
|
864
|
+
'x-nonce': nonce,
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
response.headers.set('Content-Security-Policy', policy);
|
|
868
|
+
return response;
|
|
869
|
+
}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
Authentication reads the original request. `requestHeaders` controls only what
|
|
873
|
+
the successful Next.js render receives. Attempts to override `cookie` or
|
|
874
|
+
`authorization` fail closed; only Najm's validated recovery path may replace
|
|
875
|
+
the cookie after it authorizes the recovered session.
|
|
876
|
+
|
|
852
877
|
```typescript
|
|
853
878
|
// src/app/api/[...route]/route.ts
|
|
854
879
|
import { handle } from 'najm-core';
|
|
@@ -1052,8 +1077,9 @@ throw new HttpError(403, 'Insufficient permissions for this action');
|
|
|
1052
1077
|
- Login uses a dummy password hash for missing users to reduce timing leaks.
|
|
1053
1078
|
- Forgot-password responses avoid email enumeration.
|
|
1054
1079
|
- Auth routes register `najm-rate` and ship route-level brute-force limits.
|
|
1055
|
-
- Session cookies are signed
|
|
1056
|
-
|
|
1080
|
+
- Session cookies are signed, short-lived, and bound to their refresh-token
|
|
1081
|
+
family; server auth resolution checks both the session version and positive
|
|
1082
|
+
family liveness.
|
|
1057
1083
|
- Expired signed sessions recover through authoritative, non-rotating refresh
|
|
1058
1084
|
validation; middleware verifies the reissued HMAC before using its claims.
|
|
1059
1085
|
- Server-side recovery sends only the configured refresh cookie and accepts
|
|
@@ -1092,17 +1118,27 @@ errors cannot change the authentication result.
|
|
|
1092
1118
|
|
|
1093
1119
|
### Password Reset Tokens
|
|
1094
1120
|
|
|
1095
|
-
|
|
1121
|
+
Reset and invite links are signed JWTs whose `jti` is stored in the configured
|
|
1122
|
+
cache with the same expiry. `consumeSetPasswordToken()` atomically compares and
|
|
1123
|
+
deletes that value while preserving whether the token was a reset or invite;
|
|
1124
|
+
the backward-compatible `verifyResetToken()` returns only the user id. Exactly
|
|
1125
|
+
one concurrent caller can consume a link, and a stale link cannot delete the
|
|
1126
|
+
value for a newer one.
|
|
1096
1127
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1128
|
+
`AuthService.resetPassword()` validates the replacement password before
|
|
1129
|
+
consumption. Once consumed, a token stays consumed even if the later user
|
|
1130
|
+
mutation fails; restoring it would make the link replayable, so the user must
|
|
1131
|
+
request a new one.
|
|
1132
|
+
|
|
1133
|
+
Accepting an account invitation also marks the destination email verified and
|
|
1134
|
+
activates the account when its status is `pending`. An ordinary password reset
|
|
1135
|
+
changes neither verification nor lifecycle status, and an explicitly inactive
|
|
1136
|
+
invited account remains inactive.
|
|
1137
|
+
|
|
1138
|
+
The built-in memory and Redis drivers implement the required atomic primitive.
|
|
1139
|
+
A custom cache driver may omit `compareAndDelete()` for compatibility with
|
|
1140
|
+
unrelated cache usage, but reset and invite consumption then fails closed. Do
|
|
1141
|
+
not emulate this operation with separate `get()` and `del()` calls.
|
|
1106
1142
|
|
|
1107
1143
|
### Purpose-Bound Credential Setup
|
|
1108
1144
|
|
|
@@ -1152,8 +1188,9 @@ consuming it; `cancel()` revokes it and clears the cookie.
|
|
|
1152
1188
|
### Session Management
|
|
1153
1189
|
|
|
1154
1190
|
- Sessions are multi-device: the token table stores one refresh row per login session (keyed by a unique `tokenFamily`), so a user can stay logged in on several devices at once. Logout and rotation are scoped to the current session; password change/reset revoke every session
|
|
1191
|
+
- Revocation changes the refresh row to a durable `revoked` tombstone until its original expiry. Active-session reads and rotations require `status = active`, so losing Redis cannot revive a logged-out database session; expired tombstones are removed by normal session cleanup
|
|
1155
1192
|
- A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
|
|
1156
|
-
- The signed session cookie is accepted
|
|
1193
|
+
- The signed session cookie is accepted on the fast path only while Redis positively identifies its family as live and owned by the same user; an unknown cache state falls back to authoritative refresh-row recovery
|
|
1157
1194
|
- Use `@RateLimit` on logout for DDoS protection
|
|
1158
1195
|
|
|
1159
1196
|
### Token Blacklist
|
|
@@ -1177,8 +1214,24 @@ consuming it; `cancel()` revokes it and clears the cookie.
|
|
|
1177
1214
|
```bash
|
|
1178
1215
|
bun run test # Run all tests
|
|
1179
1216
|
bun run test:auth # Run auth tests only
|
|
1217
|
+
bun run --cwd packages/najm-auth test:real-infra # Opt-in PostgreSQL + Redis races
|
|
1218
|
+
bun packages/najm-auth/integration/mailpit-forgot-password/run.ts # Loopback Redis + Mailpit HTTP acceptance
|
|
1180
1219
|
```
|
|
1181
1220
|
|
|
1221
|
+
The real-infrastructure suite runs only with `NAJM_AUTH_REAL_INFRA=1`. Supply
|
|
1222
|
+
loopback-only `NAJM_AUTH_REAL_POSTGRES_URL` and `NAJM_AUTH_REAL_REDIS_URL` (or
|
|
1223
|
+
the conventional `DATABASE_URL` and `REDIS_URL`). It creates and drops its own
|
|
1224
|
+
randomly named PostgreSQL database and cleans only its unique Redis key prefix;
|
|
1225
|
+
remote endpoints fail before either service is touched.
|
|
1226
|
+
|
|
1227
|
+
The Mailpit acceptance runner requires Redis on `127.0.0.1:6399`, Mailpit SMTP
|
|
1228
|
+
on `127.0.0.1:1025`, and the Mailpit API on `127.0.0.1:8025` by default. It
|
|
1229
|
+
boots the real auth plugin over HTTP with an ephemeral SQLite fixture, proves
|
|
1230
|
+
ignored fields and spoofed forwarding headers cannot buy more reset emails,
|
|
1231
|
+
and removes only its run-specific messages and Redis keys. The three endpoints
|
|
1232
|
+
can be changed with the `NAJM_AUTH_MAILPIT_*` variables, but non-loopback
|
|
1233
|
+
values fail before the fixture is created.
|
|
1234
|
+
|
|
1182
1235
|
Test files include:
|
|
1183
1236
|
- `schema.test.ts` — Schema exports validation
|
|
1184
1237
|
- `auth.test.ts` — Authentication flow
|
package/dist/client/edge.d.ts
CHANGED
|
@@ -3,6 +3,19 @@ import { S as SessionRecoveryFailure } from '../sessionRecovery-D5Fa0yZ1.js';
|
|
|
3
3
|
export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../sessionRecovery-D5Fa0yZ1.js';
|
|
4
4
|
|
|
5
5
|
type ProxySessionMode = 'optimistic' | 'authoritative';
|
|
6
|
+
/**
|
|
7
|
+
* Per-request headers an application proxy needs to expose to the downstream
|
|
8
|
+
* Next.js render. This is primarily useful for request-scoped values such as a
|
|
9
|
+
* Content Security Policy nonce.
|
|
10
|
+
*
|
|
11
|
+
* Values are merged over the incoming request headers. Authentication always
|
|
12
|
+
* evaluates the original request, so these overrides cannot change which
|
|
13
|
+
* principal or cookie Najm authorizes. `authorization` and `cookie` are
|
|
14
|
+
* rejected here; only Najm's validated recovery path may replace the cookie.
|
|
15
|
+
*/
|
|
16
|
+
interface AuthProxyOptions {
|
|
17
|
+
requestHeaders?: HeadersInit;
|
|
18
|
+
}
|
|
6
19
|
interface AuthMiddlewareConfig {
|
|
7
20
|
/** Routes that require authentication (glob patterns) */
|
|
8
21
|
protectedRoutes?: string[];
|
|
@@ -73,6 +86,6 @@ interface AuthMiddlewareConfig {
|
|
|
73
86
|
* };
|
|
74
87
|
* ```
|
|
75
88
|
*/
|
|
76
|
-
declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
|
|
89
|
+
declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request, options?: AuthProxyOptions) => Promise<next_server.NextResponse<unknown>>;
|
|
77
90
|
|
|
78
|
-
export { type AuthMiddlewareConfig, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
|
|
91
|
+
export { type AuthMiddlewareConfig, type AuthProxyOptions, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
|
package/dist/client/edge.js
CHANGED
|
@@ -19,6 +19,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
19
19
|
if (!isRecord(data) || !isValidUser(data.user)) return null;
|
|
20
20
|
if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
|
|
21
21
|
if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
|
|
22
|
+
if (typeof data.tokenFamily !== "string" || !data.tokenFamily) return null;
|
|
22
23
|
if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
|
|
23
24
|
const issuedAt = data.iat;
|
|
24
25
|
if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
|
|
@@ -28,6 +29,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
28
29
|
roles: [...data.roles],
|
|
29
30
|
permissions: [...data.permissions],
|
|
30
31
|
sessionVersion: data.sessionVersion,
|
|
32
|
+
tokenFamily: data.tokenFamily,
|
|
31
33
|
iat: issuedAt
|
|
32
34
|
};
|
|
33
35
|
} catch {
|
|
@@ -353,8 +355,25 @@ function withAuthMiddleware(config) {
|
|
|
353
355
|
} = config;
|
|
354
356
|
const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
|
|
355
357
|
const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
|
|
356
|
-
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
358
|
+
return /* @__PURE__ */ __name(async function middleware(request, options = {}) {
|
|
357
359
|
const { NextResponse } = await import("next/server");
|
|
360
|
+
const downstreamHeaders = new Headers(request.headers);
|
|
361
|
+
if (options.requestHeaders) {
|
|
362
|
+
const overrides = new Headers(options.requestHeaders);
|
|
363
|
+
for (const identityHeader of ["authorization", "cookie"]) {
|
|
364
|
+
if (overrides.has(identityHeader)) {
|
|
365
|
+
throw new TypeError(
|
|
366
|
+
`Auth proxy requestHeaders cannot override ${identityHeader}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
overrides.forEach((value, key) => {
|
|
371
|
+
downstreamHeaders.set(key, value);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const continueRequest = /* @__PURE__ */ __name(() => NextResponse.next({
|
|
375
|
+
request: { headers: downstreamHeaders }
|
|
376
|
+
}), "continueRequest");
|
|
358
377
|
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
359
378
|
const loginUrl = new URL(loginRoute, request.url);
|
|
360
379
|
loginUrl.searchParams.set("from", returnPath2);
|
|
@@ -371,10 +390,10 @@ function withAuthMiddleware(config) {
|
|
|
371
390
|
const pathname = url.pathname;
|
|
372
391
|
const returnPath = `${url.pathname}${url.search}`;
|
|
373
392
|
if (matchesAny(pathname, publicRoutes)) {
|
|
374
|
-
return
|
|
393
|
+
return continueRequest();
|
|
375
394
|
}
|
|
376
395
|
const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
|
|
377
|
-
if (!isProtected) return
|
|
396
|
+
if (!isProtected) return continueRequest();
|
|
378
397
|
const cookie = request.headers.get("cookie") ?? "";
|
|
379
398
|
const sessionCookie = readCookieValue(cookie, sessionCookieName);
|
|
380
399
|
const secret = resolveSessionSecret(sessionSecret);
|
|
@@ -420,16 +439,15 @@ function withAuthMiddleware(config) {
|
|
|
420
439
|
return forbidden;
|
|
421
440
|
}
|
|
422
441
|
if (recovery?.status === "recovered") {
|
|
423
|
-
|
|
424
|
-
requestHeaders.set(
|
|
442
|
+
downstreamHeaders.set(
|
|
425
443
|
"cookie",
|
|
426
444
|
replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
|
|
427
445
|
);
|
|
428
|
-
const response =
|
|
446
|
+
const response = continueRequest();
|
|
429
447
|
response.headers.append("Set-Cookie", recovery.setCookie);
|
|
430
448
|
return response;
|
|
431
449
|
}
|
|
432
|
-
return
|
|
450
|
+
return continueRequest();
|
|
433
451
|
}, "middleware");
|
|
434
452
|
}
|
|
435
453
|
__name(withAuthMiddleware, "withAuthMiddleware");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as AuthUser, R as RetryConfig } from '../../types-CI2t8wpJ.js';
|
|
2
2
|
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-BygfBCpF.js';
|
|
3
|
-
import { ProxySessionMode } from '../edge.js';
|
|
3
|
+
import { AuthProxyOptions, ProxySessionMode } from '../edge.js';
|
|
4
4
|
export { withAuthMiddleware } from '../edge.js';
|
|
5
5
|
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-DBX8XpsR.js';
|
|
6
6
|
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-DBX8XpsR.js';
|
|
@@ -244,9 +244,9 @@ interface AuthKit {
|
|
|
244
244
|
*/
|
|
245
245
|
requireRole: (roles: string[]) => Promise<ServerSession$1>;
|
|
246
246
|
/** Generated Next.js 16 Proxy function. */
|
|
247
|
-
proxy: (request: Request) => Promise<Response>;
|
|
247
|
+
proxy: (request: Request, options?: AuthProxyOptions) => Promise<Response>;
|
|
248
248
|
/** @deprecated Next.js 16 renamed Middleware to Proxy. Use `proxy`. */
|
|
249
|
-
middleware: (request: Request) => Promise<Response>;
|
|
249
|
+
middleware: (request: Request, options?: AuthProxyOptions) => Promise<Response>;
|
|
250
250
|
/** @deprecated Next.js 16 requires a static config literal in `proxy.ts`. */
|
|
251
251
|
config: {
|
|
252
252
|
matcher: string[];
|
|
@@ -298,4 +298,4 @@ interface SafeRedirectOptions {
|
|
|
298
298
|
*/
|
|
299
299
|
declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
|
|
300
300
|
|
|
301
|
-
export { type AuthCookiePersistenceOptions, type AuthKit, type AuthRouteHandler, type DefineAuthConfig, GetSessionConfig, type NextAuthRouteHandlers, ProxySessionMode, type SafeRedirectOptions, ServerSession$1 as ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
|
|
301
|
+
export { type AuthCookiePersistenceOptions, type AuthKit, AuthProxyOptions, type AuthRouteHandler, type DefineAuthConfig, GetSessionConfig, type NextAuthRouteHandlers, ProxySessionMode, type SafeRedirectOptions, ServerSession$1 as ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
|
|
@@ -22,6 +22,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
22
22
|
if (!isRecord(data) || !isValidUser(data.user)) return null;
|
|
23
23
|
if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
|
|
24
24
|
if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
|
|
25
|
+
if (typeof data.tokenFamily !== "string" || !data.tokenFamily) return null;
|
|
25
26
|
if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
|
|
26
27
|
const issuedAt = data.iat;
|
|
27
28
|
if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
|
|
@@ -31,6 +32,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
|
|
|
31
32
|
roles: [...data.roles],
|
|
32
33
|
permissions: [...data.permissions],
|
|
33
34
|
sessionVersion: data.sessionVersion,
|
|
35
|
+
tokenFamily: data.tokenFamily,
|
|
34
36
|
iat: issuedAt
|
|
35
37
|
};
|
|
36
38
|
} catch {
|
|
@@ -764,8 +766,25 @@ function withAuthMiddleware(config) {
|
|
|
764
766
|
} = config;
|
|
765
767
|
const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
|
|
766
768
|
const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
|
|
767
|
-
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
769
|
+
return /* @__PURE__ */ __name(async function middleware(request, options = {}) {
|
|
768
770
|
const { NextResponse } = await import("next/server");
|
|
771
|
+
const downstreamHeaders = new Headers(request.headers);
|
|
772
|
+
if (options.requestHeaders) {
|
|
773
|
+
const overrides = new Headers(options.requestHeaders);
|
|
774
|
+
for (const identityHeader of ["authorization", "cookie"]) {
|
|
775
|
+
if (overrides.has(identityHeader)) {
|
|
776
|
+
throw new TypeError(
|
|
777
|
+
`Auth proxy requestHeaders cannot override ${identityHeader}`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
overrides.forEach((value, key) => {
|
|
782
|
+
downstreamHeaders.set(key, value);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const continueRequest = /* @__PURE__ */ __name(() => NextResponse.next({
|
|
786
|
+
request: { headers: downstreamHeaders }
|
|
787
|
+
}), "continueRequest");
|
|
769
788
|
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
770
789
|
const loginUrl = new URL(loginRoute, request.url);
|
|
771
790
|
loginUrl.searchParams.set("from", returnPath2);
|
|
@@ -782,10 +801,10 @@ function withAuthMiddleware(config) {
|
|
|
782
801
|
const pathname = url.pathname;
|
|
783
802
|
const returnPath = `${url.pathname}${url.search}`;
|
|
784
803
|
if (matchesAny(pathname, publicRoutes)) {
|
|
785
|
-
return
|
|
804
|
+
return continueRequest();
|
|
786
805
|
}
|
|
787
806
|
const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
|
|
788
|
-
if (!isProtected) return
|
|
807
|
+
if (!isProtected) return continueRequest();
|
|
789
808
|
const cookie = request.headers.get("cookie") ?? "";
|
|
790
809
|
const sessionCookie = readCookieValue(cookie, sessionCookieName);
|
|
791
810
|
const secret = resolveSessionSecret(sessionSecret);
|
|
@@ -831,16 +850,15 @@ function withAuthMiddleware(config) {
|
|
|
831
850
|
return forbidden;
|
|
832
851
|
}
|
|
833
852
|
if (recovery?.status === "recovered") {
|
|
834
|
-
|
|
835
|
-
requestHeaders.set(
|
|
853
|
+
downstreamHeaders.set(
|
|
836
854
|
"cookie",
|
|
837
855
|
replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
|
|
838
856
|
);
|
|
839
|
-
const response =
|
|
857
|
+
const response = continueRequest();
|
|
840
858
|
response.headers.append("Set-Cookie", recovery.setCookie);
|
|
841
859
|
return response;
|
|
842
860
|
}
|
|
843
|
-
return
|
|
861
|
+
return continueRequest();
|
|
844
862
|
}, "middleware");
|
|
845
863
|
}
|
|
846
864
|
__name(withAuthMiddleware, "withAuthMiddleware");
|