@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.
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __typeError = (msg) => {
7
+ throw TypeError(msg);
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -16,6 +19,10 @@ var __copyProps = (to, from, except, desc) => {
16
19
  return to;
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
23
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
24
+ 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);
25
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
19
26
 
20
27
  // src/middleware/index.ts
21
28
  var middleware_exports = {};
@@ -25,10 +32,128 @@ __export(middleware_exports, {
25
32
  });
26
33
  module.exports = __toCommonJS(middleware_exports);
27
34
 
35
+ // src/server/verified-userinfo.ts
36
+ async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
37
+ if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
38
+ try {
39
+ const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
40
+ const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
41
+ const response = await fetcher(target, {
42
+ headers: { Authorization: `Bearer ${accessToken}` },
43
+ redirect: "error",
44
+ cache: "no-store",
45
+ signal: AbortSignal.timeout(3e4)
46
+ });
47
+ if (!response.ok) return null;
48
+ const user = await response.json();
49
+ return parseVerifiedUserInfo(user, subject);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function parseVerifiedUserInfo(value, subject) {
55
+ if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
56
+ const user = value;
57
+ if (user.sub !== subject) return null;
58
+ if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
59
+ if (user.email !== void 0 && typeof user.email !== "string") return null;
60
+ if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
61
+ const profile = { sub: user.sub };
62
+ for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
63
+ if (user[field] != null) {
64
+ if (typeof user[field] !== "string") return null;
65
+ profile[field] = user[field];
66
+ }
67
+ }
68
+ if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
69
+ if (user.groups !== void 0) profile.groups = user.groups;
70
+ return profile;
71
+ }
72
+ async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
73
+ if (typeof session.access_token !== "string" || !session.access_token) return null;
74
+ if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
75
+ return parseVerifiedUserInfo(session.user_info, subject);
76
+ }
77
+ return verifiedUserInfo(issuer, session.access_token, subject, dpop);
78
+ }
79
+
28
80
  // src/server/middleware.ts
29
81
  var import_server = require("next/server");
30
82
  var import_jwt = require("next-auth/jwt");
31
83
 
