@oauth42/next 0.4.8 → 0.5.0

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,39 +1,2 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
-
3
- interface OAuth42AuthOptions {
4
- pages?: {
5
- signIn?: string;
6
- error?: string;
7
- };
8
- callbacks?: {
9
- authorized?: (params: {
10
- token: any;
11
- req: NextRequest;
12
- }) => boolean | Promise<boolean>;
13
- };
14
- protectedPaths?: string[];
15
- publicPaths?: string[];
16
- /**
17
- * Cookie prefix for custom cookie names. Must match the prefix used in createAuth().
18
- * E.g., 'oauth42-portal' will look for cookie 'oauth42-portal.session-token'
19
- */
20
- cookiePrefix?: string;
21
- }
22
- /**
23
- * Middleware helper for protecting routes with OAuth42
24
- *
25
- * This middleware handles:
26
- * 1. Route protection (redirect to login if no session)
27
- * 2. Token refresh (refresh expired tokens and update cookie)
28
- */
29
- declare function withOAuth42Auth(options?: OAuth42AuthOptions): (req: NextRequest) => Promise<NextResponse<unknown>>;
30
- /**
31
- * Helper to create middleware configuration
32
- */
33
- declare function createMiddlewareConfig(protectedPaths?: string[], publicPaths?: string[]): {
34
- matcher: string[];
35
- protectedPaths: string[];
36
- publicPaths: string[];
37
- };
38
-
39
- export { type OAuth42AuthOptions, createMiddlewareConfig, withOAuth42Auth };
1
+ export { O as OAuth42AuthOptions, c as createMiddlewareConfig, w as withOAuth42Auth } from '../index-Cfqxl9r2.js';
2
+ import 'next/server';
@@ -28,10 +28,54 @@ module.exports = __toCommonJS(middleware_exports);
28
28
  // src/server/middleware.ts
29
29
  var import_server = require("next/server");
30
30
  var import_jwt = require("next-auth/jwt");
31
+
32
+ // src/server/backend-session.ts
33
+ function isBackendSessionMode(mode) {
34
+ return mode === "backend";
35
+ }
36
+ function sessionEndpoint(issuer) {
37
+ return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
38
+ }
39
+ async function resolveBackendSession(params) {
40
+ const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({
44
+ session_id: params.sessionId,
45
+ client_id: params.clientId,
46
+ client_secret: params.clientSecret,
47
+ issuer: params.issuer
48
+ })
49
+ });
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();
57
+ }
58
+
59
+ // src/server/middleware.ts
31
60
  var ALLOWED_COOKIE_SIZE = 4096;
32
61
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
33
62
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
63
+ var REFRESH_RACE_GRACE_SECONDS = 60;
64
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
34
65
  var pendingRefreshes = /* @__PURE__ */ new Map();
66
+ function shouldPreserveSessionOnRefreshFailure(params) {
67
+ if (params.error !== "invalid_grant" || !params.expiresAt) {
68
+ return false;
69
+ }
70
+ return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
71
+ }
72
+ function resolveRefreshConfig(options, token) {
73
+ return {
74
+ clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
75
+ clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
76
+ issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
77
+ };
78
+ }
35
79
  async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
36
80
  try {
37
81
  const tokenUrl = `${issuer}/oauth2/token`;
@@ -66,9 +110,6 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
66
110
  }
67
111
  function withOAuth42Auth(options = {}) {
68
112
  const secret = process.env.NEXTAUTH_SECRET;
69
- const clientId = process.env.OAUTH42_CLIENT_ID;
70
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
71
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
72
113
  if (!secret) {
73
114
  console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
74
115
  }
@@ -93,6 +134,42 @@ function withOAuth42Auth(options = {}) {
93
134
  url.searchParams.set("callbackUrl", pathname);
94
135
  return import_server.NextResponse.redirect(url);
95
136
  }
137
+ const refreshConfig = resolveRefreshConfig(options, token);
138
+ if (isBackendSessionMode(options.sessionStore)) {
139
+ const sessionId = token.oauth42SessionId;
140
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
141
+ const signInUrl = options.pages?.signIn || "/auth/signin";
142
+ const url = new URL(signInUrl, req.url);
143
+ url.searchParams.set("callbackUrl", pathname);
144
+ const response = import_server.NextResponse.redirect(url);
145
+ response.cookies.delete(cookieName);
146
+ return response;
147
+ }
148
+ const backendSession = await resolveBackendSession({
149
+ issuer: refreshConfig.issuer,
150
+ clientId: refreshConfig.clientId,
151
+ clientSecret: refreshConfig.clientSecret,
152
+ sessionId
153
+ }).catch((error) => {
154
+ console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
155
+ return null;
156
+ });
157
+ if (!backendSession) {
158
+ const signInUrl = options.pages?.signIn || "/auth/signin";
159
+ const url = new URL(signInUrl, req.url);
160
+ url.searchParams.set("callbackUrl", pathname);
161
+ const response = import_server.NextResponse.redirect(url);
162
+ response.cookies.delete(cookieName);
163
+ return response;
164
+ }
165
+ const requestHeaders = new Headers(req.headers);
166
+ requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
167
+ return import_server.NextResponse.next({
168
+ request: {
169
+ headers: requestHeaders
170
+ }
171
+ });
172
+ }
96
173
  const expiresAt = token.expiresAt;
