@oauth42/next 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,11 +29,17 @@ 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,
36
45
  createBackendSession: () => createBackendSession,
@@ -39,15 +48,196 @@ __export(server_exports, {
39
48
  createMiddlewareConfig: () => createMiddlewareConfig,
40
49
  getOAuth42Session: () => getOAuth42Session,
41
50
  getServerSession: () => getServerSession,
51
+ pushAuthorizationURL: () => pushAuthorizationURL,
42
52
  refreshAccessToken: () => refreshAccessToken,
43
53
  resolveBackendSession: () => resolveBackendSession,
44
54
  revokeBackendSession: () => revokeBackendSession,
45
55
  withOAuth42Auth: () => withOAuth42Auth,
46
56
  withOAuth42ServerSideProps: () => withOAuth42ServerSideProps,
47
- withOAuth42Session: () => withOAuth42Session
57
+ withOAuth42Session: () => withOAuth42Session,
58
+ withPushedAuthorization: () => withPushedAuthorization
48
59
  });
49
60
  module.exports = __toCommonJS(server_exports);
50
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
+
51
241
  // src/server/auth.ts
52
242
  var import_next_auth2 = __toESM(require("next-auth"));
53
243
 
@@ -55,13 +245,20 @@ var import_next_auth2 = __toESM(require("next-auth"));
55
245
  function OAuth42Provider(options) {
56
246
  const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
57
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");
58
254
  return {
59
255
  id: "oauth42",
60
256
  name: "OAuth42",
61
257
  type: "oauth",
62
258
  version: "2.0",
63
- // Use OIDC discovery to automatically find endpoints
64
- 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`,
65
262
  // Use ID token for user profile (standard OIDC pattern)
66
263
  // This avoids an extra HTTP call to /userinfo on every auth flow
67
264
  // Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
@@ -74,17 +271,26 @@ function OAuth42Provider(options) {
74
271
  response_type: "code"
75
272
  }
76
273
  },
77
- 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`,
78
282
  userinfo: `${baseUrl}/oauth2/userinfo`,
283
+ ...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
79
284
  client: {
80
285
  id: options.clientId,
81
- secret: options.clientSecret,
82
- 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" } : {},
83
289
  id_token_signed_response_alg: "RS256"
84
290
  // OAuth42 uses RS256 for ID tokens
85
291
  },
86
- issuer: baseUrl,
87
- checks: options.pkceEnabled !== false ? ["pkce", "state"] : ["state"],
292
+ issuer,
293
+ checks: ["pkce", "state", "nonce"],
88
294
  profile(profile, tokens) {
89
295
  return {
90
296
  id: profile.sub || profile.id || profile.email,
@@ -100,42 +306,18 @@ function OAuth42Provider(options) {
100
306
  bg: "#1e40af",
101
307
  text: "#ffffff"
102
308
  },
103
- options
309
+ // NextAuth v4 types require a string even when private_key_jwt sends no secret.
310
+ options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
104
311
  };
105
312
  }
106
313
 
107
314
  // src/server/session.ts
108
315
  var import_next_auth = require("next-auth");
109
- var import_headers = require("next/headers");
110
- var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
111
316
  async function getOAuth42Session(...args) {
112
317
  const session = await (0, import_next_auth.getServerSession)(...args);
113
- if (!session) {
318
+ if (!session || session.error) {
114
319
  return null;
115
320
  }
116
- let refreshedToken = null;
117
- try {
118
- if (args.length === 1) {
119
- const headersList = await (0, import_headers.headers)();
120
- refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
121
- } else if (args.length === 3) {
122
- const req = args[0];
123
- refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
124
- }
125
- } catch {
126
- }
127
- if (refreshedToken) {
128
- const sessionToken = session.accessToken;
129
- console.log("[OAuth42 Session] Using refreshed token from middleware:", {
130
- refreshedPrefix: refreshedToken.substring(0, 20),
131
- sessionPrefix: sessionToken?.substring(0, 20),
132
- tokensMatch: refreshedToken === sessionToken
133
- });
134
- return {
135
- ...session,
136
- accessToken: refreshedToken
137
- };
138
- }
139
321
  return session;
140
322
  }
141
323
  function withOAuth42Session(handler, authOptions) {
@@ -173,66 +355,59 @@ function isBackendSessionMode(mode) {
173
355
  function sessionEndpoint(issuer) {
174
356
  return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
175
357
  }
176
- async function createBackendSession(params) {
177
- const response = await fetch(sessionEndpoint(params.issuer), {
178
- method: "POST",
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),
179
365
  headers: { "Content-Type": "application/json" },
180
366
  body: JSON.stringify({
181
- access_token: params.accessToken,
182
- refresh_token: params.refreshToken,
183
- expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
184
- id_token: params.idToken,
367
+ ...body,
185
368
  client_id: params.clientId,
186
- client_secret: params.clientSecret,
187
- issuer: params.issuer,
188
- user: params.user
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 }
189
373
  })
190
374
  });
191
- if (!response.ok) {
192
- throw new Error(`OAuth42 backend session create failed: ${response.status}`);
193
- }
194
- return response.json();
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);
195
391
  }
196
392
  async function resolveBackendSession(params) {
197
- const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
198
- method: "POST",
199
- headers: { "Content-Type": "application/json" },
200
- body: JSON.stringify({
201
- session_id: params.sessionId,
202
- client_id: params.clientId,
203
- client_secret: params.clientSecret,
204
- issuer: params.issuer
205
- })
206
- });
207
- if (response.status === 401 || response.status === 403 || response.status === 404) {
208
- return null;
209
- }
210
- if (!response.ok) {
211
- throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
212
- }
213
- return response.json();
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);
214
396
  }
