@oauth42/next 0.4.8 → 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.
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __typeError = (msg) => {
9
+ throw TypeError(msg);
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -26,25 +29,215 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
29
  mod
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
33
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
34
+ 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);
35
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
29
36
 
30
37
  // src/server/index.ts
31
38
  var server_exports = {};
32
39
  __export(server_exports, {
33
40
  NextAuth: () => import_next_auth3.default,
41
+ OAuth42ClientAssertion: () => OAuth42ClientAssertion,
42
+ OAuth42DPoP: () => OAuth42DPoP,
34
43
  OAuth42Provider: () => OAuth42Provider,
35
44
  createAuth: () => createAuth,
45
+ createBackendSession: () => createBackendSession,
36
46
  createHandlers: () => createHandlers,
37
47
  createHostedAuthCallback: () => createHostedAuthCallback,
38
48
  createMiddlewareConfig: () => createMiddlewareConfig,
39
49
  getOAuth42Session: () => getOAuth42Session,
40
50
  getServerSession: () => getServerSession,
51
+ pushAuthorizationURL: () => pushAuthorizationURL,
41
52
  refreshAccessToken: () => refreshAccessToken,
53
+ resolveBackendSession: () => resolveBackendSession,
54
+ revokeBackendSession: () => revokeBackendSession,
42
55
  withOAuth42Auth: () => withOAuth42Auth,
43
56
  withOAuth42ServerSideProps: () => withOAuth42ServerSideProps,
44
- withOAuth42Session: () => withOAuth42Session
57
+ withOAuth42Session: () => withOAuth42Session,
58
+ withPushedAuthorization: () => withPushedAuthorization
45
59
  });
46
60
  module.exports = __toCommonJS(server_exports);
47
61
 