97
174
  const now = Math.floor(Date.now() / 1e3);
98
175
  const bufferSeconds = 15;
@@ -106,7 +183,7 @@ function withOAuth42Auth(options = {}) {
106
183
  path: pathname
107
184
  });
108
185
  }
109
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
186
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
110
187
  const userId = token.sub;
111
188
  let refreshPromise = pendingRefreshes.get(userId);
112
189
  if (refreshPromise) {
@@ -115,11 +192,14 @@ function withOAuth42Auth(options = {}) {
115
192
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
116
193
  refreshPromise = refreshTokens(
117
194
  token.refreshToken,
118
- clientId,
119
- clientSecret,
120
- issuer
195
+ refreshConfig.clientId,
196
+ refreshConfig.clientSecret,
197
+ refreshConfig.issuer
121
198
  ).finally(() => {
122
- setTimeout(() => pendingRefreshes.delete(userId), 3e4);
199
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
200
+ if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
201
+ cleanupTimer.unref();
202
+ }
123
203
  });
124
204
  pendingRefreshes.set(userId, refreshPromise);
125
205
  }
@@ -146,7 +226,8 @@ function withOAuth42Auth(options = {}) {
146
226
  httpOnly: true,
147
227
  sameSite: "lax",
148
228
  path: "/",
149
- secure: process.env.NODE_ENV === "production"
229
+ secure: process.env.NODE_ENV === "production",
230
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
150
231
  };
151
232
  if (newJwt.length <= CHUNK_SIZE) {
152
233
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -194,7 +275,23 @@ function withOAuth42Auth(options = {}) {
194
275
  });
195
276
  return response;