84
+ // src/server/client-assertion.ts
85
+ var import_node_crypto = require("crypto");
86
+ var _key, _public;
87
+ var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
88
+ constructor(kid, key) {
89
+ __privateAdd(this, _key);
90
+ __privateAdd(this, _public);
91
+ if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
92
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
93
+ throw new Error("Client assertions require a private P-256 key");
94
+ }
95
+ __privateSet(this, _key, key);
96
+ this.kid = kid;
97
+ const publicKey = (0, import_node_crypto.createPublicKey)(key).export({ format: "jwk" });
98
+ __privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
99
+ this.thumbprint = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
100
+ }
101
+ static generate(kid) {
102
+ return new _OAuth42ClientAssertion(kid, (0, import_node_crypto.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
103
+ }
104
+ get publicJwk() {
105
+ return { ...__privateGet(this, _public), kid: this.kid };
106
+ }
107
+ /** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
108
+ get oidcJwks() {
109
+ return { keys: [{
110
+ kty: "EC",
111
+ crv: "P-256",
112
+ x: __privateGet(this, _public).x,
113
+ y: __privateGet(this, _public).y,
114
+ d: __privateGet(this, _key).export({ format: "jwk" }).d,
115
+ kid: this.kid
116
+ }] };
117
+ }
118
+ static validateEndpoint(target) {
119
+ const url = new URL(target);
120
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
121
+ throw new Error("Client authentication requires HTTPS outside loopback development");
122
+ }
123
+ }
124
+ assertion(clientId, audience) {
125
+ _OAuth42ClientAssertion.validateEndpoint(audience);
126
+ if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
127
+ const now = Math.floor(Date.now() / 1e3);
128
+ const header = { alg: "ES256", typ: "JWT", kid: this.kid };
129
+ const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: (0, import_node_crypto.randomBytes)(32).toString("base64url") };
130
+ const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
131
+ return `${data}.${(0, import_node_crypto.sign)("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
132
+ }
133
+ async fetch(clientId, target, init, dpop) {
134
+ if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
135
+ throw new Error("Client assertions require a form POST");
136
+ }
137
+ const prepare = () => {
138
+ const body = new URLSearchParams(init.body);
139
+ body.delete("client_secret");
140
+ body.set("client_id", clientId);
141
+ body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
142
+ body.set("client_assertion", this.assertion(clientId, target));
143
+ const headers = new Headers(init.headers);
144
+ headers.delete("Authorization");
145
+ headers.delete("Content-Length");
146
+ headers.delete("Transfer-Encoding");
147
+ headers.set("Content-Type", "application/x-www-form-urlencoded");
148
+ return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
149
+ };
150
+ return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
151
+ }
152
+ };
153
+ _key = new WeakMap();
154
+ _public = new WeakMap();
155
+ var OAuth42ClientAssertion = _OAuth42ClientAssertion;
156
+
32
157
  // src/server/backend-session.ts
33
158
  function isBackendSessionMode(mode) {
34
159
  return mode === "backend";
@@ -36,38 +161,56 @@ function isBackendSessionMode(mode) {
36
161
  function sessionEndpoint(issuer) {
37
162
  return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
38
163
  }
39
- async function resolveBackendSession(params) {
40
- const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
41
- method: "POST",
164
+ async function sessionFetch(params, target, body, credential, method = "POST") {
165
+ OAuth42ClientAssertion.validateEndpoint(target);
166
+ const prepare = () => ({
167
+ method,
168
+ redirect: "error",
169
+ cache: "no-store",
170
+ signal: AbortSignal.timeout(3e4),
42
171
  headers: { "Content-Type": "application/json" },
43
172
  body: JSON.stringify({
44
- session_id: params.sessionId,
173
+ ...body,
45
174
  client_id: params.clientId,
46
- client_secret: params.clientSecret,
47
- issuer: params.issuer
175
+ ...params.clientAssertionKey ? {
176
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
177
+ client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
178
+ } : { client_secret: params.clientSecret }
48
179
  })
49
180
  });
50
- if (response.status === 401 || response.status === 403 || response.status === 404) {
51
- return null;
52
- }
53
- if (!response.ok) {
54
- throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
55
- }
56
- return response.json();
181
+ return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
182
+ }
183
+ async function sessionResponse(response, params) {
184
+ if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
185
+ const session = await response.json();
186
+ if (params.dpop) params.dpop.assertTokenResponse(session);
187
+ else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
188
+ return session;
189
+ }
190
+ async function resolveBackendSession(params) {
191
+ const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
192
+ if ([401, 403, 404].includes(response.status)) return null;
193
+ return sessionResponse(response, params);
57
194
  }
58
195
 
59
196
  // src/server/middleware.ts
60
197
  var ALLOWED_COOKIE_SIZE = 4096;
61
198
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
62
199
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
63
- var REFRESH_RACE_GRACE_SECONDS = 60;
64
200
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
65
201
  var pendingRefreshes = /* @__PURE__ */ new Map();
66
- function shouldPreserveSessionOnRefreshFailure(params) {
67
- if (params.error !== "invalid_grant" || !params.expiresAt) {
68
- return false;
202
+ function forwardedHeaders(req, cookieName, jwt) {
203
+ const headers = new Headers(req.headers);
204
+ headers.delete("x-oauth42-refreshed-token");
205
+ if (cookieName && jwt) {
206
+ const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
207
+ if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
208
+ else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
209
+ cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
210
+ }
211
+ headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
69
212
  }
70
- return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
213
+ return headers;
71
214
  }
72
215
  function resolveRefreshConfig(options, token) {
73
216
  return {
@@ -76,11 +219,15 @@ function resolveRefreshConfig(options, token) {
76
219
  issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
77
220
  };
78
221
  }
79
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
222
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
80
223
  try {
81
224
  const tokenUrl = `${issuer}/oauth2/token`;
82
- const response = await fetch(tokenUrl, {
225
+ const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
226
+ const response = await fetcher(tokenUrl, {
83
227
  method: "POST",
228
+ redirect: "error",
229
+ cache: "no-store",
230
+ signal: AbortSignal.timeout(3e4),
84
231
  headers: {
85
232
  "Content-Type": "application/x-www-form-urlencoded"
86
233
  },
@@ -93,9 +240,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
93
240
  });
94
241
  const data = await response.json();
95
242
  if (!response.ok) {
96
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
243
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
97
244
  return { success: false, error: data.error || "refresh_failed" };
98
245
  }
246
+ dpop?.assertTokenResponse(data);
99
247
  console.log("[OAuth42 Middleware] Token refreshed successfully");
100
248
  return {
101
249
  success: true,
@@ -104,14 +252,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
104
252
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
105
253
  };
106
254
  } catch (error) {
107
- console.error("[OAuth42 Middleware] Token refresh error:", error);
255
+ console.error("[OAuth42 Middleware] Token refresh request failed");
108
256
  return { success: false, error: "refresh_error" };
109
257
  }
110
258
  }
111
259
  function withOAuth42Auth(options = {}) {
112
260
  const secret = process.env.NEXTAUTH_SECRET;
113
261
  if (!secret) {
114
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
262
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
115
263
  }
116
264
  return async function middleware(req) {
117
265
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -122,22 +270,41 @@ function withOAuth42Auth(options = {}) {
122
270
  });
123
271
  const pathname = req.nextUrl.pathname;
124
272
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
125
- return import_server.NextResponse.next();
273
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
126
274
  }
127
275
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
128
276
  if (!needsProtection) {
129
- return import_server.NextResponse.next();
277
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
130
278
  }
131
- if (!token) {
279
+ if (!token || token.error) {
132
280
  const signInUrl = options.pages?.signIn || "/auth/signin";
133
281
  const url = new URL(signInUrl, req.url);
134
282
  url.searchParams.set("callbackUrl", pathname);
135
283
  return import_server.NextResponse.redirect(url);
136
284
  }
137
285
  const refreshConfig = resolveRefreshConfig(options, token);
286
+ const denied = () => {
287
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
288
+ url.searchParams.set("callbackUrl", pathname);
289
+ const response = import_server.NextResponse.redirect(url);
290
+ for (const cookie of req.cookies.getAll()) {
291
+ if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
292
+ }
293
+ return response;
294
+ };
295
+ const verifyAndAuthorize = async (current, resolved) => {
296
+ const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
297
+ if (!user) return false;
298
+ current.groups = user.groups ?? [];
299
+ current.email = user.email;
300
+ current.name = user.name;
301
+ current.username = user.preferred_username ?? user.username;
302
+ current.emailVerified = user.email_verified;
303
+ return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
304
+ };
138
305
  if (isBackendSessionMode(options.sessionStore)) {
139
306
  const sessionId = token.oauth42SessionId;
140
- if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
307
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
141
308
  const signInUrl = options.pages?.signIn || "/auth/signin";
142
309
  const url = new URL(signInUrl, req.url);
143
310
  url.searchParams.set("callbackUrl", pathname);
@@ -146,9 +313,11 @@ function withOAuth42Auth(options = {}) {
146
313
  return response;
147
314
  }
148
315
  const backendSession = await resolveBackendSession({
316
+ dpop: options.dpop,
317
+ clientAssertionKey: options.clientAssertionKey,
149
318
  issuer: refreshConfig.issuer,
150
319
  clientId: refreshConfig.clientId,
151
- clientSecret: refreshConfig.clientSecret,
320
+ clientSecret: refreshConfig.clientSecret || "",
152
321
  sessionId
153
322
  }).catch((error) => {
154
323
  console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
@@ -162,8 +331,9 @@ function withOAuth42Auth(options = {}) {
162
331
  response.cookies.delete(cookieName);
163
332
  return response;
164
333
  }
165
- const requestHeaders = new Headers(req.headers);
166
- requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
334
+ const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
335
+ if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
336
+ const requestHeaders = forwardedHeaders(req);
167
337
  return import_server.NextResponse.next({
168
338
  request: {
169
339
  headers: requestHeaders
@@ -179,13 +349,12 @@ function withOAuth42Auth(options = {}) {
179
349
  expiresAt,
180
350
  now,
181
351
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
182
- accessTokenPrefix: token.accessToken?.substring(0, 20),
183
352
  path: pathname
184
353
  });
185
354
  }
186
- if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
187
- const userId = token.sub;
188
- let refreshPromise = pendingRefreshes.get(userId);
355
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
356
+ const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
357
+ let refreshPromise = pendingRefreshes.get(grantKey);
189
358
  if (refreshPromise) {
190
359
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
191
360
  } else {
@@ -193,15 +362,17 @@ function withOAuth42Auth(options = {}) {
193
362
  refreshPromise = refreshTokens(
194
363
  token.refreshToken,
195
364
  refreshConfig.clientId,
196
- refreshConfig.clientSecret,
197
- refreshConfig.issuer
365
+ refreshConfig.clientSecret || "",
366
+ refreshConfig.issuer,
367
+ options.dpop,
368
+ options.clientAssertionKey
198
369
  ).finally(() => {
199
- const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
370
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
200
371
  if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
201
372
  cleanupTimer.unref();
202
373
  }
203
374
  });
204
- pendingRefreshes.set(userId, refreshPromise);
375
+ pendingRefreshes.set(grantKey, refreshPromise);
205
376
  }
206
377
  const refreshed = await refreshPromise;
207
378
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -211,12 +382,13 @@ function withOAuth42Auth(options = {}) {
211
382
  refreshToken: refreshed.refreshToken,
212
383
  expiresAt: refreshed.expiresAt
213
384
  };
385
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
386
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
214
387
  const newJwt = await (0, import_jwt.encode)({
215
388
  token: updatedToken,
216
389
  secret
217
390
  });
218
- const requestHeaders = new Headers(req.headers);
219
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
391
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
220
392
  const response = import_server.NextResponse.next({
221
393
  request: {
222
394
  headers: requestHeaders
@@ -269,28 +441,11 @@ function withOAuth42Auth(options = {}) {
269
441
  chunked: newJwt.length > CHUNK_SIZE,
270
442
  existingBaseCooke: req.cookies.has(cookieName),
271
443
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
272
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
273
444
  newExpiresAt: refreshed.expiresAt,
274
445
  cookieName
275
446
  });
276
447
  return response;
277
448
  } else {
278
- if (shouldPreserveSessionOnRefreshFailure({
279
- error: refreshed.error,
280
- expiresAt,
281
- now
282
- })) {
283
- console.warn(
284
- "[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
285
- {
286
- expiresAt,
287
- now,
288
- expiredAgo: now - expiresAt,
289
- path: pathname
290
- }
291
- );
292
- return import_server.NextResponse.next();
293
- }
294
449
  console.log("[OAuth42 Middleware] Refresh failed, clearing session");
295
450
  const signInUrl = options.pages?.signIn || "/auth/signin";
296
451
  const url = new URL(signInUrl, req.url);
@@ -308,16 +463,13 @@ function withOAuth42Auth(options = {}) {
308
463
  return response;
309
464
  }
310
465
  }
311
- if (options.callbacks?.authorized) {
312
- const isAuthorized = await options.callbacks.authorized({ token, req });
313
- if (!isAuthorized) {
314
- const signInUrl = options.pages?.signIn || "/auth/signin";
315
- const url = new URL(signInUrl, req.url);
316
- url.searchParams.set("callbackUrl", pathname);
317
- return import_server.NextResponse.redirect(url);
318
- }
466
+ if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
467
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
468
+ url.searchParams.set("callbackUrl", pathname);
469
+ return import_server.NextResponse.redirect(url);
319
470
  }
320
- return import_server.NextResponse.next();
471
+ if (!await verifyAndAuthorize(token)) return denied();
472
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
321
473
  };
322
474
  }
323
475
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/index.ts","../../src/server/middleware.ts","../../src/server/backend-session.ts"],"sourcesContent":["// Edge-compatible middleware exports\n// This file is separate from server/index.ts to avoid pulling in Node.js modules\n\nexport { withOAuth42Auth, createMiddlewareConfig } from '../server/middleware';\nexport type { OAuth42AuthOptions } from '../server/middleware';\n","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/middleware/index.ts","../../src/server/verified-userinfo.ts","../../src/server/middleware.ts","../../src/server/client-assertion.ts","../../src/server/backend-session.ts"],"sourcesContent":["// Edge-compatible middleware exports\n// This file is separate from server/index.ts to avoid pulling in Node.js modules\n\nexport { withOAuth42Auth, createMiddlewareConfig } from '../server/middleware';\nexport type { OAuth42AuthOptions } from '../server/middleware';\n","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":[]}