215
397
  async function revokeBackendSession(params) {
216
- await fetch(sessionEndpoint(params.issuer), {
217
- method: "DELETE",
218
- headers: { "Content-Type": "application/json" },
219
- body: JSON.stringify({
220
- session_id: params.sessionId,
221
- client_id: params.clientId,
222
- client_secret: params.clientSecret
223
- })
224
- });
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}`);
225
400
  }
226
401
 
227
402
  // src/server/auth.ts
228
403
  var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
229
- var activeRefresh = null;
404
+ var activeRefreshes = /* @__PURE__ */ new Map();
230
405
  function createAuth(options = {}) {
231
406
  const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
232
- const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
407
+ const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
233
408
  const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
234
409
  const useBackendSessions = isBackendSessionMode(options.sessionStore);
235
- if (!clientId || !clientSecret) {
410
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
236
411
  throw new Error(
237
412
  "OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
238
413
  );
@@ -241,14 +416,17 @@ function createAuth(options = {}) {
241
416
  providers: [
242
417
  OAuth42Provider({
243
418
  clientId,
244
- clientSecret,
419
+ clientSecret: clientSecret || "",
245
420
  issuer,
246
421
  scopes: options.scopes,
247
- pkceEnabled: options.pkceEnabled
422
+ pkceEnabled: options.pkceEnabled,
423
+ dpop: options.dpop,
424
+ clientAssertionKey: options.clientAssertionKey
248
425
  })
249
426
  ],
250
427
  callbacks: {
251
428
  async jwt({ token, account, profile }) {
429
+ if (options.clientAssertionKey) delete token.clientSecret;
252
430
  console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
253
431
  if (account) {
254
432
  if (useBackendSessions) {
@@ -257,9 +435,11 @@ function createAuth(options = {}) {
257
435
  }
258
436
  const oauth42Profile = profile;
259
437
  const backendSession = await createBackendSession({
438
+ dpop: options.dpop,
439
+ clientAssertionKey: options.clientAssertionKey,
260
440
  issuer,
261
441
  clientId,
262
- clientSecret,
442
+ clientSecret: clientSecret || "",
263
443
  accessToken: account.access_token,
264
444
  refreshToken: account.refresh_token,
265
445
  expiresAt: account.expires_at,
@@ -283,7 +463,8 @@ function createAuth(options = {}) {
283
463
  token.expiresAt = account.expires_at;
284
464
  token.idToken = account.id_token;
285
465
  token.clientId = clientId;
286
- token.clientSecret = clientSecret;
466
+ if (options.clientAssertionKey) delete token.clientSecret;
467
+ else token.clientSecret = clientSecret;
287
468
  token.issuer = issuer;
288
469
  }
289
470
  }
@@ -302,25 +483,36 @@ function createAuth(options = {}) {
302
483
  },
303
484
  async session({ session, token }) {
304
485
  console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
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;
305
496
  if (useBackendSessions) {
306
497
  const sessionId = token.oauth42SessionId;
307
498
  if (!sessionId) {
308
- session.error = "SessionRevoked";
309
- return session;
499
+ return revokedSession();
310
500
  }
311
501
  const backendSession = await resolveBackendSession({
502
+ dpop: options.dpop,
503
+ clientAssertionKey: options.clientAssertionKey,
312
504
  issuer,
313
505
  clientId,
314
- clientSecret,
506
+ clientSecret: clientSecret || "",
315
507
  sessionId
316
508
  });
317
509
  if (!backendSession) {
318
- session.error = "SessionRevoked";
319
- return session;
510
+ return revokedSession();
320
511
  }
321
512
  session.accessToken = backendSession.access_token;
322
513
  session.idToken = backendSession.id_token;
323
514
  token.expiresAt = backendSession.expires_at;
515
+ verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
324
516
  if (backendSession.user && session.user) {
325
517
  session.user.email = backendSession.user.email;
326
518
  session.user.name = backendSession.user.name;
@@ -331,6 +523,23 @@ function createAuth(options = {}) {
331
523
  } else {
332
524
  session.accessToken = token.accessToken;
333
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 ?? [];
334
543
  }
335
544
  if (token.error) {
336
545
  session.error = token.error;
@@ -365,9 +574,11 @@ function createAuth(options = {}) {
365
574
  const sessionId = message.token?.oauth42SessionId;
366
575
  if (sessionId) {
367
576
  await revokeBackendSession({
577
+ dpop: options.dpop,
578
+ clientAssertionKey: options.clientAssertionKey,
368
579
  issuer,
369
580
  clientId,
370
- clientSecret,
581
+ clientSecret: clientSecret || "",
371
582
  sessionId
372
583
  }).catch((error) => {
373
584
  console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
@@ -454,7 +665,8 @@ function createAuth(options = {}) {
454
665
  }
455
666
  }
456
667
  };
457
- 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;
458
670
  return {
459
671
  auth: authOptions,
460
672
  handlers: { GET: handler, POST: handler }
@@ -465,24 +677,40 @@ function createHandlers(authOptions) {
465
677
  return { GET: handler, POST: handler };
466
678
  }
467
679
  var getServerSession = getOAuth42Session;
468
- async function refreshAccessToken(token, clientId, clientSecret, issuer) {
469
- if (activeRefresh) {
470
- console.log("[OAuth42] Refresh already in progress, waiting...");
471
- 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);
472
689
  }
473
- activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
474
690
  try {
475
- 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;
476
701
  } finally {
477
- activeRefresh = null;
702
+ if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
478
703
  }
479
704
  }
480
- async function doRefresh(token, clientId, clientSecret, issuer) {
705
+ async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
481
706
  try {
482
707
  const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
483
708
  const tokenUrl = `${baseUrl}/oauth2/token`;
484
709
  const fetchOptions = {
485
710
  method: "POST",
711
+ redirect: "error",
712
+ cache: "no-store",
713
+ signal: AbortSignal.timeout(3e4),
486
714
  headers: {
487
715
  "Content-Type": "application/x-www-form-urlencoded"
488
716
  },
@@ -493,17 +721,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
493
721
  client_secret: clientSecret
494
722
  })
495
723
  };
496
- if (process.env.NODE_ENV !== "production" && tokenUrl.startsWith("https://")) {
497
- const https2 = await import("https");
498
- fetchOptions.agent = new https2.Agent({
499
- rejectUnauthorized: false
500
- });
501
- }
502
- 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);
503
725
  const refreshedTokens = await response.json();
504
726
  if (!response.ok) {
505
727
  throw refreshedTokens;
506
728
  }
729
+ dpop?.assertTokenResponse(refreshedTokens);
507
730
  console.log("[OAuth42] Token refreshed successfully");
508
731
  return {
509
732
  ...token,
@@ -515,7 +738,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
515
738
  error: void 0
516
739
  };
517
740
  } catch (error) {
518
- console.error("[OAuth42] Failed to refresh access token:", error);
741
+ console.error("[OAuth42] Failed to refresh access token");
519
742
  return {
520
743
  ...token,
521
744
  error: "RefreshAccessTokenError"
@@ -532,14 +755,20 @@ var import_jwt = require("next-auth/jwt");
532
755
  var ALLOWED_COOKIE_SIZE = 4096;
533
756
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
534
757
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
535
- var REFRESH_RACE_GRACE_SECONDS = 60;
536
758
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
537
759
  var pendingRefreshes = /* @__PURE__ */ new Map();
538
- function shouldPreserveSessionOnRefreshFailure(params) {
539
- if (params.error !== "invalid_grant" || !params.expiresAt) {
540
- return false;
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("; "));
541
770
  }
542
- return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
771
+ return headers;
543
772
  }
544
773
  function resolveRefreshConfig(options, token) {
545
774
  return {
@@ -548,11 +777,15 @@ function resolveRefreshConfig(options, token) {
548
777
  issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
549
778
  };
550
779
  }
551
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
780
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
552
781
  try {
553
782
  const tokenUrl = `${issuer}/oauth2/token`;
554
- 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, {
555
785
  method: "POST",
786
+ redirect: "error",
787
+ cache: "no-store",
788
+ signal: AbortSignal.timeout(3e4),
556
789
  headers: {
557
790
  "Content-Type": "application/x-www-form-urlencoded"
558
791
  },
@@ -565,9 +798,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
565
798
  });
566
799
  const data = await response.json();
567
800
  if (!response.ok) {
568
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
801
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
569
802
  return { success: false, error: data.error || "refresh_failed" };
570
803
  }
804
+ dpop?.assertTokenResponse(data);
571
805
  console.log("[OAuth42 Middleware] Token refreshed successfully");
572
806
  return {
573
807
  success: true,
@@ -576,14 +810,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
576
810
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
577
811
  };
578
812
  } catch (error) {
579
- console.error("[OAuth42 Middleware] Token refresh error:", error);
813
+ console.error("[OAuth42 Middleware] Token refresh request failed");
580
814
  return { success: false, error: "refresh_error" };
581
815
  }
582
816
  }
583
817
  function withOAuth42Auth(options = {}) {
584
818
  const secret = process.env.NEXTAUTH_SECRET;
585
819
  if (!secret) {
586
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
820
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
587
821
  }
588
822
  return async function middleware(req) {
589
823
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -594,22 +828,41 @@ function withOAuth42Auth(options = {}) {
594
828
  });
595
829
  const pathname = req.nextUrl.pathname;
596
830
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
597
- return import_server.NextResponse.next();
831
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
598
832
  }
599
833
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
600
834
  if (!needsProtection) {
601
- return import_server.NextResponse.next();
835
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
602
836
  }
603
- if (!token) {
837
+ if (!token || token.error) {
604
838
  const signInUrl = options.pages?.signIn || "/auth/signin";
605
839
  const url = new URL(signInUrl, req.url);
606
840
  url.searchParams.set("callbackUrl", pathname);
607
841
  return import_server.NextResponse.redirect(url);
608
842
  }
609
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
+ };
610
863
  if (isBackendSessionMode(options.sessionStore)) {
611
864
  const sessionId = token.oauth42SessionId;
612
- if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
865
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
613
866
  const signInUrl = options.pages?.signIn || "/auth/signin";
614
867
  const url = new URL(signInUrl, req.url);
615
868
  url.searchParams.set("callbackUrl", pathname);
@@ -618,9 +871,11 @@ function withOAuth42Auth(options = {}) {
618
871
  return response;
619
872
  }
620
873
  const backendSession = await resolveBackendSession({
874
+ dpop: options.dpop,
875
+ clientAssertionKey: options.clientAssertionKey,
621
876
  issuer: refreshConfig.issuer,
622
877
  clientId: refreshConfig.clientId,
623
- clientSecret: refreshConfig.clientSecret,
878
+ clientSecret: refreshConfig.clientSecret || "",
624
879
  sessionId
625
880
  }).catch((error) => {
626
881
  console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
@@ -634,8 +889,9 @@ function withOAuth42Auth(options = {}) {
634
889
  response.cookies.delete(cookieName);
635
890
  return response;
636
891
  }
637
- const requestHeaders = new Headers(req.headers);
638
- requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
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);
639
895
  return import_server.NextResponse.next({
640
896
  request: {
641
897
  headers: requestHeaders
@@ -651,13 +907,12 @@ function withOAuth42Auth(options = {}) {
651
907
  expiresAt,
652
908
  now,
653
909
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
654
- accessTokenPrefix: token.accessToken?.substring(0, 20),
655
910
  path: pathname
656
911
  });
657
912
  }
658
- if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
659
- const userId = token.sub;
660
- 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);
661
916
  if (refreshPromise) {
662
917
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
663
918
  } else {
@@ -665,15 +920,17 @@ function withOAuth42Auth(options = {}) {
665
920
  refreshPromise = refreshTokens(
666
921
  token.refreshToken,
667
922
  refreshConfig.clientId,
668
- refreshConfig.clientSecret,
669
- refreshConfig.issuer
923
+ refreshConfig.clientSecret || "",
924
+ refreshConfig.issuer,
925
+ options.dpop,
926
+ options.clientAssertionKey
670
927
  ).finally(() => {
671
- const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
928
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
672
929
  if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
673
930
  cleanupTimer.unref();
674
931
  }
675
932
  });
676
- pendingRefreshes.set(userId, refreshPromise);
933
+ pendingRefreshes.set(grantKey, refreshPromise);
677
934
  }
678
935
  const refreshed = await refreshPromise;
679
936
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -683,12 +940,13 @@ function withOAuth42Auth(options = {}) {
683
940
  refreshToken: refreshed.refreshToken,
684
941
  expiresAt: refreshed.expiresAt
685
942
  };
943
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
944
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
686
945
  const newJwt = await (0, import_jwt.encode)({
687
946
  token: updatedToken,
688
947
  secret
689
948
  });
690
- const requestHeaders = new Headers(req.headers);
691
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
949
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
692
950
  const response = import_server.NextResponse.next({
693
951
  request: {
694
952
  headers: requestHeaders
@@ -741,28 +999,11 @@ function withOAuth42Auth(options = {}) {
741
999
  chunked: newJwt.length > CHUNK_SIZE,
742
1000
  existingBaseCooke: req.cookies.has(cookieName),
743
1001
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
744
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
745
1002
  newExpiresAt: refreshed.expiresAt,
746
1003
  cookieName
747
1004
  });
748
1005
  return response;
749
1006
  } else {
750
- if (shouldPreserveSessionOnRefreshFailure({
751
- error: refreshed.error,
752
- expiresAt,
753
- now
754
- })) {
755
- console.warn(
756
- "[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
757
- {
758
- expiresAt,
759
- now,
760
- expiredAgo: now - expiresAt,
761
- path: pathname
762
- }
763
- );
764
- return import_server.NextResponse.next();
765
- }
766
1007
  console.log("[OAuth42 Middleware] Refresh failed, clearing session");
767
1008
  const signInUrl = options.pages?.signIn || "/auth/signin";
768
1009
  const url = new URL(signInUrl, req.url);
@@ -780,16 +1021,13 @@ function withOAuth42Auth(options = {}) {
780
1021
  return response;
781
1022
  }
782
1023
  }
783
- if (options.callbacks?.authorized) {
784
- const isAuthorized = await options.callbacks.authorized({ token, req });
785
- if (!isAuthorized) {
786
- const signInUrl = options.pages?.signIn || "/auth/signin";
787
- const url = new URL(signInUrl, req.url);
788
- url.searchParams.set("callbackUrl", pathname);
789
- return import_server.NextResponse.redirect(url);
790
- }
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);
791
1028
  }
792
- return import_server.NextResponse.next();
1029
+ if (!await verifyAndAuthorize(token)) return denied();
1030
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
793
1031
  };
794
1032
  }
795
1033
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -811,7 +1049,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
811
1049
 
812
1050
  // src/server/hosted-auth-callback.ts
813
1051
  var import_server2 = require("next/server");
814
- var import_https = __toESM(require("https"));
1052
+ var import_crypto = require("crypto");
815
1053
  var import_jwt2 = require("next-auth/jwt");
816
1054
  function createHostedAuthCallback(options = {}) {
817
1055
  return async function GET(request) {
@@ -824,7 +1062,7 @@ function createHostedAuthCallback(options = {}) {
824
1062
  redirectUrl = "/dashboard",
825
1063
  errorUrl = "/"
826
1064
  } = options;
827
- if (!clientId || !clientSecret) {
1065
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
828
1066
  console.error("OAuth42 client credentials are required");
829
1067
  return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
830
1068
  }
@@ -834,6 +1072,14 @@ function createHostedAuthCallback(options = {}) {
834
1072
  }
835
1073
  const searchParams = request.nextUrl.searchParams;
836
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
+ }
837
1083
  if (!code) {
838
1084
  return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
839
1085
  }
@@ -841,39 +1087,38 @@ function createHostedAuthCallback(options = {}) {
841
1087
  const tokenEndpoint = `${issuer}/oauth2/token`;
842
1088
  const body = new URLSearchParams({
843
1089
  grant_type: "authorization_code",
1090
+ code_verifier: verifier,
844
1091
  code,
845
1092
  redirect_uri: `${baseUrl}/api/oauth/callback`,
846
1093
  client_id: clientId,
847
- client_secret: clientSecret
1094
+ client_secret: clientSecret || ""
848
1095
  });
849
1096
  const fetchOptions = {
850
1097
  method: "POST",
1098
+ redirect: "error",
1099
+ cache: "no-store",
1100
+ signal: AbortSignal.timeout(3e4),
851
1101
  headers: {
852
1102
  "Content-Type": "application/x-www-form-urlencoded"
853
1103
  },
854
1104
  body: body.toString()
855
1105
  };
856
- if (process.env.NODE_ENV !== "production") {
857
- const agent = new import_https.default.Agent({
858
- rejectUnauthorized: false
859
- });
860
- fetchOptions.agent = agent;
861
- }
862
- 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);
863
1107
  const tokens = await tokenResponse.json();
864
1108
  if (!tokenResponse.ok) {
865
- console.error("Token exchange failed:", tokens);
1109
+ console.error("Token exchange failed:", tokenResponse.status);
866
1110
  return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
867
1111
  }
1112
+ options.dpop?.assertTokenResponse(tokens);
868
1113
  const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
869
- const userInfoResponse = await fetch(userInfoEndpoint, {
870
- headers: {
871
- "Authorization": `Bearer ${tokens.access_token}`
872
- },
873
- ...process.env.NODE_ENV !== "production" ? {
874
- agent: new import_https.default.Agent({ rejectUnauthorized: false })
875
- } : {}
876
- });
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");
877
1122
  const userInfo = await userInfoResponse.json();
878
1123
  const sessionToken = await (0, import_jwt2.encode)({
879
1124
  token: {
@@ -898,6 +1143,14 @@ function createHostedAuthCallback(options = {}) {
898
1143
  // 30 days
899
1144
  path: "/"
900
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
+ }
901
1154
  return redirectResponse;
902
1155
  } catch (error) {
903
1156
  console.error("OAuth callback error:", error);
@@ -905,9 +1158,79 @@ function createHostedAuthCallback(options = {}) {
905
1158
  }
906
1159
  };
907
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
+ };
908
1229
  // Annotate the CommonJS export names for ESM import in node:
909
1230
  0 && (module.exports = {
910
1231
  NextAuth,
1232
+ OAuth42ClientAssertion,
1233
+ OAuth42DPoP,
911
1234
  OAuth42Provider,
912
1235
  createAuth,
913
1236
  createBackendSession,
@@ -916,11 +1239,13 @@ function createHostedAuthCallback(options = {}) {
916
1239
  createMiddlewareConfig,
917
1240
  getOAuth42Session,
918
1241
  getServerSession,
1242
+ pushAuthorizationURL,
919
1243
  refreshAccessToken,
920
1244
  resolveBackendSession,
921
1245
  revokeBackendSession,
922
1246
  withOAuth42Auth,
923
1247
  withOAuth42ServerSideProps,
924
- withOAuth42Session
1248
+ withOAuth42Session,
1249
+ withPushedAuthorization
925
1250
  });
926
1251
  //# sourceMappingURL=index.js.map