196
277
  } else {
197
- console.log("[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), clearing session");
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
+ console.log("[OAuth42 Middleware] Refresh failed, clearing session");
198
295
  const signInUrl = options.pages?.signIn || "/auth/signin";
199
296
  const url = new URL(signInUrl, req.url);
200
297
  url.searchParams.set("callbackUrl", pathname);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/index.ts","../../src/server/middleware.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';\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;\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\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 const clientId = process.env.OAUTH42_CLIENT_ID;\n const clientSecret = process.env.OAUTH42_CLIENT_SECRET;\n const issuer = process.env.OAUTH42_ISSUER || 'https://localhost:8443';\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 // 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 && clientId && 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 clientId,\n clientSecret,\n issuer\n ).finally(() => {\n setTimeout(() => pendingRefreshes.delete(userId), 30000);\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 };\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 // Refresh failed - clear cookie and redirect to sign in (no error, just let user log in fresh)\n console.log('[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), 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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,oBAA0C;AAC1C,iBAAiC;AAWjC,IAAM,sBAAsB;AAC5B,IAAM,8BAA8B;AACpC,IAAM,aAAa,sBAAsB;AAMzC,IAAM,mBAAmB,oBAAI,IAMzB;AAsBJ,eAAe,cACb,cACA,UACA,cACA,QACgH;AAChH,MAAI;AACF,UAAM,WAAW,GAAG,MAAM;AAE1B,UAAM,WAAW,MAAM,MAAM,UAAU;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,eAAe;AAAA,QACf,WAAW;AAAA,QACX,eAAe;AAAA,MACjB,CAAC;AAAA,IACH,CAAC;AAED,UAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,8CAA8C,IAAI;AAChE,aAAO,EAAE,SAAS,OAAO,OAAO,KAAK,SAAS,iBAAiB;AAAA,IACjE;AAEA,YAAQ,IAAI,mDAAmD;AAC/D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa,KAAK;AAAA,MAClB,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,KAAK,KAAK,cAAc;AAAA,IACjE;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,6CAA6C,KAAK;AAChE,WAAO,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAAA,EAClD;AACF;AASO,SAAS,gBAAgB,UAA8B,CAAC,GAAG;AAChE,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,eAAe,QAAQ,IAAI;AACjC,QAAM,SAAS,QAAQ,IAAI,kBAAkB;AAE7C,MAAI,CAAC,QAAQ;AACX,YAAQ,KAAK,8CAA8C;AAAA,EAC7D;AAEA,SAAO,eAAe,WAAW,KAAkB;AAEjD,UAAM,aAAa,QAAQ,eACvB,GAAG,QAAQ,YAAY,mBACvB;AAEJ,UAAM,QAAQ,UAAM,qBAAS;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,UAAM,WAAW,IAAI,QAAQ;AAG7B,QAAI,QAAQ,aAAa,KAAK,UAAQ,SAAS,WAAW,IAAI,CAAC,GAAG;AAChE,aAAO,2BAAa,KAAK;AAAA,IAC3B;AAGA,UAAM,kBAAkB,QAAQ,iBAC5B,QAAQ,eAAe,KAAK,UAAQ,SAAS,WAAW,IAAI,CAAC,IAC7D;AAEJ,QAAI,CAAC,iBAAiB;AACpB,aAAO,2BAAa,KAAK;AAAA,IAC3B;AAGA,QAAI,CAAC,OAAO;AACV,YAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,YAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,UAAI,aAAa,IAAI,eAAe,QAAQ;AAC5C,aAAO,2BAAa,SAAS,GAAG;AAAA,IAClC;AAIA,UAAM,YAAY,MAAM;AACxB,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,gBAAgB;AACtB,UAAM,eAAe,aAAa,OAAO,YAAY;AAErD,QAAI,cAAc;AAChB,cAAQ,IAAI,6CAA6C;AAAA,QACvD;AAAA,QACA;AAAA,QACA,YAAY,YAAY,MAAM,YAAY;AAAA,QAC1C,mBAAoB,MAAM,aAAwB,UAAU,GAAG,EAAE;AAAA,QACjE,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,QAAI,gBAAgB,MAAM,gBAAgB,YAAY,cAAc;AAClE,YAAM,SAAS,MAAM;AAGrB,UAAI,iBAAiB,iBAAiB,IAAI,MAAM;AAEhD,UAAI,gBAAgB;AAClB,gBAAQ,IAAI,uDAAuD;AAAA,MACrE,OAAO;AACL,gBAAQ,IAAI,0DAA0D;AAMtE,yBAAiB;AAAA,UACf,MAAM;AAAA,UACN;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,QAAQ,MAAM;AACd,qBAAW,MAAM,iBAAiB,OAAO,MAAM,GAAG,GAAK;AAAA,QACzD,CAAC;AAED,yBAAiB,IAAI,QAAQ,cAAc;AAAA,MAC7C;AAEA,YAAM,YAAY,MAAM;AAExB,UAAI,UAAU,WAAW,UAAU,eAAe,UAAU,cAAc;AAExE,cAAM,eAAe;AAAA,UACnB,GAAG;AAAA,UACH,aAAa,UAAU;AAAA,UACvB,cAAc,UAAU;AAAA,UACxB,WAAW,UAAU;AAAA,QACvB;AAGA,cAAM,SAAS,UAAM,mBAAO;AAAA,UAC1B,OAAO;AAAA,UACP;AAAA,QACF,CAAC;AAID,cAAM,iBAAiB,IAAI,QAAQ,IAAI,OAAO;AAC9C,uBAAe,IAAI,6BAA6B,UAAU,WAAW;AAErE,cAAM,WAAW,2BAAa,KAAK;AAAA,UACjC,SAAS;AAAA,YACP,SAAS;AAAA,UACX;AAAA,QACF,CAAC;AAED,cAAM,gBAAgB;AAAA,UACpB,UAAU;AAAA,UACV,UAAU;AAAA,UACV,MAAM;AAAA,UACN,QAAQ,QAAQ,IAAI,aAAa;AAAA,QACnC;AAMA,YAAI,OAAO,UAAU,YAAY;AAE/B,mBAAS,QAAQ,IAAI,YAAY,QAAQ,aAAa;AAEtD,mBAAS,IAAI,KAAK,KAAK;AACrB,kBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,gBAAI,IAAI,QAAQ,IAAI,SAAS,GAAG;AAC9B,uBAAS,QAAQ,IAAI,WAAW,IAAI,EAAE,GAAG,eAAe,QAAQ,EAAE,CAAC;AAAA,YACrE,OAAO;AACL;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAAO;AAEL,gBAAM,aAAa,KAAK,KAAK,OAAO,SAAS,UAAU;AACvD,mBAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,kBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,kBAAM,aAAa,OAAO,MAAM,IAAI,aAAa,IAAI,KAAK,UAAU;AACpE,qBAAS,QAAQ,IAAI,WAAW,YAAY,aAAa;AAAA,UAC3D;AAEA,mBAAS,QAAQ,IAAI,YAAY,IAAI,EAAE,GAAG,eAAe,QAAQ,EAAE,CAAC;AAEpE,mBAAS,IAAI,cAAc,KAAK;AAC9B,kBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,gBAAI,IAAI,QAAQ,IAAI,SAAS,GAAG;AAC9B,uBAAS,QAAQ,IAAI,WAAW,IAAI,EAAE,GAAG,eAAe,QAAQ,EAAE,CAAC;AAAA,YACrE,OAAO;AACL;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,cAAM,iBAAiB,CAAC;AACxB,iBAAS,IAAI,KAAK,KAAK;AACrB,cAAI,IAAI,QAAQ,IAAI,GAAG,UAAU,IAAI,CAAC,EAAE,GAAG;AACzC,2BAAe,KAAK,CAAC;AAAA,UACvB,OAAO;AACL;AAAA,UACF;AAAA,QACF;AACA,gBAAQ,IAAI,gDAAgD;AAAA,UAC1D,UAAU,OAAO;AAAA,UACjB,SAAS,OAAO,SAAS;AAAA,UACzB,mBAAmB,IAAI,QAAQ,IAAI,UAAU;AAAA,UAC7C,gBAAgB,eAAe,SAAS,IAAI,iBAAiB;AAAA,UAC7D,sBAAsB,UAAU,aAAa,UAAU,GAAG,EAAE;AAAA,UAC5D,cAAc,UAAU;AAAA,UACxB;AAAA,QACF,CAAC;AACD,eAAO;AAAA,MACT,OAAO;AAEL,gBAAQ,IAAI,+FAA+F;AAC3G,cAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,cAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,YAAI,aAAa,IAAI,eAAe,QAAQ;AAG5C,cAAM,WAAW,2BAAa,SAAS,GAAG;AAE1C,iBAAS,QAAQ,OAAO,UAAU;AAClC,iBAAS,IAAI,KAAK,KAAK;AACrB,gBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,cAAI,IAAI,QAAQ,IAAI,SAAS,GAAG;AAC9B,qBAAS,QAAQ,OAAO,SAAS;AAAA,UACnC,OAAO;AACL;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,QAAQ,WAAW,YAAY;AACjC,YAAM,eAAe,MAAM,QAAQ,UAAU,WAAW,EAAE,OAAO,IAAI,CAAC;AACtE,UAAI,CAAC,cAAc;AACjB,cAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,cAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,YAAI,aAAa,IAAI,eAAe,QAAQ;AAC5C,eAAO,2BAAa,SAAS,GAAG;AAAA,MAClC;AAAA,IACF;AAEA,WAAO,2BAAa,KAAK;AAAA,EAC3B;AACF;AAKO,SAAS,uBACd,iBAA2B,CAAC,YAAY,GACxC,cAAwB,CAAC,SAAS,WAAW,GAC7C;AACA,SAAO;AAAA,IACL,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQP;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
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,10 +1,54 @@
1
1
  // src/server/middleware.ts
2
2
  import { NextResponse } from "next/server";
3
3
  import { getToken, encode } from "next-auth/jwt";
4
+
5
+ // src/server/backend-session.ts
6
+ function isBackendSessionMode(mode) {
7
+ return mode === "backend";
8
+ }
9
+ function sessionEndpoint(issuer) {
10
+ return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
11
+ }
12
+ async function resolveBackendSession(params) {
13
+ const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify({
17
+ session_id: params.sessionId,
18
+ client_id: params.clientId,
19
+ client_secret: params.clientSecret,
20
+ issuer: params.issuer
21
+ })
22
+ });
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();
30
+ }
31
+
32
+ // src/server/middleware.ts
4
33
  var ALLOWED_COOKIE_SIZE = 4096;
