@oauth42/next 0.5.0 → 0.5.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.
@@ -1,7 +1,133 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+
9
+ // src/server/verified-userinfo.ts
10
+ async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
11
+ if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
12
+ try {
13
+ const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
14
+ const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
15
+ const response = await fetcher(target, {
16
+ headers: { Authorization: `Bearer ${accessToken}` },
17
+ redirect: "error",
18
+ cache: "no-store",
19
+ signal: AbortSignal.timeout(3e4)
20
+ });
21
+ if (!response.ok) return null;
22
+ const user = await response.json();
23
+ return parseVerifiedUserInfo(user, subject);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function parseVerifiedUserInfo(value, subject) {
29
+ if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
30
+ const user = value;
31
+ if (user.sub !== subject) return null;
32
+ if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
33
+ if (user.email !== void 0 && typeof user.email !== "string") return null;
34
+ if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
35
+ const profile = { sub: user.sub };
36
+ for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
37
+ if (user[field] != null) {
38
+ if (typeof user[field] !== "string") return null;
39
+ profile[field] = user[field];
40
+ }
41
+ }
42
+ if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
43
+ if (user.groups !== void 0) profile.groups = user.groups;
44
+ return profile;
45
+ }
46
+ async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
47
+ if (typeof session.access_token !== "string" || !session.access_token) return null;
48
+ if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
49
+ return parseVerifiedUserInfo(session.user_info, subject);
50
+ }
51
+ return verifiedUserInfo(issuer, session.access_token, subject, dpop);
52
+ }
53
+
1
54
  // src/server/middleware.ts
2
55
  import { NextResponse } from "next/server";
3
56
  import { getToken, encode } from "next-auth/jwt";
4
57
 
58
+ // src/server/client-assertion.ts
59
+ import { createHash, createPublicKey, generateKeyPairSync, randomBytes, sign } from "crypto";
60
+ var _key, _public;
61
+ var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
62
+ constructor(kid, key) {
63
+ __privateAdd(this, _key);
64
+ __privateAdd(this, _public);
65
+ if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
66
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
67
+ throw new Error("Client assertions require a private P-256 key");
68
+ }
69
+ __privateSet(this, _key, key);
70
+ this.kid = kid;
71
+ const publicKey = createPublicKey(key).export({ format: "jwk" });
72
+ __privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
73
+ this.thumbprint = createHash("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
74
+ }
75
+ static generate(kid) {
76
+ return new _OAuth42ClientAssertion(kid, generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey);
77
+ }
78
+ get publicJwk() {
79
+ return { ...__privateGet(this, _public), kid: this.kid };
80
+ }
81
+ /** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
82
+ get oidcJwks() {
83
+ return { keys: [{
84
+ kty: "EC",
85
+ crv: "P-256",
86
+ x: __privateGet(this, _public).x,
87
+ y: __privateGet(this, _public).y,
88
+ d: __privateGet(this, _key).export({ format: "jwk" }).d,
89
+ kid: this.kid
90
+ }] };
91
+ }
92
+ static validateEndpoint(target) {
93
+ const url = new URL(target);
94
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
95
+ throw new Error("Client authentication requires HTTPS outside loopback development");
96
+ }
97
+ }
98
+ assertion(clientId, audience) {
99
+ _OAuth42ClientAssertion.validateEndpoint(audience);
100
+ if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
101
+ const now = Math.floor(Date.now() / 1e3);
102
+ const header = { alg: "ES256", typ: "JWT", kid: this.kid };
103
+ const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: randomBytes(32).toString("base64url") };
104
+ const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
105
+ return `${data}.${sign("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
106
+ }
107
+ async fetch(clientId, target, init, dpop) {
108
+ if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
109
+ throw new Error("Client assertions require a form POST");
110
+ }
111
+ const prepare = () => {
112
+ const body = new URLSearchParams(init.body);
113
+ body.delete("client_secret");
114
+ body.set("client_id", clientId);
115
+ body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
116
+ body.set("client_assertion", this.assertion(clientId, target));
117
+ const headers = new Headers(init.headers);
118
+ headers.delete("Authorization");
119
+ headers.delete("Content-Length");
120
+ headers.delete("Transfer-Encoding");
121
+ headers.set("Content-Type", "application/x-www-form-urlencoded");
122
+ return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
123
+ };
124
+ return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
125
+ }
126
+ };
127
+ _key = new WeakMap();
128
+ _public = new WeakMap();
129
+ var OAuth42ClientAssertion = _OAuth42ClientAssertion;
130
+
5
131
  // src/server/backend-session.ts
6
132
  function isBackendSessionMode(mode) {
7
133
  return mode === "backend";
@@ -9,38 +135,56 @@ function isBackendSessionMode(mode) {
9
135
  function sessionEndpoint(issuer) {
10
136
  return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
11
137
  }
12
- async function resolveBackendSession(params) {
13
- const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
14
- method: "POST",
138
+ async function sessionFetch(params, target, body, credential, method = "POST") {
139
+ OAuth42ClientAssertion.validateEndpoint(target);
140
+ const prepare = () => ({
141
+ method,
142
+ redirect: "error",
143
+ cache: "no-store",
144
+ signal: AbortSignal.timeout(3e4),
15
145
  headers: { "Content-Type": "application/json" },
16
146
  body: JSON.stringify({
17
- session_id: params.sessionId,
147
+ ...body,
18
148
  client_id: params.clientId,
19
- client_secret: params.clientSecret,
20
- issuer: params.issuer
149
+ ...params.clientAssertionKey ? {
150
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
151
+ client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
152
+ } : { client_secret: params.clientSecret }
21
153
  })
22
154
  });
23
- if (response.status === 401 || response.status === 403 || response.status === 404) {
24
- return null;
25
- }
26
- if (!response.ok) {
27
- throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
28
- }
29
- return response.json();
155
+ return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
156
+ }
157
+ async function sessionResponse(response, params) {
158
+ if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
159
+ const session = await response.json();
160
+ if (params.dpop) params.dpop.assertTokenResponse(session);
161
+ else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
162
+ return session;
163
+ }
164
+ async function resolveBackendSession(params) {
165
+ const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
166
+ if ([401, 403, 404].includes(response.status)) return null;
167
+ return sessionResponse(response, params);
30
168
  }
31
169
 
32
170
  // src/server/middleware.ts
33
171
  var ALLOWED_COOKIE_SIZE = 4096;
34
172
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
35
173
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
36
- var REFRESH_RACE_GRACE_SECONDS = 60;
37
174
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
38
175
  var pendingRefreshes = /* @__PURE__ */ new Map();