62
+ // src/server/client-assertion.ts
63
+ var import_node_crypto = require("crypto");
64
+ var _key, _public;
65
+ var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
66
+ constructor(kid, key) {
67
+ __privateAdd(this, _key);
68
+ __privateAdd(this, _public);
69
+ if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
70
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
71
+ throw new Error("Client assertions require a private P-256 key");
72
+ }
73
+ __privateSet(this, _key, key);
74
+ this.kid = kid;
75
+ const publicKey = (0, import_node_crypto.createPublicKey)(key).export({ format: "jwk" });
76
+ __privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
77
+ this.thumbprint = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
78
+ }
79
+ static generate(kid) {
80
+ return new _OAuth42ClientAssertion(kid, (0, import_node_crypto.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
81
+ }
82
+ get publicJwk() {
83
+ return { ...__privateGet(this, _public), kid: this.kid };
84
+ }
85
+ /** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
86
+ get oidcJwks() {
87
+ return { keys: [{
88
+ kty: "EC",
89
+ crv: "P-256",
90
+ x: __privateGet(this, _public).x,
91
+ y: __privateGet(this, _public).y,
92
+ d: __privateGet(this, _key).export({ format: "jwk" }).d,
93
+ kid: this.kid
94
+ }] };
95
+ }
96
+ static validateEndpoint(target) {
97
+ const url = new URL(target);
98
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
99
+ throw new Error("Client authentication requires HTTPS outside loopback development");
100
+ }
101
+ }
102
+ assertion(clientId, audience) {
103
+ _OAuth42ClientAssertion.validateEndpoint(audience);
104
+ if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
105
+ const now = Math.floor(Date.now() / 1e3);
106
+ const header = { alg: "ES256", typ: "JWT", kid: this.kid };
107
+ const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: (0, import_node_crypto.randomBytes)(32).toString("base64url") };
108
+ const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
109
+ return `${data}.${(0, import_node_crypto.sign)("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
110
+ }
111
+ async fetch(clientId, target, init, dpop) {
112
+ if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
113
+ throw new Error("Client assertions require a form POST");
114
+ }
115
+ const prepare = () => {
116
+ const body = new URLSearchParams(init.body);
117
+ body.delete("client_secret");
118
+ body.set("client_id", clientId);
119
+ body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
120
+ body.set("client_assertion", this.assertion(clientId, target));
121
+ const headers = new Headers(init.headers);
122
+ headers.delete("Authorization");
123
+ headers.delete("Content-Length");
124
+ headers.delete("Transfer-Encoding");
125
+ headers.set("Content-Type", "application/x-www-form-urlencoded");
126
+ return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
127
+ };
128
+ return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
129
+ }
130
+ };
131
+ _key = new WeakMap();
132
+ _public = new WeakMap();
133
+ var OAuth42ClientAssertion = _OAuth42ClientAssertion;
134
+
135
+ // src/server/par.ts
136
+ async function pushAuthorizationURL(value, config) {
137
+ const base = config.issuer.replace(/\/$/, "");
138
+ let url;
139
+ try {
140
+ url = new URL(value);
141
+ } catch {
142
+ return value;
143
+ }
144
+ if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
145
+ if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
146
+ const body = new URLSearchParams(url.search);
147
+ if (body.getAll("client_id").length !== 1 || body.get("client_id") !== config.clientId || body.get("response_type") !== "code" || !body.get("state") || !body.get("nonce") || body.get("code_challenge_method") !== "S256" || !/^[A-Za-z0-9_-]{43}$/.test(body.get("code_challenge") || "") || ["request_uri", "client_secret", "client_assertion", "client_assertion_type"].some((k) => body.has(k))) {
148
+ throw new Error("Invalid PAR authorization transaction");
149
+ }
150
+ const target = `${base}/oauth2/par`;
151
+ OAuth42ClientAssertion.validateEndpoint(target);
152
+ if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
153
+ const init = {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
156
+ body,
157
+ redirect: "error",
158
+ cache: "no-store",
159
+ signal: AbortSignal.timeout(3e4)
160
+ };
161
+ const response = config.clientAssertionKey ? await config.clientAssertionKey.fetch(config.clientId, target, init, config.dpop) : config.dpop ? await config.dpop.fetch(target, init) : await fetch(target, init);
162
+ if (response.status !== 201) throw new Error("Pushed authorization failed");
163
+ const pushed = await response.json();
164
+ if (typeof pushed.request_uri !== "string" || !/^urn:ietf:params:oauth:request_uri:[A-Za-z0-9_-]{43}$/.test(pushed.request_uri) || !Number.isInteger(pushed.expires_in) || pushed.expires_in <= 0 || pushed.expires_in > 600) throw new Error("Invalid PAR response");
165
+ url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
166
+ return url.href;
167
+ }
168
+ function withPushedAuthorization(handler, config) {
169
+ return async (request, context) => {
170
+ if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
171
+ const response = await handler(request, context);
172
+ const location = response.headers.get("Location");
173
+ if (location) {
174
+ const next = await pushAuthorizationURL(location, config);
175
+ if (next === location) return response;
176
+ const headers = new Headers(response.headers);
177
+ headers.set("Location", next);
178
+ headers.delete("content-length");
179
+ return new Response(response.body, { status: response.status, headers });
180
+ }
181
+ if (response.headers.get("content-type")?.includes("application/json")) {
182
+ const body = await response.clone().json();
183
+ if (typeof body?.url === "string") {
184
+ const next = await pushAuthorizationURL(body.url, config);
185
+ if (next !== body.url) {
186
+ const headers = new Headers(response.headers);
187
+ headers.delete("content-length");
188
+ return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
189
+ }
190
+ }
191
+ }
192
+ return response;
193
+ };
194
+ }
195
+
196
+ // src/server/verified-userinfo.ts
197
+ async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
198
+ if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
199
+ try {
200
+ const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
201
+ const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
202
+ const response = await fetcher(target, {
203
+ headers: { Authorization: `Bearer ${accessToken}` },
204
+ redirect: "error",
205
+ cache: "no-store",
206
+ signal: AbortSignal.timeout(3e4)
207
+ });
208
+ if (!response.ok) return null;
209
+ const user = await response.json();
210
+ return parseVerifiedUserInfo(user, subject);
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ function parseVerifiedUserInfo(value, subject) {
216
+ if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
217
+ const user = value;
218
+ if (user.sub !== subject) return null;
219
+ if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
220
+ if (user.email !== void 0 && typeof user.email !== "string") return null;
221
+ if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
222
+ const profile = { sub: user.sub };
223
+ for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
224
+ if (user[field] != null) {
225
+ if (typeof user[field] !== "string") return null;
226
+ profile[field] = user[field];
227
+ }
228
+ }
229
+ if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
230
+ if (user.groups !== void 0) profile.groups = user.groups;
231
+ return profile;
232
+ }
233
+ async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
234
+ if (typeof session.access_token !== "string" || !session.access_token) return null;
235
+ if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
236
+ return parseVerifiedUserInfo(session.user_info, subject);
237
+ }
238
+ return verifiedUserInfo(issuer, session.access_token, subject, dpop);
239
+ }
240
+
48
241
  // src/server/auth.ts
49
242
  var import_next_auth2 = __toESM(require("next-auth"));
50
243
 
@@ -52,13 +245,20 @@ var import_next_auth2 = __toESM(require("next-auth"));
52
245
  function OAuth42Provider(options) {
53
246
  const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
54
247
  const baseUrl = issuer.replace(/\/$/, "");
248
+ const { dpop, clientAssertionKey, ...providerOptions } = options;
249
+ if (clientAssertionKey) {
250
+ OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
251
+ providerOptions.clientSecret = void 0;
252
+ }
253
+ if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
55
254
  return {
56
255
  id: "oauth42",
57
256
  name: "OAuth42",
58
257
  type: "oauth",
59
258
  version: "2.0",
60
- // Use OIDC discovery to automatically find endpoints
61
- wellKnown: `${baseUrl}/.well-known/openid-configuration`,
259
+ // Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
260
+ // Constructing its OIDC client from these trusted endpoints preserves issuer binding.
261
+ jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
62
262
  // Use ID token for user profile (standard OIDC pattern)
63
263
  // This avoids an extra HTTP call to /userinfo on every auth flow
64
264
  // Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
@@ -71,17 +271,26 @@ function OAuth42Provider(options) {
71
271
  response_type: "code"
72
272
  }
73
273
  },
74
- token: `${baseUrl}/oauth2/token`,
274
+ token: dpop ? {
275
+ url: `${baseUrl}/oauth2/token`,
276
+ async request({ client, provider, params, checks }) {
277
+ const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
278
+ dpop.assertTokenResponse(tokens);
279
+ return { tokens };
280
+ }
281
+ } : `${baseUrl}/oauth2/token`,
75
282
  userinfo: `${baseUrl}/oauth2/userinfo`,
283
+ ...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
76
284
  client: {
77
285
  id: options.clientId,
78
- secret: options.clientSecret,
79
- token_endpoint_auth_method: "client_secret_post",
286
+ secret: clientAssertionKey ? void 0 : options.clientSecret,
287
+ token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
288
+ ...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
80
289
  id_token_signed_response_alg: "RS256"
81
290
  // OAuth42 uses RS256 for ID tokens
82
291
  },
83
- issuer: baseUrl,
84
- checks: options.pkceEnabled !== false ? ["pkce", "state"] : ["state"],
292
+ issuer,
293
+ checks: ["pkce", "state", "nonce"],
85
294
  profile(profile, tokens) {
86
295
  return {
87
296
  id: profile.sub || profile.id || profile.email,
@@ -97,42 +306,18 @@ function OAuth42Provider(options) {
97
306
  bg: "#1e40af",
98
307
  text: "#ffffff"
99
308
  },
100
- options
309
+ // NextAuth v4 types require a string even when private_key_jwt sends no secret.
310
+ options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
101
311
  };
102
312
  }
103
313
 
104
314
  // src/server/session.ts
105
315
  var import_next_auth = require("next-auth");
106
- var import_headers = require("next/headers");
107
- var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
108
316
  async function getOAuth42Session(...args) {
109
317
  const session = await (0, import_next_auth.getServerSession)(...args);
110
- if (!session) {
318
+ if (!session || session.error) {
111
319
  return null;
112
320
  }
113
- let refreshedToken = null;
114
- try {
115
- if (args.length === 1) {
116
- const headersList = await (0, import_headers.headers)();
117
- refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
118
- } else if (args.length === 3) {
119
- const req = args[0];
120
- refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
121
- }
122
- } catch {
123
- }
124
- if (refreshedToken) {
125
- const sessionToken = session.accessToken;
126
- console.log("[OAuth42 Session] Using refreshed token from middleware:", {
127
- refreshedPrefix: refreshedToken.substring(0, 20),
128
- sessionPrefix: sessionToken?.substring(0, 20),
129
- tokensMatch: refreshedToken === sessionToken
130
- });
131
- return {
132
- ...session,
133
- accessToken: refreshedToken
134
- };
135
- }
136
321
  return session;
137
322
  }
138
323
  function withOAuth42Session(handler, authOptions) {
@@ -163,13 +348,66 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
163
348
  };
164
349
  }
165
350
 
351
+ // src/server/backend-session.ts
352
+ function isBackendSessionMode(mode) {
353
+ return mode === "backend";
354
+ }
355
+ function sessionEndpoint(issuer) {
356
+ return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
357
+ }
358
+ async function sessionFetch(params, target, body, credential, method = "POST") {
359
+ OAuth42ClientAssertion.validateEndpoint(target);
360
+ const prepare = () => ({
361
+ method,
362
+ redirect: "error",
363
+ cache: "no-store",
364
+ signal: AbortSignal.timeout(3e4),
365
+ headers: { "Content-Type": "application/json" },
366
+ body: JSON.stringify({
367
+ ...body,
368
+ client_id: params.clientId,
369
+ ...params.clientAssertionKey ? {
370
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
371
+ client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
372
+ } : { client_secret: params.clientSecret }
373
+ })
374
+ });
375
+ return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
376
+ }
377
+ async function sessionResponse(response, params) {
378
+ if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
379
+ const session = await response.json();
380
+ if (params.dpop) params.dpop.assertTokenResponse(session);
381
+ else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
382
+ return session;
383
+ }
384
+ async function createBackendSession(params) {
385
+ return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
386
+ access_token: params.accessToken,
387
+ refresh_token: params.refreshToken,
388
+ id_token: params.idToken,
389
+ user: params.user
390
+ }, params.accessToken), params);
391
+ }
392
+ async function resolveBackendSession(params) {
393
+ const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
394
+ if ([401, 403, 404].includes(response.status)) return null;
395
+ return sessionResponse(response, params);
396
+ }
397
+ async function revokeBackendSession(params) {
398
+ const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
399
+ if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
400
+ }
401
+
166
402
  // src/server/auth.ts
167
403
  var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
168
- var activeRefresh = null;
404
+ var activeRefreshes = /* @__PURE__ */ new Map();
169
405
  function createAuth(options = {}) {
170
406
  const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
171
- const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
172
- if (!clientId || !clientSecret) {
407
+ const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
408
+ const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
409
+ const useBackendSessions = isBackendSessionMode(options.sessionStore);
410
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
173
411
  throw new Error(
174
412
  "OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
175
413
  );
@@ -178,24 +416,57 @@ function createAuth(options = {}) {
178
416
  providers: [
179
417
  OAuth42Provider({
180
418
  clientId,
181
- clientSecret,
182
- issuer: options.issuer,
419
+ clientSecret: clientSecret || "",
420
+ issuer,
183
421
  scopes: options.scopes,
184
- pkceEnabled: options.pkceEnabled
422
+ pkceEnabled: options.pkceEnabled,
423
+ dpop: options.dpop,
424
+ clientAssertionKey: options.clientAssertionKey
185
425
  })
186
426
  ],
187
427
  callbacks: {
188
428
  async jwt({ token, account, profile }) {
429
+ if (options.clientAssertionKey) delete token.clientSecret;
189
430
  console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
190
431
  if (account) {
191
- console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
192
- token.accessToken = account.access_token;
193
- token.refreshToken = account.refresh_token;
194
- token.expiresAt = account.expires_at;
195
- token.idToken = account.id_token;
196
- token.clientId = clientId;
197
- token.clientSecret = clientSecret;
198
- token.issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER;
432
+ if (useBackendSessions) {
433
+ if (!account.access_token || !account.refresh_token) {
434
+ throw new Error("OAuth42 backend sessions require access and refresh tokens");
435
+ }
436
+ const oauth42Profile = profile;
437
+ const backendSession = await createBackendSession({
438
+ dpop: options.dpop,
439
+ clientAssertionKey: options.clientAssertionKey,
440
+ issuer,
441
+ clientId,
442
+ clientSecret: clientSecret || "",
443
+ accessToken: account.access_token,
444
+ refreshToken: account.refresh_token,
445
+ expiresAt: account.expires_at,
446
+ idToken: account.id_token,
447
+ user: {
448
+ sub: oauth42Profile?.sub,
449
+ email: oauth42Profile?.email,
450
+ username: oauth42Profile?.username,
451
+ name: token.name,
452
+ emailVerified: oauth42Profile?.email_verified,
453
+ groups: oauth42Profile?.groups
454
+ }
455
+ });
456
+ token.oauth42SessionId = backendSession.session_id;
457
+ token.expiresAt = backendSession.expires_at;
458
+ token.issuer = issuer;
459
+ } else {
460
+ console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
461
+ token.accessToken = account.access_token;
462
+ token.refreshToken = account.refresh_token;
463
+ token.expiresAt = account.expires_at;
464
+ token.idToken = account.id_token;
465
+ token.clientId = clientId;
466
+ if (options.clientAssertionKey) delete token.clientSecret;
467
+ else token.clientSecret = clientSecret;
468
+ token.issuer = issuer;
469
+ }
199
470
  }
200
471
  if (profile) {
201
472
  const oauth42Profile = profile;
@@ -212,12 +483,68 @@ function createAuth(options = {}) {
212
483
  },
213
484
  async session({ session, token }) {
214
485
  console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
215
- session.accessToken = token.accessToken;
216
- session.idToken = token.idToken;
486
+ const revokedSession = () => {
487
+ session.error = "SessionRevoked";
488
+ delete session.accessToken;
489
+ delete session.idToken;
490
+ delete session.userInfo;
491
+ delete session.user;
492
+ return session;
493
+ };
494
+ if (token.error) return revokedSession();
495
+ let verified;
496
+ if (useBackendSessions) {
497
+ const sessionId = token.oauth42SessionId;
498
+ if (!sessionId) {
499
+ return revokedSession();
500
+ }
501
+ const backendSession = await resolveBackendSession({
502
+ dpop: options.dpop,
503
+ clientAssertionKey: options.clientAssertionKey,
504
+ issuer,
505
+ clientId,
506
+ clientSecret: clientSecret || "",
507
+ sessionId
508
+ });
509
+ if (!backendSession) {
510
+ return revokedSession();
511
+ }
512
+ session.accessToken = backendSession.access_token;
513
+ session.idToken = backendSession.id_token;
514
+ token.expiresAt = backendSession.expires_at;
515
+ verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
516
+ if (backendSession.user && session.user) {
517
+ session.user.email = backendSession.user.email;
518
+ session.user.name = backendSession.user.name;
519
+ session.user.username = backendSession.user.username;
520
+ session.user.emailVerified = backendSession.user.emailVerified;
521
+ session.user.groups = backendSession.user.groups;
522
+ }
523
+ } else {
524
+ session.accessToken = token.accessToken;
525
+ session.idToken = token.idToken;
526
+ verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
527
+ }
528
+ if (!verified) {
529
+ return revokedSession();
530
+ }
531
+ session.userInfo = verified;
532
+ token.email = verified.email;
533
+ token.name = verified.name;
534
+ token.username = verified.preferred_username ?? verified.username;
535
+ token.emailVerified = verified.email_verified;
536
+ token.groups = verified.groups ?? [];
537
+ if (session.user) {
538
+ session.user.email = verified.email;
539
+ session.user.name = verified.name;
540
+ session.user.username = verified.preferred_username ?? verified.username;
541
+ session.user.emailVerified = verified.email_verified;
542
+ session.user.groups = verified.groups ?? [];
543
+ }
217
544
  if (token.error) {
218
545
  session.error = token.error;
219
546
  }
220
- if (session.user) {
547
+ if (!useBackendSessions && session.user) {
221
548
  session.user.email = token.email;
222
549
  session.user.name = token.name;
223
550
  session.user.username = token.username;
@@ -241,6 +568,28 @@ function createAuth(options = {}) {
241
568
  strategy: "jwt",
242
569
  ...options.session
243
570
  },
571
+ events: {
572
+ async signOut(message) {
573
+ if (useBackendSessions) {
574
+ const sessionId = message.token?.oauth42SessionId;
575
+ if (sessionId) {
576
+ await revokeBackendSession({
577
+ dpop: options.dpop,
578
+ clientAssertionKey: options.clientAssertionKey,
579
+ issuer,
580
+ clientId,
581
+ clientSecret: clientSecret || "",
582
+ sessionId
583
+ }).catch((error) => {
584
+ console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
585
+ });
586
+ }
587
+ }
588
+ if (options.events?.signOut) {
589
+ await options.events.signOut(message);
590
+ }
591
+ }
592
+ },
244
593
  debug: options.debug || process.env.NODE_ENV === "development",
245
594
  secret: process.env.NEXTAUTH_SECRET,
246
595
  // Configure unique cookie names per app to prevent session conflicts on localhost
@@ -316,7 +665,8 @@ function createAuth(options = {}) {
316
665
  }
317
666
  }
318
667
  };
319
- const handler = NextAuth(authOptions);
668
+ const nextAuthHandler = NextAuth(authOptions);
669
+ const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
320
670
  return {
321
671
  auth: authOptions,
322
672
  handlers: { GET: handler, POST: handler }
@@ -327,24 +677,40 @@ function createHandlers(authOptions) {
327
677
  return { GET: handler, POST: handler };
328
678
  }
329
679
  var getServerSession = getOAuth42Session;
330
- async function refreshAccessToken(token, clientId, clientSecret, issuer) {
331
- if (activeRefresh) {
332
- console.log("[OAuth42] Refresh already in progress, waiting...");
333
- return await activeRefresh;
680
+ async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
681
+ if (typeof token.refreshToken !== "string" || !token.refreshToken) {
682
+ return { ...token, error: "RefreshAccessTokenError" };
683
+ }
684
+ const key = JSON.stringify([issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com", clientId, clientSecret, token.sub, token.refreshToken, dpop?.thumbprint, clientAssertionKey?.thumbprint, clientAssertionKey?.kid]);
685
+ let refresh = activeRefreshes.get(key);
686
+ if (!refresh) {
687
+ refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
688
+ activeRefreshes.set(key, refresh);
334
689
  }
335
- activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
336
690
  try {
337
- return await activeRefresh;
691
+ const result = await refresh;
692
+ const updated = {
693
+ ...token,
694
+ accessToken: result.accessToken,
695
+ refreshToken: result.refreshToken,
696
+ expiresAt: result.expiresAt,
697
+ error: result.error
698
+ };
699
+ if (clientAssertionKey) delete updated.clientSecret;
700
+ return updated;
338
701
  } finally {
339
- activeRefresh = null;
702
+ if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
340
703
  }
341
704
  }
342
- async function doRefresh(token, clientId, clientSecret, issuer) {
705
+ async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
343
706
  try {
344
707
  const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
345
708
  const tokenUrl = `${baseUrl}/oauth2/token`;
346
709
  const fetchOptions = {
347
710
  method: "POST",
711
+ redirect: "error",
712
+ cache: "no-store",
713
+ signal: AbortSignal.timeout(3e4),
348
714
  headers: {
349
715
  "Content-Type": "application/x-www-form-urlencoded"
350
716
  },
@@ -355,17 +721,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
355
721
  client_secret: clientSecret
356
722
  })
357
723
  };
358
- if (process.env.NODE_ENV !== "production" && tokenUrl.startsWith("https://")) {
359
- const https2 = await import("https");
360
- fetchOptions.agent = new https2.Agent({
361
- rejectUnauthorized: false
362
- });
363
- }
364
- const response = await fetch(tokenUrl, fetchOptions);
724
+ const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
365
725
  const refreshedTokens = await response.json();
366
726
  if (!response.ok) {
367
727
  throw refreshedTokens;
368
728
  }
729
+ dpop?.assertTokenResponse(refreshedTokens);
369
730
  console.log("[OAuth42] Token refreshed successfully");
370
731
  return {
371
732
  ...token,
@@ -377,7 +738,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
377
738
  error: void 0
378
739
  };
379
740
  } catch (error) {
380
- console.error("[OAuth42] Failed to refresh access token:", error);
741
+ console.error("[OAuth42] Failed to refresh access token");
381
742
  return {
382
743
  ...token,
383
744
  error: "RefreshAccessTokenError"
@@ -394,12 +755,37 @@ var import_jwt = require("next-auth/jwt");
394
755
  var ALLOWED_COOKIE_SIZE = 4096;
395
756
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
396
757
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
758
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
397
759
  var pendingRefreshes = /* @__PURE__ */ new Map();
398
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
760
+ function forwardedHeaders(req, cookieName, jwt) {
761
+ const headers = new Headers(req.headers);
762
+ headers.delete("x-oauth42-refreshed-token");
763
+ if (cookieName && jwt) {
764
+ const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
765
+ if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
766
+ else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
767
+ cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
768
+ }
769
+ headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
770
+ }
771
+ return headers;
772
+ }
773
+ function resolveRefreshConfig(options, token) {
774
+ return {
775
+ clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
776
+ clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
777
+ issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
778
+ };
779
+ }
780
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
399
781
  try {
400
782
  const tokenUrl = `${issuer}/oauth2/token`;
401
- const response = await fetch(tokenUrl, {
783
+ const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
784
+ const response = await fetcher(tokenUrl, {
402
785
  method: "POST",
786
+ redirect: "error",
787
+ cache: "no-store",
788
+ signal: AbortSignal.timeout(3e4),
403
789
  headers: {
404
790
  "Content-Type": "application/x-www-form-urlencoded"
405
791
  },
@@ -412,9 +798,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
412
798
  });
413
799
  const data = await response.json();
414
800
  if (!response.ok) {
415
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
801
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
416
802
  return { success: false, error: data.error || "refresh_failed" };
417
803
  }
804
+ dpop?.assertTokenResponse(data);
418
805
  console.log("[OAuth42 Middleware] Token refreshed successfully");
419
806
  return {
420
807
  success: true,
@@ -423,17 +810,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
423
810
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
424
811
  };
425
812
  } catch (error) {
426
- console.error("[OAuth42 Middleware] Token refresh error:", error);
813
+ console.error("[OAuth42 Middleware] Token refresh request failed");
427
814
  return { success: false, error: "refresh_error" };
428
815
  }
429
816
  }
430
817
  function withOAuth42Auth(options = {}) {
431
818
  const secret = process.env.NEXTAUTH_SECRET;
432
- const clientId = process.env.OAUTH42_CLIENT_ID;
433
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
434
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
435
819
  if (!secret) {
436
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
820
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
437
821
  }
438
822
  return async function middleware(req) {
439
823
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -444,18 +828,76 @@ function withOAuth42Auth(options = {}) {
444
828
  });
445
829
  const pathname = req.nextUrl.pathname;
446
830
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
447
- return import_server.NextResponse.next();
831
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
448
832
  }
449
833
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
450
834
  if (!needsProtection) {
451
- return import_server.NextResponse.next();
835
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
452
836
  }
453
- if (!token) {
837
+ if (!token || token.error) {
454
838
  const signInUrl = options.pages?.signIn || "/auth/signin";
455
839
  const url = new URL(signInUrl, req.url);
456
840
  url.searchParams.set("callbackUrl", pathname);
457
841
  return import_server.NextResponse.redirect(url);
458
842
  }
843
+ const refreshConfig = resolveRefreshConfig(options, token);
844
+ const denied = () => {
845
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
846
+ url.searchParams.set("callbackUrl", pathname);
847
+ const response = import_server.NextResponse.redirect(url);
848
+ for (const cookie of req.cookies.getAll()) {
849
+ if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
850
+ }
851
+ return response;
852
+ };
853
+ const verifyAndAuthorize = async (current, resolved) => {
854
+ const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
855
+ if (!user) return false;
856
+ current.groups = user.groups ?? [];
857
+ current.email = user.email;
858
+ current.name = user.name;
859
+ current.username = user.preferred_username ?? user.username;
860
+ current.emailVerified = user.email_verified;
861
+ return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
862
+ };
863
+ if (isBackendSessionMode(options.sessionStore)) {
864
+ const sessionId = token.oauth42SessionId;
865
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
866
+ const signInUrl = options.pages?.signIn || "/auth/signin";
867
+ const url = new URL(signInUrl, req.url);
868
+ url.searchParams.set("callbackUrl", pathname);
869
+ const response = import_server.NextResponse.redirect(url);
870
+ response.cookies.delete(cookieName);
871
+ return response;
872
+ }
873
+ const backendSession = await resolveBackendSession({
874
+ dpop: options.dpop,
875
+ clientAssertionKey: options.clientAssertionKey,
876
+ issuer: refreshConfig.issuer,
877
+ clientId: refreshConfig.clientId,
878
+ clientSecret: refreshConfig.clientSecret || "",
879
+ sessionId
880
+ }).catch((error) => {
881
+ console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
882
+ return null;
883
+ });
884
+ if (!backendSession) {
885
+ const signInUrl = options.pages?.signIn || "/auth/signin";
886
+ const url = new URL(signInUrl, req.url);
887
+ url.searchParams.set("callbackUrl", pathname);
888
+ const response = import_server.NextResponse.redirect(url);
889
+ response.cookies.delete(cookieName);
890
+ return response;
891
+ }
892
+ const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
893
+ if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
894
+ const requestHeaders = forwardedHeaders(req);
895
+ return import_server.NextResponse.next({
896
+ request: {
897
+ headers: requestHeaders
898
+ }
899
+ });
900
+ }
459
901
  const expiresAt = token.expiresAt;
460
902
  const now = Math.floor(Date.now() / 1e3);
461
903
  const bufferSeconds = 15;
@@ -465,26 +907,30 @@ function withOAuth42Auth(options = {}) {
465
907
  expiresAt,
466
908
  now,
467
909
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
468
- accessTokenPrefix: token.accessToken?.substring(0, 20),
469
910
  path: pathname
470
911
  });
471
912
  }
472
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
473
- const userId = token.sub;
474
- let refreshPromise = pendingRefreshes.get(userId);
913
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
914
+ const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
915
+ let refreshPromise = pendingRefreshes.get(grantKey);
475
916
  if (refreshPromise) {
476
917
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
477
918
  } else {
478
919
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
479
920
  refreshPromise = refreshTokens(
480
921
  token.refreshToken,
481
- clientId,
482
- clientSecret,
483
- issuer
922
+ refreshConfig.clientId,
923
+ refreshConfig.clientSecret || "",
924
+ refreshConfig.issuer,
925
+ options.dpop,
926
+ options.clientAssertionKey
484
927
  ).finally(() => {
485
- setTimeout(() => pendingRefreshes.delete(userId), 3e4);
928
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
929
+ if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
930
+ cleanupTimer.unref();
931
+ }
486
932
  });
487
- pendingRefreshes.set(userId, refreshPromise);
933
+ pendingRefreshes.set(grantKey, refreshPromise);
488
934
  }
489
935
  const refreshed = await refreshPromise;
490
936
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -494,12 +940,13 @@ function withOAuth42Auth(options = {}) {
494
940
  refreshToken: refreshed.refreshToken,
495
941
  expiresAt: refreshed.expiresAt
496
942
  };
943
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
944
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
497
945
  const newJwt = await (0, import_jwt.encode)({
498
946
  token: updatedToken,
499
947
  secret
500
948
  });
501
- const requestHeaders = new Headers(req.headers);
502
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
949
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
503
950
  const response = import_server.NextResponse.next({
504
951
  request: {
505
952
  headers: requestHeaders
@@ -509,7 +956,8 @@ function withOAuth42Auth(options = {}) {
509
956
  httpOnly: true,
510
957
  sameSite: "lax",
511
958
  path: "/",
512
- secure: process.env.NODE_ENV === "production"
959
+ secure: process.env.NODE_ENV === "production",
960
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
513
961
  };
514
962
  if (newJwt.length <= CHUNK_SIZE) {
515
963
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -551,13 +999,12 @@ function withOAuth42Auth(options = {}) {
551
999
  chunked: newJwt.length > CHUNK_SIZE,
552
1000
  existingBaseCooke: req.cookies.has(cookieName),
553
1001
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
554
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
555
1002
  newExpiresAt: refreshed.expiresAt,
556
1003
  cookieName
557
1004
  });
558
1005
  return response;
559
1006
  } else {
560
- console.log("[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), clearing session");
1007
+ console.log("[OAuth42 Middleware] Refresh failed, clearing session");
561
1008
  const signInUrl = options.pages?.signIn || "/auth/signin";
562
1009
  const url = new URL(signInUrl, req.url);
563
1010
  url.searchParams.set("callbackUrl", pathname);
@@ -574,16 +1021,13 @@ function withOAuth42Auth(options = {}) {
574
1021
  return response;
575
1022
  }
576
1023
  }
577
- if (options.callbacks?.authorized) {
578
- const isAuthorized = await options.callbacks.authorized({ token, req });
579
- if (!isAuthorized) {
580
- const signInUrl = options.pages?.signIn || "/auth/signin";
581
- const url = new URL(signInUrl, req.url);
582
- url.searchParams.set("callbackUrl", pathname);
583
- return import_server.NextResponse.redirect(url);
584
- }
1024
+ if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
1025
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
1026
+ url.searchParams.set("callbackUrl", pathname);
1027
+ return import_server.NextResponse.redirect(url);
585
1028
  }
586
- return import_server.NextResponse.next();
1029
+ if (!await verifyAndAuthorize(token)) return denied();
1030
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
587
1031
  };
588
1032
  }
589
1033
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -605,7 +1049,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
605
1049
 
606
1050
  // src/server/hosted-auth-callback.ts
607
1051
  var import_server2 = require("next/server");
608
- var import_https = __toESM(require("https"));
1052
+ var import_crypto = require("crypto");
609
1053
  var import_jwt2 = require("next-auth/jwt");
610
1054
  function createHostedAuthCallback(options = {}) {
611
1055
  return async function GET(request) {
@@ -618,7 +1062,7 @@ function createHostedAuthCallback(options = {}) {
618
1062
  redirectUrl = "/dashboard",
619
1063
  errorUrl = "/"
620
1064
  } = options;
621
- if (!clientId || !clientSecret) {
1065
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
622
1066
  console.error("OAuth42 client credentials are required");
623
1067
  return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
624
1068
  }
@@ -628,6 +1072,14 @@ function createHostedAuthCallback(options = {}) {
628
1072
  }
629
1073
  const searchParams = request.nextUrl.searchParams;
630
1074
  const code = searchParams.get("code");
1075
+ const cookiePrefix = process.env.NODE_ENV === "production" ? "__Host-" : request.nextUrl.protocol === "https:" ? "__Host-" : "";
1076
+ const expectedState = request.cookies.get(`${cookiePrefix}oauth42-state`)?.value;
1077
+ const returnedState = searchParams.get("state");
1078
+ const verifier = request.cookies.get(`${cookiePrefix}oauth42-pkce`)?.value;
1079
+ const validState = expectedState && returnedState && expectedState.length >= 32 && Buffer.byteLength(expectedState) === Buffer.byteLength(returnedState) && (0, import_crypto.timingSafeEqual)(Buffer.from(expectedState), Buffer.from(returnedState));
1080
+ if (!validState || !verifier || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
1081
+ return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=invalid_state`, request.url));
1082
+ }
631
1083
  if (!code) {
632
1084
  return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
633
1085
  }
@@ -635,39 +1087,38 @@ function createHostedAuthCallback(options = {}) {
635
1087
  const tokenEndpoint = `${issuer}/oauth2/token`;
636
1088
  const body = new URLSearchParams({
637
1089
  grant_type: "authorization_code",
1090
+ code_verifier: verifier,
638
1091
  code,
639
1092
  redirect_uri: `${baseUrl}/api/oauth/callback`,
640
1093
  client_id: clientId,
641
- client_secret: clientSecret
1094
+ client_secret: clientSecret || ""
642
1095
  });
643
1096
  const fetchOptions = {
644
1097
  method: "POST",
1098
+ redirect: "error",
1099
+ cache: "no-store",
1100
+ signal: AbortSignal.timeout(3e4),
645
1101
  headers: {
646
1102
  "Content-Type": "application/x-www-form-urlencoded"
647
1103
  },
648
1104
  body: body.toString()
649
1105
  };
650
- if (process.env.NODE_ENV !== "production") {
651
- const agent = new import_https.default.Agent({
652
- rejectUnauthorized: false
653
- });
654
- fetchOptions.agent = agent;
655
- }
656
- const tokenResponse = await fetch(tokenEndpoint, fetchOptions);
1106
+ const tokenResponse = options.clientAssertionKey ? await options.clientAssertionKey.fetch(clientId, tokenEndpoint, fetchOptions, options.dpop) : options.dpop ? await options.dpop.fetch(tokenEndpoint, fetchOptions) : await fetch(tokenEndpoint, fetchOptions);
657
1107
  const tokens = await tokenResponse.json();
658
1108
  if (!tokenResponse.ok) {
659
- console.error("Token exchange failed:", tokens);
1109
+ console.error("Token exchange failed:", tokenResponse.status);
660
1110
  return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
661
1111
  }
1112
+ options.dpop?.assertTokenResponse(tokens);
662
1113
  const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
663
- const userInfoResponse = await fetch(userInfoEndpoint, {
664
- headers: {
665
- "Authorization": `Bearer ${tokens.access_token}`
666
- },
667
- ...process.env.NODE_ENV !== "production" ? {
668
- agent: new import_https.default.Agent({ rejectUnauthorized: false })
669
- } : {}
670
- });
1114
+ const infoOptions = {
1115
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
1116
+ redirect: "error",
1117
+ cache: "no-store",
1118
+ signal: AbortSignal.timeout(3e4)
1119
+ };
1120
+ const userInfoResponse = options.dpop ? await options.dpop.fetch(userInfoEndpoint, infoOptions, tokens.access_token) : await fetch(userInfoEndpoint, infoOptions);
1121
+ if (!userInfoResponse.ok) throw new Error("User info request failed");
671
1122
  const userInfo = await userInfoResponse.json();
672
1123
  const sessionToken = await (0, import_jwt2.encode)({
673
1124
  token: {
@@ -692,6 +1143,14 @@ function createHostedAuthCallback(options = {}) {
692
1143
  // 30 days
693
1144
  path: "/"
694
1145
  });
1146
+ for (const name of ["oauth42-state", "oauth42-pkce"]) {
1147
+ redirectResponse.cookies.set(`${cookiePrefix}${name}`, "", {
1148
+ path: "/",
1149
+ secure: cookiePrefix === "__Host-",
1150
+ sameSite: "lax",
1151
+ maxAge: 0
1152
+ });
1153
+ }
695
1154
  return redirectResponse;
696
1155
  } catch (error) {
697
1156
  console.error("OAuth callback error:", error);
@@ -699,19 +1158,94 @@ function createHostedAuthCallback(options = {}) {
699
1158
  }
700
1159
  };
701
1160
  }
1161
+
1162
+ // src/server/dpop.ts
1163
+ var import_node_crypto2 = require("crypto");
1164
+ var OAuth42DPoP = class _OAuth42DPoP {
1165
+ constructor(key) {
1166
+ this.nonces = /* @__PURE__ */ new Map();
1167
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
1168
+ throw new Error("DPoP requires a private P-256 key");
1169
+ }
1170
+ this.key = key;
1171
+ const publicKey = (0, import_node_crypto2.createPublicKey)(key).export({ format: "jwk" });
1172
+ this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
1173
+ this.thumbprint = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
1174
+ }
1175
+ /** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
1176
+ static generate() {
1177
+ return new _OAuth42DPoP((0, import_node_crypto2.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
1178
+ }
1179
+ /** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
1180
+ get oidcKey() {
1181
+ return this.key;
1182
+ }
1183
+ proof(method, target, nonce, accessToken) {
1184
+ const url = new URL(target);
1185
+ if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
1186
+ throw new Error("DPoP requires HTTPS outside loopback development");
1187
+ }
1188
+ if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
1189
+ url.search = "";
1190
+ url.hash = "";
1191
+ const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
1192
+ const claims = {
1193
+ htm: method.toUpperCase(),
1194
+ htu: url.href,
1195
+ iat: Math.floor(Date.now() / 1e3),
1196
+ jti: (0, import_node_crypto2.randomBytes)(32).toString("base64url"),
1197
+ ...nonce ? { nonce } : {},
1198
+ ...accessToken !== void 0 ? { ath: (0, import_node_crypto2.createHash)("sha256").update(accessToken).digest("base64url") } : {}
1199
+ };
1200
+ const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
1201
+ return `${input}.${(0, import_node_crypto2.sign)("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
1202
+ }
1203
+ assertTokenResponse(tokens) {
1204
+ if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
1205
+ throw new Error("Server did not issue the requested DPoP-bound token");
1206
+ }
1207
+ }
1208
+ async fetch(target, init = {}, accessToken, prepare) {
1209
+ if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
1210
+ const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
1211
+ for (let attempt = 0; attempt < 2; attempt++) {
1212
+ const request = prepare ? prepare() : init;
1213
+ const headers = new Headers(request.headers);
1214
+ headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
1215
+ if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
1216
+ const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
1217
+ const nonce = response.headers.get("DPoP-Nonce");
1218
+ if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
1219
+ if (this.nonces.size >= 256) this.nonces.clear();
1220
+ this.nonces.set(cacheKey, nonce);
1221
+ } else return response;
1222
+ if (attempt || ![400, 401].includes(response.status)) return response;
1223
+ const error = await response.clone().json().catch(() => null);
1224
+ if (error?.error !== "use_dpop_nonce") return response;
1225
+ }
1226
+ throw new Error("DPoP nonce negotiation failed");
1227
+ }
1228
+ };
702
1229
  // Annotate the CommonJS export names for ESM import in node:
703
1230
  0 && (module.exports = {
704
1231
  NextAuth,
1232
+ OAuth42ClientAssertion,
1233
+ OAuth42DPoP,
705
1234
  OAuth42Provider,
706
1235
  createAuth,
1236
+ createBackendSession,
707
1237
  createHandlers,
708
1238
  createHostedAuthCallback,
709
1239
  createMiddlewareConfig,
710
1240
  getOAuth42Session,
711
1241
  getServerSession,
1242
+ pushAuthorizationURL,
712
1243
  refreshAccessToken,
1244
+ resolveBackendSession,
1245
+ revokeBackendSession,
713
1246
  withOAuth42Auth,
714
1247
  withOAuth42ServerSideProps,
715
- withOAuth42Session
1248
+ withOAuth42Session,
1249
+ withPushedAuthorization
716
1250
  });
717
1251
  //# sourceMappingURL=index.js.map