5
34
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
6
35
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
36
+ var REFRESH_RACE_GRACE_SECONDS = 60;
37
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
7
38
  var pendingRefreshes = /* @__PURE__ */ new Map();
39
+ function shouldPreserveSessionOnRefreshFailure(params) {
40
+ if (params.error !== "invalid_grant" || !params.expiresAt) {
41
+ return false;
42
+ }
43
+ return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
44
+ }
45
+ function resolveRefreshConfig(options, token) {
46
+ return {
47
+ clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
48
+ clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
49
+ issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
50
+ };
51
+ }
8
52
  async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
9
53
  try {
10
54
  const tokenUrl = `${issuer}/oauth2/token`;
@@ -39,9 +83,6 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
39
83
  }
40
84
  function withOAuth42Auth(options = {}) {
41
85
  const secret = process.env.NEXTAUTH_SECRET;
42
- const clientId = process.env.OAUTH42_CLIENT_ID;
43
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
44
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
45
86
  if (!secret) {
46
87
  console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
47
88
  }
@@ -66,6 +107,42 @@ function withOAuth42Auth(options = {}) {
66
107
  url.searchParams.set("callbackUrl", pathname);
67
108
  return NextResponse.redirect(url);
68
109
  }
110
+ const refreshConfig = resolveRefreshConfig(options, token);
111
+ if (isBackendSessionMode(options.sessionStore)) {
112
+ const sessionId = token.oauth42SessionId;
113
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
114
+ const signInUrl = options.pages?.signIn || "/auth/signin";
115
+ const url = new URL(signInUrl, req.url);
116
+ url.searchParams.set("callbackUrl", pathname);
117
+ const response = NextResponse.redirect(url);
118
+ response.cookies.delete(cookieName);
119
+ return response;
120
+ }
121
+ const backendSession = await resolveBackendSession({
122
+ issuer: refreshConfig.issuer,
123
+ clientId: refreshConfig.clientId,
124
+ clientSecret: refreshConfig.clientSecret,
125
+ sessionId
126
+ }).catch((error) => {
127
+ console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
128
+ return null;
129
+ });
130
+ if (!backendSession) {
131
+ const signInUrl = options.pages?.signIn || "/auth/signin";
132
+ const url = new URL(signInUrl, req.url);
133
+ url.searchParams.set("callbackUrl", pathname);
134
+ const response = NextResponse.redirect(url);
135
+ response.cookies.delete(cookieName);
136
+ return response;
137
+ }
138
+ const requestHeaders = new Headers(req.headers);
139
+ requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
140
+ return NextResponse.next({
141
+ request: {
142
+ headers: requestHeaders
143
+ }
144
+ });
145
+ }
69
146
  const expiresAt = token.expiresAt;