39
- function shouldPreserveSessionOnRefreshFailure(params) {
40
- if (params.error !== "invalid_grant" || !params.expiresAt) {
41
- return false;
176
+ function forwardedHeaders(req, cookieName, jwt) {
177
+ const headers = new Headers(req.headers);
178
+ headers.delete("x-oauth42-refreshed-token");
179
+ if (cookieName && jwt) {
180
+ const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
181
+ if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
182
+ else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
183
+ cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
184
+ }
185
+ headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
42
186
  }
43
- return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
187
+ return headers;
44
188
  }
45
189
  function resolveRefreshConfig(options, token) {
46
190
  return {
@@ -49,11 +193,15 @@ function resolveRefreshConfig(options, token) {
49
193
  issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
50
194
  };
51
195
  }
52
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
196
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
53
197
  try {
54
198
  const tokenUrl = `${issuer}/oauth2/token`;
55
- const response = await fetch(tokenUrl, {
199
+ const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
200
+ const response = await fetcher(tokenUrl, {
56
201
  method: "POST",
202
+ redirect: "error",
203
+ cache: "no-store",
204
+ signal: AbortSignal.timeout(3e4),
57
205
  headers: {
58
206
  "Content-Type": "application/x-www-form-urlencoded"
59
207
  },
@@ -66,9 +214,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
66
214
  });
67
215
  const data = await response.json();
68
216
  if (!response.ok) {
69
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
217
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
70
218
  return { success: false, error: data.error || "refresh_failed" };
71
219
  }
220
+ dpop?.assertTokenResponse(data);
72
221
  console.log("[OAuth42 Middleware] Token refreshed successfully");
73
222
  return {
74
223
  success: true,
@@ -77,14 +226,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
77
226
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
78
227
  };
79
228
  } catch (error) {
80
- console.error("[OAuth42 Middleware] Token refresh error:", error);
229
+ console.error("[OAuth42 Middleware] Token refresh request failed");
81
230
  return { success: false, error: "refresh_error" };
82
231
  }
83
232
  }
84
233
  function withOAuth42Auth(options = {}) {
85
234
  const secret = process.env.NEXTAUTH_SECRET;
86
235
  if (!secret) {
87
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
236
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
88
237
  }
89
238
  return async function middleware(req) {
90
239
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -95,22 +244,41 @@ function withOAuth42Auth(options = {}) {
95
244
  });
96
245
  const pathname = req.nextUrl.pathname;
97
246
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
98
- return NextResponse.next();
247
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
99
248
  }
100
249
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
101
250
  if (!needsProtection) {
102
- return NextResponse.next();
251
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
103
252
  }
104
- if (!token) {
253
+ if (!token || token.error) {
105
254
  const signInUrl = options.pages?.signIn || "/auth/signin";
106
255
  const url = new URL(signInUrl, req.url);
107
256
  url.searchParams.set("callbackUrl", pathname);
108
257
  return NextResponse.redirect(url);
109
258
  }
110
259
  const refreshConfig = resolveRefreshConfig(options, token);
260
+ const denied = () => {
261
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
262
+ url.searchParams.set("callbackUrl", pathname);
263
+ const response = NextResponse.redirect(url);
264
+ for (const cookie of req.cookies.getAll()) {
265
+ if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
266
+ }
267
+ return response;
268
+ };
269
+ const verifyAndAuthorize = async (current, resolved) => {
270
+ const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
271
+ if (!user) return false;
272
+ current.groups = user.groups ?? [];
273
+ current.email = user.email;
274
+ current.name = user.name;
275
+ current.username = user.preferred_username ?? user.username;
276
+ current.emailVerified = user.email_verified;
277
+ return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
278
+ };
111
279
  if (isBackendSessionMode(options.sessionStore)) {
112
280
  const sessionId = token.oauth42SessionId;
113
- if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
281
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
114
282
  const signInUrl = options.pages?.signIn || "/auth/signin";
115
283
  const url = new URL(signInUrl, req.url);
116
284
  url.searchParams.set("callbackUrl", pathname);
@@ -119,9 +287,11 @@ function withOAuth42Auth(options = {}) {
119
287
  return response;
120
288
  }
121
289
  const backendSession = await resolveBackendSession({
290
+ dpop: options.dpop,
291
+ clientAssertionKey: options.clientAssertionKey,
122
292
  issuer: refreshConfig.issuer,
123
293
  clientId: refreshConfig.clientId,
124
- clientSecret: refreshConfig.clientSecret,
294
+ clientSecret: refreshConfig.clientSecret || "",
125
295
  sessionId
126
296
  }).catch((error) => {
127
297
  console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
@@ -135,8 +305,9 @@ function withOAuth42Auth(options = {}) {
135
305
  response.cookies.delete(cookieName);
136
306
  return response;
137
307
  }
138
- const requestHeaders = new Headers(req.headers);
139
- requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
308
+ const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
309
+ if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
310
+ const requestHeaders = forwardedHeaders(req);
140
311
  return NextResponse.next({
141
312
  request: {
142
313
  headers: requestHeaders
@@ -152,13 +323,12 @@ function withOAuth42Auth(options = {}) {
152
323
  expiresAt,
153
324
  now,
154
325
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
155
- accessTokenPrefix: token.accessToken?.substring(0, 20),
156
326
  path: pathname
157
327
  });
158
328
  }
159
- if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
160
- const userId = token.sub;
161
- let refreshPromise = pendingRefreshes.get(userId);
329
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
330
+ const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
331
+ let refreshPromise = pendingRefreshes.get(grantKey);
162
332
  if (refreshPromise) {
163
333
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
164
334
  } else {
@@ -166,15 +336,17 @@ function withOAuth42Auth(options = {}) {
166
336
  refreshPromise = refreshTokens(
167
337
  token.refreshToken,
168
338
  refreshConfig.clientId,
169
- refreshConfig.clientSecret,
170
- refreshConfig.issuer
339
+ refreshConfig.clientSecret || "",
340
+ refreshConfig.issuer,
341
+ options.dpop,
342
+ options.clientAssertionKey
171
343
  ).finally(() => {
172
- const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
344
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
173
345
  if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
174
346
  cleanupTimer.unref();
175
347
  }
176
348
  });
177
- pendingRefreshes.set(userId, refreshPromise);
349
+ pendingRefreshes.set(grantKey, refreshPromise);
178
350
  }
179
351
  const refreshed = await refreshPromise;
180
352
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -184,12 +356,13 @@ function withOAuth42Auth(options = {}) {
184
356
  refreshToken: refreshed.refreshToken,
185
357
  expiresAt: refreshed.expiresAt
186
358
  };
359
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
360
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
187
361
  const newJwt = await encode({
188
362
  token: updatedToken,
189
363
  secret
190
364
  });
191
- const requestHeaders = new Headers(req.headers);
192
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
365
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
193
366
  const response = NextResponse.next({
194
367
  request: {
195
368
  headers: requestHeaders
@@ -242,28 +415,11 @@ function withOAuth42Auth(options = {}) {
242
415
  chunked: newJwt.length > CHUNK_SIZE,
243
416
  existingBaseCooke: req.cookies.has(cookieName),
244
417
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
245
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
246
418
  newExpiresAt: refreshed.expiresAt,
247
419
  cookieName
248
420
  });
249
421
  return response;
250
422
  } else {
251
- if (shouldPreserveSessionOnRefreshFailure({
252
- error: refreshed.error,
253
- expiresAt,
254
- now
255
- })) {
256
- console.warn(
257
- "[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
258
- {
259
- expiresAt,
260
- now,
261
- expiredAgo: now - expiresAt,
262
- path: pathname
263
- }
264
- );
265
- return NextResponse.next();
266
- }
267
423
  console.log("[OAuth42 Middleware] Refresh failed, clearing session");
268
424
  const signInUrl = options.pages?.signIn || "/auth/signin";
269
425
  const url = new URL(signInUrl, req.url);
@@ -281,16 +437,13 @@ function withOAuth42Auth(options = {}) {
281
437
  return response;
282
438
  }
283
439
  }
284
- if (options.callbacks?.authorized) {
285
- const isAuthorized = await options.callbacks.authorized({ token, req });
286
- if (!isAuthorized) {
287
- const signInUrl = options.pages?.signIn || "/auth/signin";
288
- const url = new URL(signInUrl, req.url);
289
- url.searchParams.set("callbackUrl", pathname);
290
- return NextResponse.redirect(url);
291
- }
440
+ if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
441
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
442
+ url.searchParams.set("callbackUrl", pathname);
443
+ return NextResponse.redirect(url);
292
444
  }
293
- return NextResponse.next();
445
+ if (!await verifyAndAuthorize(token)) return denied();
446
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
294
447
  };
295
448
  }
296
449
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/middleware.ts","../../src/server/backend-session.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\nimport { getToken, encode } from 'next-auth/jwt';\nimport {\n isBackendSessionMode,\n resolveBackendSession,\n type SessionStoreMode,\n} from './backend-session';\n\n/**\n * Cookie chunking constants — must match NextAuth's SessionStore behavior.\n * NextAuth splits cookies at ALLOWED_COOKIE_SIZE (4096) minus overhead (163).\n * When middleware re-encodes the JWT after a token refresh, the cookie must be\n * chunked the same way, otherwise:\n * - A single cookie > ~4 KB is silently rejected by the browser\n * - Leftover chunk cookies from the original session cause SessionStore to\n * concatenate old chunks with the new base cookie, producing garbage\n */\nconst ALLOWED_COOKIE_SIZE = 4096;\nconst ESTIMATED_EMPTY_COOKIE_SIZE = 163;\nconst CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;\nconst REFRESH_RACE_GRACE_SECONDS = 60;\nconst DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;\n\n/**\n * In-flight refresh tracking to prevent parallel refresh calls.\n * Key: user ID (from token.sub), Value: Promise that resolves with refresh result\n */\nconst pendingRefreshes = new Map<string, Promise<{\n success: boolean;\n accessToken?: string;\n refreshToken?: string;\n expiresAt?: number;\n error?: string;\n}>>();\n\nexport interface OAuth42AuthOptions {\n pages?: {\n signIn?: string;\n error?: string;\n };\n callbacks?: {\n authorized?: (params: { token: any; req: NextRequest }) => boolean | Promise<boolean>;\n };\n protectedPaths?: string[];\n publicPaths?: string[];\n /**\n * Cookie prefix for custom cookie names. Must match the prefix used in createAuth().\n * E.g., 'oauth42-portal' will look for cookie 'oauth42-portal.session-token'\n */\n cookiePrefix?: string;\n /**\n * OAuth client credentials for middleware refresh. When omitted, the middleware\n * prefers credentials embedded in the NextAuth token by createAuth(), then\n * falls back to OAUTH42_CLIENT_ID / OAUTH42_CLIENT_SECRET.\n */\n clientId?: string;\n clientSecret?: string;\n issuer?: string;\n /**\n * \"backend\" keeps OAuth token material in the Rust backend and stores only\n * an opaque backend session id in the NextAuth JWT cookie.\n */\n sessionStore?: SessionStoreMode;\n /**\n * Session cookie lifetime. Should match createAuth({ session.maxAge }).\n * Defaults to OAuth42's persistent-session policy of 365 days.\n */\n session?: {\n maxAge?: number;\n };\n}\n\nexport function shouldPreserveSessionOnRefreshFailure(params: {\n error?: string;\n expiresAt?: number;\n now: number;\n}): boolean {\n if (params.error !== 'invalid_grant' || !params.expiresAt) {\n return false;\n }\n\n return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;\n}\n\nexport function resolveRefreshConfig(\n options: OAuth42AuthOptions,\n token: any\n): { clientId?: string; clientSecret?: string; issuer: string } {\n return {\n clientId:\n options.clientId ||\n (token.clientId as string | undefined) ||\n process.env.OAUTH42_CLIENT_ID,\n clientSecret:\n options.clientSecret ||\n (token.clientSecret as string | undefined) ||\n process.env.OAUTH42_CLIENT_SECRET,\n issuer:\n options.issuer ||\n (token.issuer as string | undefined) ||\n process.env.OAUTH42_ISSUER ||\n 'https://localhost:8443',\n };\n}\n\n/**\n * Refresh tokens by calling the OAuth42 backend directly\n */\nasync function refreshTokens(\n refreshToken: string,\n clientId: string,\n clientSecret: string,\n issuer: string\n): Promise<{ success: boolean; accessToken?: string; refreshToken?: string; expiresAt?: number; error?: string }> {\n try {\n const tokenUrl = `${issuer}/oauth2/token`;\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n console.error('[OAuth42 Middleware] Token refresh failed:', data);\n return { success: false, error: data.error || 'refresh_failed' };\n }\n\n console.log('[OAuth42 Middleware] Token refreshed successfully');\n return {\n success: true,\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),\n };\n } catch (error) {\n console.error('[OAuth42 Middleware] Token refresh error:', error);\n return { success: false, error: 'refresh_error' };\n }\n}\n\n/**\n * Middleware helper for protecting routes with OAuth42\n *\n * This middleware handles:\n * 1. Route protection (redirect to login if no session)\n * 2. Token refresh (refresh expired tokens and update cookie)\n */\nexport function withOAuth42Auth(options: OAuth42AuthOptions = {}) {\n const secret = process.env.NEXTAUTH_SECRET;\n\n if (!secret) {\n console.warn('[OAuth42 Middleware] NEXTAUTH_SECRET not set');\n }\n\n return async function middleware(req: NextRequest) {\n // Build cookie name - if prefix is provided, use custom name\n const cookieName = options.cookiePrefix\n ? `${options.cookiePrefix}.session-token`\n : 'next-auth.session-token';\n\n const token = await getToken({\n req: req as any,\n secret,\n cookieName,\n });\n\n const pathname = req.nextUrl.pathname;\n\n // Check if path is explicitly public\n if (options.publicPaths?.some(path => pathname.startsWith(path))) {\n return NextResponse.next();\n }\n\n // Check if path needs protection\n const needsProtection = options.protectedPaths\n ? options.protectedPaths.some(path => pathname.startsWith(path))\n : true; // Default to protecting all paths\n\n if (!needsProtection) {\n return NextResponse.next();\n }\n\n // No token at all - redirect to sign in\n if (!token) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n return NextResponse.redirect(url);\n }\n\n const refreshConfig = resolveRefreshConfig(options, token);\n\n if (isBackendSessionMode(options.sessionStore)) {\n const sessionId = token.oauth42SessionId as string | undefined;\n if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n const response = NextResponse.redirect(url);\n response.cookies.delete(cookieName);\n return response;\n }\n\n const backendSession = await resolveBackendSession({\n issuer: refreshConfig.issuer,\n clientId: refreshConfig.clientId,\n clientSecret: refreshConfig.clientSecret,\n sessionId,\n }).catch(error => {\n console.error('[OAuth42 Middleware] Backend session resolve failed:', error);\n return null;\n });\n\n if (!backendSession) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n const response = NextResponse.redirect(url);\n response.cookies.delete(cookieName);\n return response;\n }\n\n const requestHeaders = new Headers(req.headers);\n requestHeaders.set('x-oauth42-refreshed-token', backendSession.access_token);\n return NextResponse.next({\n request: {\n headers: requestHeaders,\n },\n });\n }\n\n // Check if access token is expired or expiring soon (15 second buffer)\n // Buffer must be less than the token TTL to have a valid window\n const expiresAt = token.expiresAt as number | undefined;\n const now = Math.floor(Date.now() / 1000);\n const bufferSeconds = 15;\n const needsRefresh = expiresAt && now >= expiresAt - bufferSeconds;\n\n if (needsRefresh) {\n console.log('[OAuth42 Middleware] Token needs refresh:', {\n expiresAt,\n now,\n expiredAgo: expiresAt ? now - expiresAt : 'n/a',\n accessTokenPrefix: (token.accessToken as string)?.substring(0, 20),\n path: pathname,\n });\n }\n\n if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {\n const userId = token.sub as string;\n\n // Check if there's already a refresh in progress for this user\n let refreshPromise = pendingRefreshes.get(userId);\n\n if (refreshPromise) {\n console.log('[OAuth42 Middleware] Waiting for in-flight refresh...');\n } else {\n console.log('[OAuth42 Middleware] Access token expired, refreshing...');\n\n // Create the refresh promise and store it.\n // Keep the entry for 30s after completion so late-arriving requests\n // (with stale cookies) reuse the cached result instead of triggering\n // a new refresh with the already-rotated token. See issue #470.\n refreshPromise = refreshTokens(\n token.refreshToken as string,\n refreshConfig.clientId,\n refreshConfig.clientSecret,\n refreshConfig.issuer\n ).finally(() => {\n const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 30000);\n if (typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {\n cleanupTimer.unref();\n }\n });\n\n pendingRefreshes.set(userId, refreshPromise);\n }\n\n const refreshed = await refreshPromise;\n\n if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {\n // Update the token with new values\n const updatedToken = {\n ...token,\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken,\n expiresAt: refreshed.expiresAt,\n };\n\n // Re-encode the JWT\n const newJwt = await encode({\n token: updatedToken,\n secret: secret!,\n });\n\n // Create response with request headers that pass the new token to API routes\n // This is necessary because API routes read from the request, not the response cookie\n const requestHeaders = new Headers(req.headers);\n requestHeaders.set('x-oauth42-refreshed-token', refreshed.accessToken);\n\n const response = NextResponse.next({\n request: {\n headers: requestHeaders,\n },\n });\n\n const cookieOptions = {\n httpOnly: true,\n sameSite: 'lax' as const,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,\n };\n\n // Set cookie with chunking matching NextAuth's SessionStore behavior.\n // Without chunking, a JWT > ~4 KB is silently rejected by the browser,\n // causing an infinite refresh loop where every request re-triggers\n // token refresh but the cookie never persists.\n if (newJwt.length <= CHUNK_SIZE) {\n // Fits in a single cookie\n response.cookies.set(cookieName, newJwt, cookieOptions);\n // Clean up any leftover chunk cookies from a previously chunked session\n for (let i = 0; ; i++) {\n const chunkName = `${cookieName}.${i}`;\n if (req.cookies.has(chunkName)) {\n response.cookies.set(chunkName, '', { ...cookieOptions, maxAge: 0 });\n } else {\n break;\n }\n }\n } else {\n // Cookie too large — chunk it like NextAuth does\n const chunkCount = Math.ceil(newJwt.length / CHUNK_SIZE);\n for (let i = 0; i < chunkCount; i++) {\n const chunkName = `${cookieName}.${i}`;\n const chunkValue = newJwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);\n response.cookies.set(chunkName, chunkValue, cookieOptions);\n }\n // Delete the base cookie so SessionStore only reads chunks\n response.cookies.set(cookieName, '', { ...cookieOptions, maxAge: 0 });\n // Clean up any extra old chunks beyond the new count\n for (let i = chunkCount; ; i++) {\n const chunkName = `${cookieName}.${i}`;\n if (req.cookies.has(chunkName)) {\n response.cookies.set(chunkName, '', { ...cookieOptions, maxAge: 0 });\n } else {\n break;\n }\n }\n }\n\n // Log details to help diagnose cookie persistence issues\n const existingChunks = [];\n for (let i = 0; ; i++) {\n if (req.cookies.has(`${cookieName}.${i}`)) {\n existingChunks.push(i);\n } else {\n break;\n }\n }\n console.log(`[OAuth42 Middleware] Token refresh complete:`, {\n jwtBytes: newJwt.length,\n chunked: newJwt.length > CHUNK_SIZE,\n existingBaseCooke: req.cookies.has(cookieName),\n existingChunks: existingChunks.length > 0 ? existingChunks : 'none',\n newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),\n newExpiresAt: refreshed.expiresAt,\n cookieName,\n });\n return response;\n } else {\n if (shouldPreserveSessionOnRefreshFailure({\n error: refreshed.error,\n expiresAt,\n now,\n })) {\n console.warn(\n '[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race',\n {\n expiresAt,\n now,\n expiredAgo: now - expiresAt,\n path: pathname,\n }\n );\n return NextResponse.next();\n }\n\n // Refresh failed - clear cookie and redirect to sign in (no error, just let user log in fresh)\n console.log('[OAuth42 Middleware] Refresh failed, clearing session');\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n // DO NOT set error parameter - just redirect silently\n\n const response = NextResponse.redirect(url);\n // Clear the base session cookie and any chunk cookies\n response.cookies.delete(cookieName);\n for (let i = 0; ; i++) {\n const chunkName = `${cookieName}.${i}`;\n if (req.cookies.has(chunkName)) {\n response.cookies.delete(chunkName);\n } else {\n break;\n }\n }\n return response;\n }\n }\n\n // Check custom authorization callback\n if (options.callbacks?.authorized) {\n const isAuthorized = await options.callbacks.authorized({ token, req });\n if (!isAuthorized) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n return NextResponse.redirect(url);\n }\n }\n\n return NextResponse.next();\n };\n}\n\n/**\n * Helper to create middleware configuration\n */\nexport function createMiddlewareConfig(\n protectedPaths: string[] = ['/protected'],\n publicPaths: string[] = ['/auth', '/api/auth']\n) {\n return {\n matcher: [\n /*\n * Match all request paths except for the ones starting with:\n * - _next/static (static files)\n * - _next/image (image optimization files)\n * - favicon.ico (favicon file)\n * - public folder\n */\n '/((?!_next/static|_next/image|favicon.ico|public).*)',\n ],\n protectedPaths,\n publicPaths,\n };\n}\n","export type SessionStoreMode = 'jwt' | 'backend';\n\nexport interface BackendSessionUser {\n sub?: string;\n email?: string;\n username?: string;\n name?: string;\n emailVerified?: boolean;\n groups?: string[];\n}\n\nexport interface BackendSessionResponse {\n session_id: string;\n access_token: string;\n expires_at: number;\n id_token?: string;\n user?: BackendSessionUser;\n}\n\nexport interface BackendSessionConfig {\n issuer: string;\n clientId: string;\n clientSecret: string;\n}\n\nexport function isBackendSessionMode(mode?: SessionStoreMode): boolean {\n return mode === 'backend';\n}\n\nfunction sessionEndpoint(issuer: string): string {\n return `${issuer.replace(/\\/$/, '')}/api/sdk/sessions`;\n}\n\nexport async function createBackendSession(params: BackendSessionConfig & {\n accessToken: string;\n refreshToken: string;\n expiresAt?: number;\n idToken?: string;\n user?: BackendSessionUser;\n}): Promise<BackendSessionResponse> {\n const response = await fetch(sessionEndpoint(params.issuer), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n access_token: params.accessToken,\n refresh_token: params.refreshToken,\n expires_at: params.expiresAt ?? Math.floor(Date.now() / 1000) + 3600,\n id_token: params.idToken,\n client_id: params.clientId,\n client_secret: params.clientSecret,\n issuer: params.issuer,\n user: params.user,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`OAuth42 backend session create failed: ${response.status}`);\n }\n\n return response.json();\n}\n\nexport async function resolveBackendSession(params: BackendSessionConfig & {\n sessionId: string;\n}): Promise<BackendSessionResponse | null> {\n const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n session_id: params.sessionId,\n client_id: params.clientId,\n client_secret: params.clientSecret,\n issuer: params.issuer,\n }),\n });\n\n if (response.status === 401 || response.status === 403 || response.status === 404) {\n return null;\n }\n\n if (!response.ok) {\n throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);\n }\n\n return response.json();\n}\n\nexport async function revokeBackendSession(params: BackendSessionConfig & {\n sessionId: string;\n}): Promise<void> {\n await fetch(sessionEndpoint(params.issuer), {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n session_id: params.sessionId,\n client_id: params.clientId,\n client_secret: params.clientSecret,\n }),\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/verified-userinfo.ts","../../src/server/middleware.ts","../../src/server/client-assertion.ts","../../src/server/backend-session.ts"],"sourcesContent":["import type { OAuth42DPoP } from './dpop';\n/** Live verification binds an application session to the issuer's current user. */\nexport interface VerifiedUserInfo {\n sub: string;\n email?: string;\n name?: string;\n preferred_username?: string;\n username?: string;\n email_verified?: boolean;\n groups?: string[];\n given_name?: string;\n family_name?: string;\n company_id?: string;\n company_name?: string;\n}\n\nexport async function verifiedUserInfo(issuer: string, accessToken: unknown, subject: unknown, dpop?: OAuth42DPoP): Promise<VerifiedUserInfo | null> {\n if (typeof accessToken !== 'string' || !accessToken || typeof subject !== 'string' || !subject) return null;\n try {\n const target = `${issuer.replace(/\\/$/, '')}/oauth2/userinfo`;\n const fetcher = dpop ? (url: string, init: RequestInit) => dpop.fetch(url, init, accessToken) : fetch;\n const response = await fetcher(target, {\n headers: { Authorization: `Bearer ${accessToken}` },\n redirect: 'error', cache: 'no-store', signal: AbortSignal.timeout(30_000),\n });\n if (!response.ok) return null;\n const user = await response.json();\n return parseVerifiedUserInfo(user, subject);\n } catch {\n return null;\n }\n}\n\n\n/** Validate only live issuer response data, never cached JWT/session metadata. */\nexport function parseVerifiedUserInfo(value: unknown, subject: unknown): VerifiedUserInfo | null {\n if (typeof subject !== 'string' || !subject || !value || typeof value !== 'object' || Array.isArray(value)) return null;\n const user = value as Record<string, unknown>;\n if (user.sub !== subject) return null;\n if (user.groups !== undefined && (!Array.isArray(user.groups) || user.groups.some((group: unknown) => typeof group !== 'string'))) return null;\n // Email is required by O42's contract when present. Optional display fields\n // serialize as null for existing email-only accounts and are omitted below.\n if (user.email !== undefined && typeof user.email !== 'string') return null;\n if (user.email_verified !== undefined && typeof user.email_verified !== 'boolean') return null;\n // Return only the profile fields applications display. Never forward arbitrary\n // issuer properties into the browser session.\n const profile: VerifiedUserInfo = { sub: user.sub };\n for (const field of ['email', 'name', 'preferred_username', 'username', 'given_name', 'family_name', 'company_id', 'company_name'] as const) {\n if (user[field] != null) {\n if (typeof user[field] !== 'string') return null;\n profile[field] = user[field];\n }\n }\n if (user.email_verified !== undefined) profile.email_verified = user.email_verified;\n if (user.groups !== undefined) profile.groups = user.groups;\n return profile;\n}\n\n/** New backends verify identity within resolve; old backends retain live /userinfo. */\nexport async function verifiedBackendSessionUserInfo(\n issuer: string,\n session: { access_token: unknown; user_info?: unknown },\n subject: unknown,\n dpop?: OAuth42DPoP,\n): Promise<VerifiedUserInfo | null> {\n if (typeof session.access_token !== 'string' || !session.access_token) return null;\n if (Object.prototype.hasOwnProperty.call(session, 'user_info')) {\n // A malformed/null response fails closed. Never fall back to cached `user`\n // or let a second endpoint hide an invalid consolidated identity response.\n return parseVerifiedUserInfo(session.user_info, subject);\n }\n return verifiedUserInfo(issuer, session.access_token, subject, dpop);\n}\n","import type { OAuth42ClientAssertion } from './client-assertion';\nimport type { OAuth42DPoP } from './dpop';\nimport { verifiedBackendSessionUserInfo, verifiedUserInfo, type VerifiedUserInfo } from './verified-userinfo';\nimport { NextRequest, NextResponse } from 'next/server';\nimport { getToken, encode } from 'next-auth/jwt';\nimport {\n isBackendSessionMode,\n resolveBackendSession,\n type SessionStoreMode,\n} from './backend-session';\n\n/**\n * Cookie chunking constants — must match NextAuth's SessionStore behavior.\n * NextAuth splits cookies at ALLOWED_COOKIE_SIZE (4096) minus overhead (163).\n * When middleware re-encodes the JWT after a token refresh, the cookie must be\n * chunked the same way, otherwise:\n * - A single cookie > ~4 KB is silently rejected by the browser\n * - Leftover chunk cookies from the original session cause SessionStore to\n * concatenate old chunks with the new base cookie, producing garbage\n */\nconst ALLOWED_COOKIE_SIZE = 4096;\nconst ESTIMATED_EMPTY_COOKIE_SIZE = 163;\nconst CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;\nconst DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;\n\n/**\n * In-flight refresh tracking to prevent parallel refresh calls.\n * Key: user ID (from token.sub), Value: Promise that resolves with refresh result\n */\nconst pendingRefreshes = new Map<string, Promise<{\n success: boolean;\n accessToken?: string;\n refreshToken?: string;\n expiresAt?: number;\n error?: string;\n}>>();\n\nexport interface OAuth42AuthOptions {\n pages?: {\n signIn?: string;\n error?: string;\n };\n callbacks?: {\n authorized?: (params: { token: any; req: NextRequest }) => boolean | Promise<boolean>;\n };\n protectedPaths?: string[];\n publicPaths?: string[];\n /**\n * Cookie prefix for custom cookie names. Must match the prefix used in createAuth().\n * E.g., 'oauth42-portal' will look for cookie 'oauth42-portal.session-token'\n */\n cookiePrefix?: string;\n /**\n * OAuth client credentials for middleware refresh. When omitted, the middleware\n * prefers credentials embedded in the NextAuth token by createAuth(), then\n * falls back to OAUTH42_CLIENT_ID / OAUTH42_CLIENT_SECRET.\n */\n clientId?: string;\n clientSecret?: string;\n issuer?: string;\n dpop?: OAuth42DPoP;\n clientAssertionKey?: OAuth42ClientAssertion;\n /**\n * \"backend\" keeps OAuth token material in the Rust backend and stores only\n * an opaque backend session id in the NextAuth JWT cookie.\n */\n sessionStore?: SessionStoreMode;\n /**\n * Session cookie lifetime. Should match createAuth({ session.maxAge }).\n * Defaults to OAuth42's persistent-session policy of 365 days.\n */\n session?: {\n maxAge?: number;\n };\n}\n\n/** @deprecated Invalid refresh grants must fail closed, including concurrent requests. */\nexport function shouldPreserveSessionOnRefreshFailure(_params: {\n error?: string; expiresAt?: number; now: number;\n}): boolean {\n return false;\n}\n\nfunction forwardedHeaders(req: NextRequest, cookieName?: string, jwt?: string): Headers {\n const headers = new Headers(req.headers);\n headers.delete('x-oauth42-refreshed-token');\n if (cookieName && jwt) {\n const cookies = req.cookies.getAll().filter(cookie => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));\n if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });\n else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {\n cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });\n }\n headers.set('cookie', cookies.map(cookie => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join('; '));\n }\n return headers;\n}\n\nexport function resolveRefreshConfig(\n options: OAuth42AuthOptions,\n token: any\n): { clientId?: string; clientSecret?: string; issuer: string } {\n return {\n clientId:\n options.clientId ||\n (token.clientId as string | undefined) ||\n process.env.OAUTH42_CLIENT_ID,\n clientSecret:\n options.clientSecret ||\n (token.clientSecret as string | undefined) ||\n process.env.OAUTH42_CLIENT_SECRET,\n issuer:\n options.issuer ||\n (token.issuer as string | undefined) ||\n process.env.OAUTH42_ISSUER ||\n 'https://localhost:8443',\n };\n}\n\n/**\n * Refresh tokens by calling the OAuth42 backend directly\n */\nasync function refreshTokens(\n refreshToken: string,\n clientId: string,\n clientSecret: string,\n issuer: string,\n dpop?: OAuth42DPoP,\n clientAssertionKey?: OAuth42ClientAssertion\n): Promise<{ success: boolean; accessToken?: string; refreshToken?: string; expiresAt?: number; error?: string }> {\n try {\n const tokenUrl = `${issuer}/oauth2/token`;\n\n const fetcher = clientAssertionKey ? (url: string, init: RequestInit) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;\n const response = await fetcher(tokenUrl, {\n method: 'POST',\n redirect: 'error',\n cache: 'no-store',\n signal: AbortSignal.timeout(30_000),\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n console.error('[OAuth42 Middleware] Token refresh failed:', response.status);\n return { success: false, error: data.error || 'refresh_failed' };\n }\n\n dpop?.assertTokenResponse(data);\n console.log('[OAuth42 Middleware] Token refreshed successfully');\n return {\n success: true,\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),\n };\n } catch (error) {\n console.error('[OAuth42 Middleware] Token refresh request failed');\n return { success: false, error: 'refresh_error' };\n }\n}\n\n/**\n * Middleware helper for protecting routes with OAuth42\n *\n * This middleware handles:\n * 1. Route protection (redirect to login if no session)\n * 2. Token refresh (refresh expired tokens and update cookie)\n */\nexport function withOAuth42Auth(options: OAuth42AuthOptions = {}) {\n const secret = process.env.NEXTAUTH_SECRET;\n\n if (!secret) {\n throw new Error('NEXTAUTH_SECRET is required for OAuth42 middleware');\n }\n\n return async function middleware(req: NextRequest) {\n // Build cookie name - if prefix is provided, use custom name\n const cookieName = options.cookiePrefix\n ? `${options.cookiePrefix}.session-token`\n : 'next-auth.session-token';\n\n const token = await getToken({\n req: req as any,\n secret,\n cookieName,\n });\n\n const pathname = req.nextUrl.pathname;\n\n // Check if path is explicitly public\n if (options.publicPaths?.some(path => pathname.startsWith(path))) {\n return NextResponse.next({ request: { headers: forwardedHeaders(req) } });\n }\n\n // Check if path needs protection\n const needsProtection = options.protectedPaths\n ? options.protectedPaths.some(path => pathname.startsWith(path))\n : true; // Default to protecting all paths\n\n if (!needsProtection) {\n return NextResponse.next({ request: { headers: forwardedHeaders(req) } });\n }\n\n // No token at all - redirect to sign in\n if (!token || token.error) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n return NextResponse.redirect(url);\n }\n\n const refreshConfig = resolveRefreshConfig(options, token);\n\n const denied = () => {\n const url = new URL(options.pages?.signIn || '/auth/signin', req.url);\n url.searchParams.set('callbackUrl', pathname);\n const response = NextResponse.redirect(url);\n for (const cookie of req.cookies.getAll()) {\n if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);\n }\n return response;\n };\n const verifyAndAuthorize = async (current: typeof token, resolved?: VerifiedUserInfo | null) => {\n const user = resolved === undefined\n ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop)\n : resolved;\n if (!user) return false;\n current.groups = user.groups ?? [];\n current.email = user.email;\n current.name = user.name;\n current.username = user.preferred_username ?? user.username;\n current.emailVerified = user.email_verified;\n return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;\n };\n\n if (isBackendSessionMode(options.sessionStore)) {\n const sessionId = token.oauth42SessionId as string | undefined;\n if (!sessionId || !refreshConfig.clientId || (!refreshConfig.clientSecret && !options.clientAssertionKey)) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n const response = NextResponse.redirect(url);\n response.cookies.delete(cookieName);\n return response;\n }\n\n const backendSession = await resolveBackendSession({\n dpop: options.dpop, clientAssertionKey: options.clientAssertionKey,\n issuer: refreshConfig.issuer,\n clientId: refreshConfig.clientId,\n clientSecret: refreshConfig.clientSecret || '',\n sessionId,\n }).catch(error => {\n console.error('[OAuth42 Middleware] Backend session resolve failed:', error);\n return null;\n });\n\n if (!backendSession) {\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n const response = NextResponse.redirect(url);\n response.cookies.delete(cookieName);\n return response;\n }\n\n const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);\n if (!(await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified))) return denied();\n const requestHeaders = forwardedHeaders(req);\n return NextResponse.next({\n request: {\n headers: requestHeaders,\n },\n });\n }\n\n // Check if access token is expired or expiring soon (15 second buffer)\n // Buffer must be less than the token TTL to have a valid window\n const expiresAt = token.expiresAt as number | undefined;\n const now = Math.floor(Date.now() / 1000);\n const bufferSeconds = 15;\n const needsRefresh = expiresAt && now >= expiresAt - bufferSeconds;\n\n if (needsRefresh) {\n console.log('[OAuth42 Middleware] Token needs refresh:', {\n expiresAt,\n now,\n expiredAgo: expiresAt ? now - expiresAt : 'n/a',\n path: pathname,\n });\n }\n\n if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {\n const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);\n\n // Check if there's already a refresh in progress for this user\n let refreshPromise = pendingRefreshes.get(grantKey);\n\n if (refreshPromise) {\n console.log('[OAuth42 Middleware] Waiting for in-flight refresh...');\n } else {\n console.log('[OAuth42 Middleware] Access token expired, refreshing...');\n\n // Create the refresh promise and store it.\n // Keep the entry for 30s after completion so late-arriving requests\n // (with stale cookies) reuse the cached result instead of triggering\n // a new refresh with the already-rotated token. See issue #470.\n refreshPromise = refreshTokens(\n token.refreshToken as string,\n refreshConfig.clientId,\n refreshConfig.clientSecret || '',\n refreshConfig.issuer,\n options.dpop,\n options.clientAssertionKey\n ).finally(() => {\n const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 30000);\n if (typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {\n cleanupTimer.unref();\n }\n });\n\n pendingRefreshes.set(grantKey, refreshPromise);\n }\n\n const refreshed = await refreshPromise;\n\n if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {\n // Update the token with new values\n const updatedToken: typeof token = {\n ...token,\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken,\n expiresAt: refreshed.expiresAt,\n };\n\n if (options.clientAssertionKey) delete updatedToken.clientSecret;\n if (!(await verifyAndAuthorize(updatedToken))) return denied();\n // Re-encode the JWT\n const newJwt = await encode({\n token: updatedToken,\n secret: secret!,\n });\n\n // Create response with request headers that pass the new token to API routes\n // This is necessary because API routes read from the request, not the response cookie\n const requestHeaders = forwardedHeaders(req, cookieName, newJwt);\n\n const response = NextResponse.next({\n request: {\n headers: requestHeaders,\n },\n });\n\n const cookieOptions = {\n httpOnly: true,\n sameSite: 'lax' as const,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,\n };\n\n // Set cookie with chunking matching NextAuth's SessionStore behavior.\n // Without chunking, a JWT > ~4 KB is silently rejected by the browser,\n // causing an infinite refresh loop where every request re-triggers\n // token refresh but the cookie never persists.\n if (newJwt.length <= CHUNK_SIZE) {\n // Fits in a single cookie\n response.cookies.set(cookieName, newJwt, cookieOptions);\n // Clean up any leftover chunk cookies from a previously chunked session\n for (let i = 0; ; i++) {\n const chunkName = `${cookieName}.${i}`;\n if (req.cookies.has(chunkName)) {\n response.cookies.set(chunkName, '', { ...cookieOptions, maxAge: 0 });\n } else {\n break;\n }\n }\n } else {\n // Cookie too large — chunk it like NextAuth does\n const chunkCount = Math.ceil(newJwt.length / CHUNK_SIZE);\n for (let i = 0; i < chunkCount; i++) {\n const chunkName = `${cookieName}.${i}`;\n const chunkValue = newJwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);\n response.cookies.set(chunkName, chunkValue, cookieOptions);\n }\n // Delete the base cookie so SessionStore only reads chunks\n response.cookies.set(cookieName, '', { ...cookieOptions, maxAge: 0 });\n // Clean up any extra old chunks beyond the new count\n for (let i = chunkCount; ; i++) {\n const chunkName = `${cookieName}.${i}`;\n if (req.cookies.has(chunkName)) {\n response.cookies.set(chunkName, '', { ...cookieOptions, maxAge: 0 });\n } else {\n break;\n }\n }\n }\n\n // Log details to help diagnose cookie persistence issues\n const existingChunks = [];\n for (let i = 0; ; i++) {\n if (req.cookies.has(`${cookieName}.${i}`)) {\n existingChunks.push(i);\n } else {\n break;\n }\n }\n console.log(`[OAuth42 Middleware] Token refresh complete:`, {\n jwtBytes: newJwt.length,\n chunked: newJwt.length > CHUNK_SIZE,\n existingBaseCooke: req.cookies.has(cookieName),\n existingChunks: existingChunks.length > 0 ? existingChunks : 'none',\n newExpiresAt: refreshed.expiresAt,\n cookieName,\n });\n return response;\n } else {\n // Refresh failed - clear cookie and redirect to sign in (no error, just let user log in fresh)\n console.log('[OAuth42 Middleware] Refresh failed, clearing session');\n const signInUrl = options.pages?.signIn || '/auth/signin';\n const url = new URL(signInUrl, req.url);\n url.searchParams.set('callbackUrl', pathname);\n // DO NOT set error parameter - just redirect silently\n\n const response = NextResponse.redirect(url);\n // Clear the base session cookie and any chunk cookies\n response.cookies.delete(cookieName);\n for (let i = 0; ; i++) {\n const chunkName = `${cookieName}.${i}`;\n if (req.cookies.has(chunkName)) {\n response.cookies.delete(chunkName);\n } else {\n break;\n }\n }\n return response;\n }\n }\n\n\n if (!Number.isFinite(expiresAt) || now >= (expiresAt as number) || !token.accessToken) {\n const url = new URL(options.pages?.signIn || '/auth/signin', req.url);\n url.searchParams.set('callbackUrl', pathname);\n return NextResponse.redirect(url);\n }\n if (!(await verifyAndAuthorize(token))) return denied();\n return NextResponse.next({ request: { headers: forwardedHeaders(req) } });\n };\n}\n\n/**\n * Helper to create middleware configuration\n */\nexport function createMiddlewareConfig(\n protectedPaths: string[] = ['/protected'],\n publicPaths: string[] = ['/auth', '/api/auth']\n) {\n return {\n matcher: [\n /*\n * Match all request paths except for the ones starting with:\n * - _next/static (static files)\n * - _next/image (image optimization files)\n * - favicon.ico (favicon file)\n * - public folder\n */\n '/((?!_next/static|_next/image|favicon.ico|public).*)',\n ],\n protectedPaths,\n publicPaths,\n };\n}\n","import { createHash, createPublicKey, generateKeyPairSync, KeyObject, randomBytes, sign } from 'node:crypto';\nimport type { OAuth42DPoP } from './dpop';\n\n/** Retained software signing key for a confidential backend. Never put this in a session. */\nexport class OAuth42ClientAssertion {\n readonly #key: KeyObject;\n readonly #public: Record<string, string>;\n readonly kid: string;\n readonly thumbprint: string;\n\n constructor(kid: string, key: KeyObject) {\n if (typeof kid !== 'string' || !/^[\\x21-\\x7e]{1,128}$/.test(kid)) throw new Error('Invalid client signing key ID');\n if (key.type !== 'private' || key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {\n throw new Error('Client assertions require a private P-256 key');\n }\n this.#key = key;\n this.kid = kid;\n const publicKey = createPublicKey(key).export({ format: 'jwk' });\n this.#public = { crv: 'P-256', kty: 'EC', x: publicKey.x!, y: publicKey.y! };\n this.thumbprint = createHash('sha256').update(JSON.stringify(this.#public)).digest('base64url');\n }\n\n static generate(kid: string): OAuth42ClientAssertion {\n return new OAuth42ClientAssertion(kid, generateKeyPairSync('ec', { namedCurve: 'prime256v1' }).privateKey);\n }\n\n get publicJwk(): Record<string, string> { return { ...this.#public, kid: this.kid }; }\n\n /** Private keys for the server-only NextAuth/openid-client configuration, never registration. */\n get oidcJwks() {\n return { keys: [{ kty: 'EC', crv: 'P-256', x: this.#public.x, y: this.#public.y,\n d: this.#key.export({ format: 'jwk' }).d!, kid: this.kid }] };\n }\n\n static validateEndpoint(target: string): void {\n const url = new URL(target);\n if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash ||\n (url.protocol !== 'https:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) {\n throw new Error('Client authentication requires HTTPS outside loopback development');\n }\n }\n\n assertion(clientId: string, audience: string): string {\n OAuth42ClientAssertion.validateEndpoint(audience);\n if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error('Invalid client identity');\n const now = Math.floor(Date.now() / 1000);\n const header = { alg: 'ES256', typ: 'JWT', kid: this.kid };\n const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: randomBytes(32).toString('base64url') };\n const data = `${Buffer.from(JSON.stringify(header)).toString('base64url')}.${Buffer.from(JSON.stringify(payload)).toString('base64url')}`;\n return `${data}.${sign('sha256', Buffer.from(data), { key: this.#key, dsaEncoding: 'ieee-p1363' }).toString('base64url')}`;\n }\n\n async fetch(clientId: string, target: string, init: RequestInit, dpop?: OAuth42DPoP): Promise<Response> {\n if (init.method?.toUpperCase() !== 'POST' || !(typeof init.body === 'string' || init.body instanceof URLSearchParams)) {\n throw new Error('Client assertions require a form POST');\n }\n const prepare = (): RequestInit => {\n const body = new URLSearchParams(init.body as string | URLSearchParams);\n body.delete('client_secret');\n body.set('client_id', clientId);\n body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');\n body.set('client_assertion', this.assertion(clientId, target));\n const headers = new Headers(init.headers);\n headers.delete('Authorization'); headers.delete('Content-Length'); headers.delete('Transfer-Encoding');\n headers.set('Content-Type', 'application/x-www-form-urlencoded');\n return { ...init, body, headers, redirect: 'error', cache: 'no-store', signal: init.signal || AbortSignal.timeout(30_000) };\n };\n return dpop ? dpop.fetch(target, init, undefined, prepare) : fetch(target, prepare());\n }\n}\n","import type { OAuth42DPoP } from './dpop';\nimport { OAuth42ClientAssertion } from './client-assertion';\nexport type SessionStoreMode = 'jwt' | 'backend';\n\nexport interface BackendSessionUser {\n sub?: string;\n email?: string;\n username?: string;\n name?: string;\n emailVerified?: boolean;\n groups?: string[];\n}\n\nexport interface BackendSessionResponse {\n token_type?: string;\n session_id: string;\n access_token: string;\n expires_at: number;\n id_token?: string;\n user?: BackendSessionUser;\n /** Wire data must pass subject/shape validation before becoming a session. */\n user_info?: unknown;\n}\n\nexport interface BackendSessionConfig {\n issuer: string;\n clientId: string;\n clientSecret: string;\n dpop?: OAuth42DPoP;\n clientAssertionKey?: OAuth42ClientAssertion;\n}\n\nexport function isBackendSessionMode(mode?: SessionStoreMode): boolean {\n return mode === 'backend';\n}\n\nfunction sessionEndpoint(issuer: string): string {\n return `${issuer.replace(/\\/$/, '')}/api/sdk/sessions`;\n}\n\nasync function sessionFetch(params: BackendSessionConfig, target: string, body: Record<string, unknown>, credential: string, method = 'POST'): Promise<Response> {\n OAuth42ClientAssertion.validateEndpoint(target);\n const prepare = (): RequestInit => ({\n method, redirect: 'error', cache: 'no-store', signal: AbortSignal.timeout(30_000),\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...body, client_id: params.clientId,\n ...(params.clientAssertionKey ? {\n client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',\n client_assertion: params.clientAssertionKey.assertion(params.clientId, target),\n } : { client_secret: params.clientSecret }),\n }),\n });\n return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());\n}\n\nasync function sessionResponse(response: Response, params: BackendSessionConfig): Promise<BackendSessionResponse> {\n if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);\n const session: BackendSessionResponse = await response.json();\n if (params.dpop) params.dpop.assertTokenResponse(session);\n else if (session.token_type?.toLowerCase() === 'dpop') throw new Error('Backend session requires its retained proof key');\n return session;\n}\n\nexport async function createBackendSession(params: BackendSessionConfig & {\n accessToken: string; refreshToken: string; expiresAt?: number; idToken?: string; user?: BackendSessionUser;\n}): Promise<BackendSessionResponse> {\n return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {\n access_token: params.accessToken, refresh_token: params.refreshToken,\n id_token: params.idToken, user: params.user,\n }, params.accessToken), params);\n}\n\nexport async function resolveBackendSession(params: BackendSessionConfig & { sessionId: string }): Promise<BackendSessionResponse | null> {\n const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);\n if ([401, 403, 404].includes(response.status)) return null;\n return sessionResponse(response, params);\n}\n\nexport async function revokeBackendSession(params: BackendSessionConfig & { sessionId: string }): Promise<void> {\n const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, 'DELETE');\n if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);\n}\n"],"mappings":"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","names":[]}