70
147
  const now = Math.floor(Date.now() / 1e3);
71
148
  const bufferSeconds = 15;
@@ -79,7 +156,7 @@ function withOAuth42Auth(options = {}) {
79
156
  path: pathname
80
157
  });
81
158
  }
82
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
159
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
83
160
  const userId = token.sub;
84
161
  let refreshPromise = pendingRefreshes.get(userId);
85
162
  if (refreshPromise) {
@@ -88,11 +165,14 @@ function withOAuth42Auth(options = {}) {
88
165
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
89
166
  refreshPromise = refreshTokens(
90
167
  token.refreshToken,
91
- clientId,
92
- clientSecret,
93
- issuer
168
+ refreshConfig.clientId,
169
+ refreshConfig.clientSecret,
170
+ refreshConfig.issuer
94
171
  ).finally(() => {
95
- setTimeout(() => pendingRefreshes.delete(userId), 3e4);
172
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
173
+ if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
174
+ cleanupTimer.unref();
175
+ }
96
176
  });
97
177
  pendingRefreshes.set(userId, refreshPromise);
98
178
  }
@@ -119,7 +199,8 @@ function withOAuth42Auth(options = {}) {
119
199
  httpOnly: true,
120
200
  sameSite: "lax",
121
201
  path: "/",
122
- secure: process.env.NODE_ENV === "production"
202
+ secure: process.env.NODE_ENV === "production",
203
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
123
204
  };
124
205
  if (newJwt.length <= CHUNK_SIZE) {
125
206
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -167,7 +248,23 @@ function withOAuth42Auth(options = {}) {
167
248
  });
168
249
  return response;
169
250
  } else {
170
- console.log("[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), clearing session");
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
+ console.log("[OAuth42 Middleware] Refresh failed, clearing session");
171
268
  const signInUrl = options.pages?.signIn || "/auth/signin";
172
269
  const url = new URL(signInUrl, req.url);
173
270
  url.searchParams.set("callbackUrl", pathname);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/middleware.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\nimport { getToken, encode } from 'next-auth/jwt';\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;\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\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 const clientId = process.env.OAUTH42_CLIENT_ID;\n const clientSecret = process.env.OAUTH42_CLIENT_SECRET;\n const issuer = process.env.OAUTH42_ISSUER || 'https://localhost:8443';\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 // 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 && clientId && 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 clientId,\n clientSecret,\n issuer\n ).finally(() => {\n setTimeout(() => pendingRefreshes.delete(userId), 30000);\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 };\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 // Refresh failed - clear cookie and redirect to sign in (no error, just let user log in fresh)\n console.log('[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), 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"],"mappings":"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","names":[]}
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":";AAAA,SAAsB,oBAAoB;AAC1C,SAAS,UAAU,cAAc;;;ACwB1B,SAAS,qBAAqB,MAAkC;AACrE,SAAO,SAAS;AAClB;AAEA,SAAS,gBAAgB,QAAwB;AAC/C,SAAO,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC;AACrC;AA+BA,eAAsB,sBAAsB,QAED;AACzC,QAAM,WAAW,MAAM,MAAM,GAAG,gBAAgB,OAAO,MAAM,CAAC,YAAY;AAAA,IACxE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB,YAAY,OAAO;AAAA,MACnB,WAAW,OAAO;AAAA,MAClB,eAAe,OAAO;AAAA,MACtB,QAAQ,OAAO;AAAA,IACjB,CAAC;AAAA,EACH,CAAC;AAED,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACjF,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,2CAA2C,SAAS,MAAM,EAAE;AAAA,EAC9E;AAEA,SAAO,SAAS,KAAK;AACvB;;;ADpEA,IAAM,sBAAsB;AAC5B,IAAM,8BAA8B;AACpC,IAAM,aAAa,sBAAsB;AACzC,IAAM,6BAA6B;AACnC,IAAM,kCAAkC,MAAM,KAAK,KAAK;AAMxD,IAAM,mBAAmB,oBAAI,IAMzB;AAuCG,SAAS,sCAAsC,QAI1C;AACV,MAAI,OAAO,UAAU,mBAAmB,CAAC,OAAO,WAAW;AACzD,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,MAAM,OAAO,aAAa;AAC1C;AAEO,SAAS,qBACd,SACA,OAC8D;AAC9D,SAAO;AAAA,IACL,UACE,QAAQ,YACP,MAAM,YACP,QAAQ,IAAI;AAAA,IACd,cACE,QAAQ,gBACP,MAAM,gBACP,QAAQ,IAAI;AAAA,IACd,QACE,QAAQ,UACP,MAAM,UACP,QAAQ,IAAI,kBACZ;AAAA,EACJ;AACF;AAKA,eAAe,cACb,cACA,UACA,cACA,QACgH;AAChH,MAAI;AACF,UAAM,WAAW,GAAG,MAAM;AAE1B,UAAM,WAAW,MAAM,MAAM,UAAU;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,eAAe;AAAA,QACf,WAAW;AAAA,QACX,eAAe;AAAA,MACjB,CAAC;AAAA,IACH,CAAC;AAED,UAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,8CAA8C,IAAI;AAChE,aAAO,EAAE,SAAS,OAAO,OAAO,KAAK,SAAS,iBAAiB;AAAA,IACjE;AAEA,YAAQ,IAAI,mDAAmD;AAC/D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa,KAAK;AAAA,MAClB,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,KAAK,KAAK,cAAc;AAAA,IACjE;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,6CAA6C,KAAK;AAChE,WAAO,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAAA,EAClD;AACF;AASO,SAAS,gBAAgB,UAA8B,CAAC,GAAG;AAChE,QAAM,SAAS,QAAQ,IAAI;AAE3B,MAAI,CAAC,QAAQ;AACX,YAAQ,KAAK,8CAA8C;AAAA,EAC7D;AAEA,SAAO,eAAe,WAAW,KAAkB;AAEjD,UAAM,aAAa,QAAQ,eACvB,GAAG,QAAQ,YAAY,mBACvB;AAEJ,UAAM,QAAQ,MAAM,SAAS;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,UAAM,WAAW,IAAI,QAAQ;AAG7B,QAAI,QAAQ,aAAa,KAAK,UAAQ,SAAS,WAAW,IAAI,CAAC,GAAG;AAChE,aAAO,aAAa,KAAK;AAAA,IAC3B;AAGA,UAAM,kBAAkB,QAAQ,iBAC5B,QAAQ,eAAe,KAAK,UAAQ,SAAS,WAAW,IAAI,CAAC,IAC7D;AAEJ,QAAI,CAAC,iBAAiB;AACpB,aAAO,aAAa,KAAK;AAAA,IAC3B;AAGA,QAAI,CAAC,OAAO;AACV,YAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,YAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,UAAI,aAAa,IAAI,eAAe,QAAQ;AAC5C,aAAO,aAAa,SAAS,GAAG;AAAA,IAClC;AAEA,UAAM,gBAAgB,qBAAqB,SAAS,KAAK;AAEzD,QAAI,qBAAqB,QAAQ,YAAY,GAAG;AAC9C,YAAM,YAAY,MAAM;AACxB,UAAI,CAAC,aAAa,CAAC,cAAc,YAAY,CAAC,cAAc,cAAc;AACxE,cAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,cAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,YAAI,aAAa,IAAI,eAAe,QAAQ;AAC5C,cAAM,WAAW,aAAa,SAAS,GAAG;AAC1C,iBAAS,QAAQ,OAAO,UAAU;AAClC,eAAO;AAAA,MACT;AAEA,YAAM,iBAAiB,MAAM,sBAAsB;AAAA,QACjD,QAAQ,cAAc;AAAA,QACtB,UAAU,cAAc;AAAA,QACxB,cAAc,cAAc;AAAA,QAC5B;AAAA,MACF,CAAC,EAAE,MAAM,WAAS;AAChB,gBAAQ,MAAM,wDAAwD,KAAK;AAC3E,eAAO;AAAA,MACT,CAAC;AAED,UAAI,CAAC,gBAAgB;AACnB,cAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,cAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,YAAI,aAAa,IAAI,eAAe,QAAQ;AAC5C,cAAM,WAAW,aAAa,SAAS,GAAG;AAC1C,iBAAS,QAAQ,OAAO,UAAU;AAClC,eAAO;AAAA,MACT;AAEA,YAAM,iBAAiB,IAAI,QAAQ,IAAI,OAAO;AAC9C,qBAAe,IAAI,6BAA6B,eAAe,YAAY;AAC3E,aAAO,aAAa,KAAK;AAAA,QACvB,SAAS;AAAA,UACP,SAAS;AAAA,QACX;AAAA,MACF,CAAC;AAAA,IACH;AAIA,UAAM,YAAY,MAAM;AACxB,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,gBAAgB;AACtB,UAAM,eAAe,aAAa,OAAO,YAAY;AAErD,QAAI,cAAc;AAChB,cAAQ,IAAI,6CAA6C;AAAA,QACvD;AAAA,QACA;AAAA,QACA,YAAY,YAAY,MAAM,YAAY;AAAA,QAC1C,mBAAoB,MAAM,aAAwB,UAAU,GAAG,EAAE;AAAA,QACjE,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,QAAI,gBAAgB,MAAM,gBAAgB,cAAc,YAAY,cAAc,cAAc;AAC9F,YAAM,SAAS,MAAM;AAGrB,UAAI,iBAAiB,iBAAiB,IAAI,MAAM;AAEhD,UAAI,gBAAgB;AAClB,gBAAQ,IAAI,uDAAuD;AAAA,MACrE,OAAO;AACL,gBAAQ,IAAI,0DAA0D;AAMtE,yBAAiB;AAAA,UACf,MAAM;AAAA,UACN,cAAc;AAAA,UACd,cAAc;AAAA,UACd,cAAc;AAAA,QAChB,EAAE,QAAQ,MAAM;AACd,gBAAM,eAAe,WAAW,MAAM,iBAAiB,OAAO,MAAM,GAAG,GAAK;AAC5E,cAAI,OAAO,iBAAiB,YAAY,WAAW,cAAc;AAC/D,yBAAa,MAAM;AAAA,UACrB;AAAA,QACF,CAAC;AAED,yBAAiB,IAAI,QAAQ,cAAc;AAAA,MAC7C;AAEA,YAAM,YAAY,MAAM;AAExB,UAAI,UAAU,WAAW,UAAU,eAAe,UAAU,cAAc;AAExE,cAAM,eAAe;AAAA,UACnB,GAAG;AAAA,UACH,aAAa,UAAU;AAAA,UACvB,cAAc,UAAU;AAAA,UACxB,WAAW,UAAU;AAAA,QACvB;AAGA,cAAM,SAAS,MAAM,OAAO;AAAA,UAC1B,OAAO;AAAA,UACP;AAAA,QACF,CAAC;AAID,cAAM,iBAAiB,IAAI,QAAQ,IAAI,OAAO;AAC9C,uBAAe,IAAI,6BAA6B,UAAU,WAAW;AAErE,cAAM,WAAW,aAAa,KAAK;AAAA,UACjC,SAAS;AAAA,YACP,SAAS;AAAA,UACX;AAAA,QACF,CAAC;AAED,cAAM,gBAAgB;AAAA,UACpB,UAAU;AAAA,UACV,UAAU;AAAA,UACV,MAAM;AAAA,UACN,QAAQ,QAAQ,IAAI,aAAa;AAAA,UACjC,QAAQ,QAAQ,SAAS,UAAU;AAAA,QACrC;AAMA,YAAI,OAAO,UAAU,YAAY;AAE/B,mBAAS,QAAQ,IAAI,YAAY,QAAQ,aAAa;AAEtD,mBAAS,IAAI,KAAK,KAAK;AACrB,kBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,gBAAI,IAAI,QAAQ,IAAI,SAAS,GAAG;AAC9B,uBAAS,QAAQ,IAAI,WAAW,IAAI,EAAE,GAAG,eAAe,QAAQ,EAAE,CAAC;AAAA,YACrE,OAAO;AACL;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAAO;AAEL,gBAAM,aAAa,KAAK,KAAK,OAAO,SAAS,UAAU;AACvD,mBAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,kBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,kBAAM,aAAa,OAAO,MAAM,IAAI,aAAa,IAAI,KAAK,UAAU;AACpE,qBAAS,QAAQ,IAAI,WAAW,YAAY,aAAa;AAAA,UAC3D;AAEA,mBAAS,QAAQ,IAAI,YAAY,IAAI,EAAE,GAAG,eAAe,QAAQ,EAAE,CAAC;AAEpE,mBAAS,IAAI,cAAc,KAAK;AAC9B,kBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,gBAAI,IAAI,QAAQ,IAAI,SAAS,GAAG;AAC9B,uBAAS,QAAQ,IAAI,WAAW,IAAI,EAAE,GAAG,eAAe,QAAQ,EAAE,CAAC;AAAA,YACrE,OAAO;AACL;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,cAAM,iBAAiB,CAAC;AACxB,iBAAS,IAAI,KAAK,KAAK;AACrB,cAAI,IAAI,QAAQ,IAAI,GAAG,UAAU,IAAI,CAAC,EAAE,GAAG;AACzC,2BAAe,KAAK,CAAC;AAAA,UACvB,OAAO;AACL;AAAA,UACF;AAAA,QACF;AACA,gBAAQ,IAAI,gDAAgD;AAAA,UAC1D,UAAU,OAAO;AAAA,UACjB,SAAS,OAAO,SAAS;AAAA,UACzB,mBAAmB,IAAI,QAAQ,IAAI,UAAU;AAAA,UAC7C,gBAAgB,eAAe,SAAS,IAAI,iBAAiB;AAAA,UAC7D,sBAAsB,UAAU,aAAa,UAAU,GAAG,EAAE;AAAA,UAC5D,cAAc,UAAU;AAAA,UACxB;AAAA,QACF,CAAC;AACD,eAAO;AAAA,MACT,OAAO;AACL,YAAI,sCAAsC;AAAA,UACxC,OAAO,UAAU;AAAA,UACjB;AAAA,UACA;AAAA,QACF,CAAC,GAAG;AACF,kBAAQ;AAAA,YACN;AAAA,YACA;AAAA,cACE;AAAA,cACA;AAAA,cACA,YAAY,MAAM;AAAA,cAClB,MAAM;AAAA,YACR;AAAA,UACF;AACA,iBAAO,aAAa,KAAK;AAAA,QAC3B;AAGA,gBAAQ,IAAI,uDAAuD;AACnE,cAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,cAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,YAAI,aAAa,IAAI,eAAe,QAAQ;AAG5C,cAAM,WAAW,aAAa,SAAS,GAAG;AAE1C,iBAAS,QAAQ,OAAO,UAAU;AAClC,iBAAS,IAAI,KAAK,KAAK;AACrB,gBAAM,YAAY,GAAG,UAAU,IAAI,CAAC;AACpC,cAAI,IAAI,QAAQ,IAAI,SAAS,GAAG;AAC9B,qBAAS,QAAQ,OAAO,SAAS;AAAA,UACnC,OAAO;AACL;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,QAAQ,WAAW,YAAY;AACjC,YAAM,eAAe,MAAM,QAAQ,UAAU,WAAW,EAAE,OAAO,IAAI,CAAC;AACtE,UAAI,CAAC,cAAc;AACjB,cAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,cAAM,MAAM,IAAI,IAAI,WAAW,IAAI,GAAG;AACtC,YAAI,aAAa,IAAI,eAAe,QAAQ;AAC5C,eAAO,aAAa,SAAS,GAAG;AAAA,MAClC;AAAA,IACF;AAEA,WAAO,aAAa,KAAK;AAAA,EAC3B;AACF;AAKO,SAAS,uBACd,iBAA2B,CAAC,YAAY,GACxC,cAAwB,CAAC,SAAS,WAAW,GAC7C;AACA,SAAO;AAAA,IACL,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQP;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
- export { C as CreateAuthOptions, a as OAuth42Provider, c as createAuth, f as createHandlers, g as getOAuth42Session, d as getServerSession, r as refreshAccessToken, w as withOAuth42ServerSideProps, e as withOAuth42Session } from '../auth-BThRQMzh.mjs';
1
+ export { C as CreateAuthOptions, a as OAuth42Provider, c as createAuth, f as createHandlers, g as getOAuth42Session, d as getServerSession, r as refreshAccessToken, w as withOAuth42ServerSideProps, e as withOAuth42Session } from '../auth-D3tAruwx.mjs';
2
2
  export { default as NextAuth, NextAuthOptions } from 'next-auth';
3
- export { OAuth42AuthOptions, createMiddlewareConfig, withOAuth42Auth } from '../middleware/index.mjs';
3
+ export { B as BackendSessionConfig, a as BackendSessionResponse, b as BackendSessionUser, O as OAuth42AuthOptions, S as SessionStoreMode, d as createBackendSession, c as createMiddlewareConfig, r as resolveBackendSession, e as revokeBackendSession, w as withOAuth42Auth } from '../index-Cfqxl9r2.mjs';
4
4
  import { NextRequest, NextResponse } from 'next/server';
5
5
  import 'next-auth/providers/oauth';
6
6
  import 'next';
@@ -1,6 +1,6 @@
1
- export { C as CreateAuthOptions, a as OAuth42Provider, c as createAuth, f as createHandlers, g as getOAuth42Session, d as getServerSession, r as refreshAccessToken, w as withOAuth42ServerSideProps, e as withOAuth42Session } from '../auth-BThRQMzh.js';
1
+ export { C as CreateAuthOptions, a as OAuth42Provider, c as createAuth, f as createHandlers, g as getOAuth42Session, d as getServerSession, r as refreshAccessToken, w as withOAuth42ServerSideProps, e as withOAuth42Session } from '../auth-CYMPe3k0.js';
2
2
  export { default as NextAuth, NextAuthOptions } from 'next-auth';
3
- export { OAuth42AuthOptions, createMiddlewareConfig, withOAuth42Auth } from '../middleware/index.js';
3
+ export { B as BackendSessionConfig, a as BackendSessionResponse, b as BackendSessionUser, O as OAuth42AuthOptions, S as SessionStoreMode, d as createBackendSession, c as createMiddlewareConfig, r as resolveBackendSession, e as revokeBackendSession, w as withOAuth42Auth } from '../index-Cfqxl9r2.js';
4
4
  import { NextRequest, NextResponse } from 'next/server';
5
5
  import 'next-auth/providers/oauth';
6
6
  import 'next';