@oauth42/next 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,190 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+
9
+ // src/server/client-assertion.ts
10
+ import { createHash, createPublicKey, generateKeyPairSync, randomBytes, sign } from "crypto";
11
+ var _key, _public;
12
+ var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
13
+ constructor(kid, key) {
14
+ __privateAdd(this, _key);
15
+ __privateAdd(this, _public);
16
+ if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
17
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
18
+ throw new Error("Client assertions require a private P-256 key");
19
+ }
20
+ __privateSet(this, _key, key);
21
+ this.kid = kid;
22
+ const publicKey = createPublicKey(key).export({ format: "jwk" });
23
+ __privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
24
+ this.thumbprint = createHash("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
25
+ }
26
+ static generate(kid) {
27
+ return new _OAuth42ClientAssertion(kid, generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey);
28
+ }
29
+ get publicJwk() {
30
+ return { ...__privateGet(this, _public), kid: this.kid };
31
+ }
32
+ /** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
33
+ get oidcJwks() {
34
+ return { keys: [{
35
+ kty: "EC",
36
+ crv: "P-256",
37
+ x: __privateGet(this, _public).x,
38
+ y: __privateGet(this, _public).y,
39
+ d: __privateGet(this, _key).export({ format: "jwk" }).d,
40
+ kid: this.kid
41
+ }] };
42
+ }
43
+ static validateEndpoint(target) {
44
+ const url = new URL(target);
45
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
46
+ throw new Error("Client authentication requires HTTPS outside loopback development");
47
+ }
48
+ }
49
+ assertion(clientId, audience) {
50
+ _OAuth42ClientAssertion.validateEndpoint(audience);
51
+ if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
52
+ const now = Math.floor(Date.now() / 1e3);
53
+ const header = { alg: "ES256", typ: "JWT", kid: this.kid };
54
+ const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: randomBytes(32).toString("base64url") };
55
+ const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
56
+ return `${data}.${sign("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
57
+ }
58
+ async fetch(clientId, target, init, dpop) {
59
+ if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
60
+ throw new Error("Client assertions require a form POST");
61
+ }
62
+ const prepare = () => {
63
+ const body = new URLSearchParams(init.body);
64
+ body.delete("client_secret");
65
+ body.set("client_id", clientId);
66
+ body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
67
+ body.set("client_assertion", this.assertion(clientId, target));
68
+ const headers = new Headers(init.headers);
69
+ headers.delete("Authorization");
70
+ headers.delete("Content-Length");
71
+ headers.delete("Transfer-Encoding");
72
+ headers.set("Content-Type", "application/x-www-form-urlencoded");
73
+ return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
74
+ };
75
+ return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
76
+ }
77
+ };
78
+ _key = new WeakMap();
79
+ _public = new WeakMap();
80
+ var OAuth42ClientAssertion = _OAuth42ClientAssertion;
81
+
82
+ // src/server/par.ts
83
+ async function pushAuthorizationURL(value, config) {
84
+ const base = config.issuer.replace(/\/$/, "");
85
+ let url;
86
+ try {
87
+ url = new URL(value);
88
+ } catch {
89
+ return value;
90
+ }
91
+ if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
92
+ if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
93
+ const body = new URLSearchParams(url.search);
94
+ 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))) {
95
+ throw new Error("Invalid PAR authorization transaction");
96
+ }
97
+ const target = `${base}/oauth2/par`;
98
+ OAuth42ClientAssertion.validateEndpoint(target);
99
+ if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
100
+ const init = {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
103
+ body,
104
+ redirect: "error",
105
+ cache: "no-store",
106
+ signal: AbortSignal.timeout(3e4)
107
+ };
108
+ 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);
109
+ if (response.status !== 201) throw new Error("Pushed authorization failed");
110
+ const pushed = await response.json();
111
+ 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");
112
+ url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
113
+ return url.href;
114
+ }
115
+ function withPushedAuthorization(handler, config) {
116
+ return async (request, context) => {
117
+ if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
118
+ const response = await handler(request, context);
119
+ const location = response.headers.get("Location");
120
+ if (location) {
121
+ const next = await pushAuthorizationURL(location, config);
122
+ if (next === location) return response;
123
+ const headers = new Headers(response.headers);
124
+ headers.set("Location", next);
125
+ headers.delete("content-length");
126
+ return new Response(response.body, { status: response.status, headers });
127
+ }
128
+ if (response.headers.get("content-type")?.includes("application/json")) {
129
+ const body = await response.clone().json();
130
+ if (typeof body?.url === "string") {
131
+ const next = await pushAuthorizationURL(body.url, config);
132
+ if (next !== body.url) {
133
+ const headers = new Headers(response.headers);
134
+ headers.delete("content-length");
135
+ return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
136
+ }
137
+ }
138
+ }
139
+ return response;
140
+ };
141
+ }
142
+
143
+ // src/server/verified-userinfo.ts
144
+ async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
145
+ if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
146
+ try {
147
+ const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
148
+ const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
149
+ const response = await fetcher(target, {
150
+ headers: { Authorization: `Bearer ${accessToken}` },
151
+ redirect: "error",
152
+ cache: "no-store",
153
+ signal: AbortSignal.timeout(3e4)
154
+ });
155
+ if (!response.ok) return null;
156
+ const user = await response.json();
157
+ return parseVerifiedUserInfo(user, subject);
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ function parseVerifiedUserInfo(value, subject) {
163
+ if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
164
+ const user = value;
165
+ if (user.sub !== subject) return null;
166
+ if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
167
+ if (user.email !== void 0 && typeof user.email !== "string") return null;
168
+ if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
169
+ const profile = { sub: user.sub };
170
+ for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
171
+ if (user[field] != null) {
172
+ if (typeof user[field] !== "string") return null;
173
+ profile[field] = user[field];
174
+ }
175
+ }
176
+ if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
177
+ if (user.groups !== void 0) profile.groups = user.groups;
178
+ return profile;
179
+ }
180
+ async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
181
+ if (typeof session.access_token !== "string" || !session.access_token) return null;
182
+ if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
183
+ return parseVerifiedUserInfo(session.user_info, subject);
184
+ }
185
+ return verifiedUserInfo(issuer, session.access_token, subject, dpop);
186
+ }
187
+
1
188
  // src/server/auth.ts
2
189
  import NextAuthDefault from "next-auth";
3
190
 
@@ -5,13 +192,20 @@ import NextAuthDefault from "next-auth";
5
192
  function OAuth42Provider(options) {
6
193
  const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
7
194
  const baseUrl = issuer.replace(/\/$/, "");
195
+ const { dpop, clientAssertionKey, ...providerOptions } = options;
196
+ if (clientAssertionKey) {
197
+ OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
198
+ providerOptions.clientSecret = void 0;
199
+ }
200
+ if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
8
201
  return {
9
202
  id: "oauth42",
10
203
  name: "OAuth42",
11
204
  type: "oauth",
12
205
  version: "2.0",
13
- // Use OIDC discovery to automatically find endpoints
14
- wellKnown: `${baseUrl}/.well-known/openid-configuration`,
206
+ // Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
207
+ // Constructing its OIDC client from these trusted endpoints preserves issuer binding.
208
+ jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
15
209
  // Use ID token for user profile (standard OIDC pattern)
16
210
  // This avoids an extra HTTP call to /userinfo on every auth flow
17
211
  // Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
@@ -24,17 +218,26 @@ function OAuth42Provider(options) {
24
218
  response_type: "code"
25
219
  }
26
220
  },
27
- token: `${baseUrl}/oauth2/token`,
221
+ token: dpop ? {
222
+ url: `${baseUrl}/oauth2/token`,
223
+ async request({ client, provider, params, checks }) {
224
+ const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
225
+ dpop.assertTokenResponse(tokens);
226
+ return { tokens };
227
+ }
228
+ } : `${baseUrl}/oauth2/token`,
28
229
  userinfo: `${baseUrl}/oauth2/userinfo`,
230
+ ...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
29
231
  client: {
30
232
  id: options.clientId,
31
- secret: options.clientSecret,
32
- token_endpoint_auth_method: "client_secret_post",
233
+ secret: clientAssertionKey ? void 0 : options.clientSecret,
234
+ token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
235
+ ...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
33
236
  id_token_signed_response_alg: "RS256"
34
237
  // OAuth42 uses RS256 for ID tokens
35
238
  },
36
- issuer: baseUrl,
37
- checks: options.pkceEnabled !== false ? ["pkce", "state"] : ["state"],
239
+ issuer,
240
+ checks: ["pkce", "state", "nonce"],
38
241
  profile(profile, tokens) {
39
242
  return {
40
243
  id: profile.sub || profile.id || profile.email,
@@ -50,42 +253,18 @@ function OAuth42Provider(options) {
50
253
  bg: "#1e40af",
51
254
  text: "#ffffff"
52
255
  },
53
- options
256
+ // NextAuth v4 types require a string even when private_key_jwt sends no secret.
257
+ options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
54
258
  };
55
259
  }
56
260
 
57
261
  // src/server/session.ts
58
262
  import { getServerSession as getNextAuthSession } from "next-auth";
59
- import { headers } from "next/headers";
60
- var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
61
263
  async function getOAuth42Session(...args) {
62
264
  const session = await getNextAuthSession(...args);
63
- if (!session) {
265
+ if (!session || session.error) {
64
266
  return null;
65
267
  }
66
- let refreshedToken = null;
67
- try {
68
- if (args.length === 1) {
69
- const headersList = await headers();
70
- refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
71
- } else if (args.length === 3) {
72
- const req = args[0];
73
- refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
74
- }
75
- } catch {
76
- }
77
- if (refreshedToken) {
78
- const sessionToken = session.accessToken;
79
- console.log("[OAuth42 Session] Using refreshed token from middleware:", {
80
- refreshedPrefix: refreshedToken.substring(0, 20),
81
- sessionPrefix: sessionToken?.substring(0, 20),
82
- tokensMatch: refreshedToken === sessionToken
83
- });
84
- return {
85
- ...session,
86
- accessToken: refreshedToken
87
- };
88
- }
89
268
  return session;
90
269
  }
91
270
  function withOAuth42Session(handler, authOptions) {
@@ -123,66 +302,59 @@ function isBackendSessionMode(mode) {
123
302
  function sessionEndpoint(issuer) {
124
303
  return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
125
304
  }
126
- async function createBackendSession(params) {
127
- const response = await fetch(sessionEndpoint(params.issuer), {
128
- method: "POST",
305
+ async function sessionFetch(params, target, body, credential, method = "POST") {
306
+ OAuth42ClientAssertion.validateEndpoint(target);
307
+ const prepare = () => ({
308
+ method,
309
+ redirect: "error",
310
+ cache: "no-store",
311
+ signal: AbortSignal.timeout(3e4),
129
312
  headers: { "Content-Type": "application/json" },
130
313
  body: JSON.stringify({
131
- access_token: params.accessToken,
132
- refresh_token: params.refreshToken,
133
- expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
134
- id_token: params.idToken,
314
+ ...body,
135
315
  client_id: params.clientId,
136
- client_secret: params.clientSecret,
137
- issuer: params.issuer,
138
- user: params.user
316
+ ...params.clientAssertionKey ? {
317
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
318
+ client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
319
+ } : { client_secret: params.clientSecret }
139
320
  })
140
321
  });
141
- if (!response.ok) {
142
- throw new Error(`OAuth42 backend session create failed: ${response.status}`);
143
- }
144
- return response.json();
322
+ return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
323
+ }
324
+ async function sessionResponse(response, params) {
325
+ if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
326
+ const session = await response.json();
327
+ if (params.dpop) params.dpop.assertTokenResponse(session);
328
+ else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
329
+ return session;
330
+ }
331
+ async function createBackendSession(params) {
332
+ return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
333
+ access_token: params.accessToken,
334
+ refresh_token: params.refreshToken,
335
+ id_token: params.idToken,
336
+ user: params.user
337
+ }, params.accessToken), params);
145
338
  }
146
339
  async function resolveBackendSession(params) {
147
- const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
148
- method: "POST",
149
- headers: { "Content-Type": "application/json" },
150
- body: JSON.stringify({
151
- session_id: params.sessionId,
152
- client_id: params.clientId,
153
- client_secret: params.clientSecret,
154
- issuer: params.issuer
155
- })
156
- });
157
- if (response.status === 401 || response.status === 403 || response.status === 404) {
158
- return null;
159
- }
160
- if (!response.ok) {
161
- throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
162
- }
163
- return response.json();
340
+ const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
341
+ if ([401, 403, 404].includes(response.status)) return null;
342
+ return sessionResponse(response, params);
164
343
  }
165
344
  async function revokeBackendSession(params) {
166
- await fetch(sessionEndpoint(params.issuer), {
167
- method: "DELETE",
168
- headers: { "Content-Type": "application/json" },
169
- body: JSON.stringify({
170
- session_id: params.sessionId,
171
- client_id: params.clientId,
172
- client_secret: params.clientSecret
173
- })
174
- });
345
+ const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
346
+ if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
175
347
  }
176
348
 
177
349
  // src/server/auth.ts
178
350
  var NextAuth = NextAuthDefault.default || NextAuthDefault;
179
- var activeRefresh = null;
351
+ var activeRefreshes = /* @__PURE__ */ new Map();
180
352
  function createAuth(options = {}) {
181
353
  const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
182
- const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
354
+ const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
183
355
  const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
184
356
  const useBackendSessions = isBackendSessionMode(options.sessionStore);
185
- if (!clientId || !clientSecret) {
357
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
186
358
  throw new Error(
187
359
  "OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
188
360
  );
@@ -191,14 +363,17 @@ function createAuth(options = {}) {
191
363
  providers: [
192
364
  OAuth42Provider({
193
365
  clientId,
194
- clientSecret,
366
+ clientSecret: clientSecret || "",
195
367
  issuer,
196
368
  scopes: options.scopes,
197
- pkceEnabled: options.pkceEnabled
369
+ pkceEnabled: options.pkceEnabled,
370
+ dpop: options.dpop,
371
+ clientAssertionKey: options.clientAssertionKey
198
372
  })
199
373
  ],
200
374
  callbacks: {
201
375
  async jwt({ token, account, profile }) {
376
+ if (options.clientAssertionKey) delete token.clientSecret;
202
377
  console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
203
378
  if (account) {
204
379
  if (useBackendSessions) {
@@ -207,9 +382,11 @@ function createAuth(options = {}) {
207
382
  }
208
383
  const oauth42Profile = profile;
209
384
  const backendSession = await createBackendSession({
385
+ dpop: options.dpop,
386
+ clientAssertionKey: options.clientAssertionKey,
210
387
  issuer,
211
388
  clientId,
212
- clientSecret,
389
+ clientSecret: clientSecret || "",
213
390
  accessToken: account.access_token,
214
391
  refreshToken: account.refresh_token,
215
392
  expiresAt: account.expires_at,
@@ -233,7 +410,8 @@ function createAuth(options = {}) {
233
410
  token.expiresAt = account.expires_at;
234
411
  token.idToken = account.id_token;
235
412
  token.clientId = clientId;
236
- token.clientSecret = clientSecret;
413
+ if (options.clientAssertionKey) delete token.clientSecret;
414
+ else token.clientSecret = clientSecret;
237
415
  token.issuer = issuer;
238
416
  }
239
417
  }
@@ -252,25 +430,36 @@ function createAuth(options = {}) {
252
430
  },
253
431
  async session({ session, token }) {
254
432
  console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
433
+ const revokedSession = () => {
434
+ session.error = "SessionRevoked";
435
+ delete session.accessToken;
436
+ delete session.idToken;
437
+ delete session.userInfo;
438
+ delete session.user;
439
+ return session;
440
+ };
441
+ if (token.error) return revokedSession();
442
+ let verified;
255
443
  if (useBackendSessions) {
256
444
  const sessionId = token.oauth42SessionId;
257
445
  if (!sessionId) {
258
- session.error = "SessionRevoked";
259
- return session;
446
+ return revokedSession();
260
447
  }
261
448
  const backendSession = await resolveBackendSession({
449
+ dpop: options.dpop,
450
+ clientAssertionKey: options.clientAssertionKey,
262
451
  issuer,
263
452
  clientId,
264
- clientSecret,
453
+ clientSecret: clientSecret || "",
265
454
  sessionId
266
455
  });
267
456
  if (!backendSession) {
268
- session.error = "SessionRevoked";
269
- return session;
457
+ return revokedSession();
270
458
  }
271
459
  session.accessToken = backendSession.access_token;
272
460
  session.idToken = backendSession.id_token;
273
461
  token.expiresAt = backendSession.expires_at;
462
+ verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
274
463
  if (backendSession.user && session.user) {
275
464
  session.user.email = backendSession.user.email;
276
465
  session.user.name = backendSession.user.name;
@@ -281,6 +470,23 @@ function createAuth(options = {}) {
281
470
  } else {
282
471
  session.accessToken = token.accessToken;
283
472
  session.idToken = token.idToken;
473
+ verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
474
+ }
475
+ if (!verified) {
476
+ return revokedSession();
477
+ }
478
+ session.userInfo = verified;
479
+ token.email = verified.email;
480
+ token.name = verified.name;
481
+ token.username = verified.preferred_username ?? verified.username;
482
+ token.emailVerified = verified.email_verified;
483
+ token.groups = verified.groups ?? [];
484
+ if (session.user) {
485
+ session.user.email = verified.email;
486
+ session.user.name = verified.name;
487
+ session.user.username = verified.preferred_username ?? verified.username;
488
+ session.user.emailVerified = verified.email_verified;
489
+ session.user.groups = verified.groups ?? [];
284
490
  }
285
491
  if (token.error) {
286
492
  session.error = token.error;
@@ -315,9 +521,11 @@ function createAuth(options = {}) {
315
521
  const sessionId = message.token?.oauth42SessionId;
316
522
  if (sessionId) {
317
523
  await revokeBackendSession({
524
+ dpop: options.dpop,
525
+ clientAssertionKey: options.clientAssertionKey,
318
526
  issuer,
319
527
  clientId,
320
- clientSecret,
528
+ clientSecret: clientSecret || "",
321
529
  sessionId
322
530
  }).catch((error) => {
323
531
  console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
@@ -404,7 +612,8 @@ function createAuth(options = {}) {
404
612
  }
405
613
  }
406
614
  };
407
- const handler = NextAuth(authOptions);
615
+ const nextAuthHandler = NextAuth(authOptions);
616
+ const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
408
617
  return {
409
618
  auth: authOptions,
410
619
  handlers: { GET: handler, POST: handler }
@@ -415,24 +624,40 @@ function createHandlers(authOptions) {
415
624
  return { GET: handler, POST: handler };
416
625
  }
417
626
  var getServerSession = getOAuth42Session;
418
- async function refreshAccessToken(token, clientId, clientSecret, issuer) {
419
- if (activeRefresh) {
420
- console.log("[OAuth42] Refresh already in progress, waiting...");
421
- return await activeRefresh;
627
+ async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
628
+ if (typeof token.refreshToken !== "string" || !token.refreshToken) {
629
+ return { ...token, error: "RefreshAccessTokenError" };
630
+ }
631
+ 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]);
632
+ let refresh = activeRefreshes.get(key);
633
+ if (!refresh) {
634
+ refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
635
+ activeRefreshes.set(key, refresh);
422
636
  }
423
- activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
424
637
  try {
425
- return await activeRefresh;
638
+ const result = await refresh;
639
+ const updated = {
640
+ ...token,
641
+ accessToken: result.accessToken,
642
+ refreshToken: result.refreshToken,
643
+ expiresAt: result.expiresAt,
644
+ error: result.error
645
+ };
646
+ if (clientAssertionKey) delete updated.clientSecret;
647
+ return updated;
426
648
  } finally {
427
- activeRefresh = null;
649
+ if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
428
650
  }
429
651
  }
430
- async function doRefresh(token, clientId, clientSecret, issuer) {
652
+ async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
431
653
  try {
432
654
  const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
433
655
  const tokenUrl = `${baseUrl}/oauth2/token`;
434
656
  const fetchOptions = {
435
657
  method: "POST",
658
+ redirect: "error",
659
+ cache: "no-store",
660
+ signal: AbortSignal.timeout(3e4),
436
661
  headers: {
437
662
  "Content-Type": "application/x-www-form-urlencoded"
438
663
  },
@@ -443,17 +668,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
443
668
  client_secret: clientSecret
444
669
  })
445
670
  };
446
- if (process.env.NODE_ENV !== "production" && tokenUrl.startsWith("https://")) {
447
- const https2 = await import("https");
448
- fetchOptions.agent = new https2.Agent({
449
- rejectUnauthorized: false
450
- });
451
- }
452
- const response = await fetch(tokenUrl, fetchOptions);
671
+ const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
453
672
  const refreshedTokens = await response.json();
454
673
  if (!response.ok) {
455
674
  throw refreshedTokens;
456
675
  }
676
+ dpop?.assertTokenResponse(refreshedTokens);
457
677
  console.log("[OAuth42] Token refreshed successfully");
458
678
  return {
459
679
  ...token,
@@ -465,7 +685,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
465
685
  error: void 0
466
686
  };
467
687
  } catch (error) {
468
- console.error("[OAuth42] Failed to refresh access token:", error);
688
+ console.error("[OAuth42] Failed to refresh access token");
469
689
  return {
470
690
  ...token,
471
691
  error: "RefreshAccessTokenError"
@@ -482,14 +702,20 @@ import { getToken, encode } from "next-auth/jwt";
482
702
  var ALLOWED_COOKIE_SIZE = 4096;
483
703
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
484
704
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
485
- var REFRESH_RACE_GRACE_SECONDS = 60;
486
705
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
487
706
  var pendingRefreshes = /* @__PURE__ */ new Map();
488
- function shouldPreserveSessionOnRefreshFailure(params) {
489
- if (params.error !== "invalid_grant" || !params.expiresAt) {
490
- return false;
707
+ function forwardedHeaders(req, cookieName, jwt) {
708
+ const headers = new Headers(req.headers);
709
+ headers.delete("x-oauth42-refreshed-token");
710
+ if (cookieName && jwt) {
711
+ const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
712
+ if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
713
+ else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
714
+ cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
715
+ }
716
+ headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
491
717
  }
492
- return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
718
+ return headers;
493
719
  }
494
720
  function resolveRefreshConfig(options, token) {
495
721
  return {
@@ -498,11 +724,15 @@ function resolveRefreshConfig(options, token) {
498
724
  issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
499
725
  };
500
726
  }
501
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
727
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
502
728
  try {
503
729
  const tokenUrl = `${issuer}/oauth2/token`;
504
- const response = await fetch(tokenUrl, {
730
+ const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
731
+ const response = await fetcher(tokenUrl, {
505
732
  method: "POST",
733
+ redirect: "error",
734
+ cache: "no-store",
735
+ signal: AbortSignal.timeout(3e4),
506
736
  headers: {
507
737
  "Content-Type": "application/x-www-form-urlencoded"
508
738
  },
@@ -515,9 +745,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
515
745
  });
516
746
  const data = await response.json();
517
747
  if (!response.ok) {
518
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
748
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
519
749
  return { success: false, error: data.error || "refresh_failed" };
520
750
  }
751
+ dpop?.assertTokenResponse(data);
521
752
  console.log("[OAuth42 Middleware] Token refreshed successfully");
522
753
  return {
523
754
  success: true,
@@ -526,14 +757,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
526
757
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
527
758
  };
528
759
  } catch (error) {
529
- console.error("[OAuth42 Middleware] Token refresh error:", error);
760
+ console.error("[OAuth42 Middleware] Token refresh request failed");
530
761
  return { success: false, error: "refresh_error" };
531
762
  }
532
763
  }
533
764
  function withOAuth42Auth(options = {}) {
534
765
  const secret = process.env.NEXTAUTH_SECRET;
535
766
  if (!secret) {
536
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
767
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
537
768
  }
538
769
  return async function middleware(req) {
539
770
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -544,22 +775,41 @@ function withOAuth42Auth(options = {}) {
544
775
  });
545
776
  const pathname = req.nextUrl.pathname;
546
777
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
547
- return NextResponse.next();
778
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
548
779
  }
549
780
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
550
781
  if (!needsProtection) {
551
- return NextResponse.next();
782
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
552
783
  }
553
- if (!token) {
784
+ if (!token || token.error) {
554
785
  const signInUrl = options.pages?.signIn || "/auth/signin";
555
786
  const url = new URL(signInUrl, req.url);
556
787
  url.searchParams.set("callbackUrl", pathname);
557
788
  return NextResponse.redirect(url);
558
789
  }
559
790
  const refreshConfig = resolveRefreshConfig(options, token);
791
+ const denied = () => {
792
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
793
+ url.searchParams.set("callbackUrl", pathname);
794
+ const response = NextResponse.redirect(url);
795
+ for (const cookie of req.cookies.getAll()) {
796
+ if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
797
+ }
798
+ return response;
799
+ };
800
+ const verifyAndAuthorize = async (current, resolved) => {
801
+ const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
802
+ if (!user) return false;
803
+ current.groups = user.groups ?? [];
804
+ current.email = user.email;
805
+ current.name = user.name;
806
+ current.username = user.preferred_username ?? user.username;
807
+ current.emailVerified = user.email_verified;
808
+ return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
809
+ };
560
810
  if (isBackendSessionMode(options.sessionStore)) {
561
811
  const sessionId = token.oauth42SessionId;
562
- if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
812
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
563
813
  const signInUrl = options.pages?.signIn || "/auth/signin";
564
814
  const url = new URL(signInUrl, req.url);
565
815
  url.searchParams.set("callbackUrl", pathname);
@@ -568,9 +818,11 @@ function withOAuth42Auth(options = {}) {
568
818
  return response;
569
819
  }
570
820
  const backendSession = await resolveBackendSession({
821
+ dpop: options.dpop,
822
+ clientAssertionKey: options.clientAssertionKey,
571
823
  issuer: refreshConfig.issuer,
572
824
  clientId: refreshConfig.clientId,
573
- clientSecret: refreshConfig.clientSecret,
825
+ clientSecret: refreshConfig.clientSecret || "",
574
826
  sessionId
575
827
  }).catch((error) => {
576
828
  console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
@@ -584,8 +836,9 @@ function withOAuth42Auth(options = {}) {
584
836
  response.cookies.delete(cookieName);
585
837
  return response;
586
838
  }
587
- const requestHeaders = new Headers(req.headers);
588
- requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
839
+ const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
840
+ if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
841
+ const requestHeaders = forwardedHeaders(req);
589
842
  return NextResponse.next({
590
843
  request: {
591
844
  headers: requestHeaders
@@ -601,13 +854,12 @@ function withOAuth42Auth(options = {}) {
601
854
  expiresAt,
602
855
  now,
603
856
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
604
- accessTokenPrefix: token.accessToken?.substring(0, 20),
605
857
  path: pathname
606
858
  });
607
859
  }
608
- if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
609
- const userId = token.sub;
610
- let refreshPromise = pendingRefreshes.get(userId);
860
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
861
+ const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
862
+ let refreshPromise = pendingRefreshes.get(grantKey);
611
863
  if (refreshPromise) {
612
864
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
613
865
  } else {
@@ -615,15 +867,17 @@ function withOAuth42Auth(options = {}) {
615
867
  refreshPromise = refreshTokens(
616
868
  token.refreshToken,
617
869
  refreshConfig.clientId,
618
- refreshConfig.clientSecret,
619
- refreshConfig.issuer
870
+ refreshConfig.clientSecret || "",
871
+ refreshConfig.issuer,
872
+ options.dpop,
873
+ options.clientAssertionKey
620
874
  ).finally(() => {
621
- const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
875
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
622
876
  if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
623
877
  cleanupTimer.unref();
624
878
  }
625
879
  });
626
- pendingRefreshes.set(userId, refreshPromise);
880
+ pendingRefreshes.set(grantKey, refreshPromise);
627
881
  }
628
882
  const refreshed = await refreshPromise;
629
883
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -633,12 +887,13 @@ function withOAuth42Auth(options = {}) {
633
887
  refreshToken: refreshed.refreshToken,
634
888
  expiresAt: refreshed.expiresAt
635
889
  };
890
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
891
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
636
892
  const newJwt = await encode({
637
893
  token: updatedToken,
638
894
  secret
639
895
  });
640
- const requestHeaders = new Headers(req.headers);
641
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
896
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
642
897
  const response = NextResponse.next({
643
898
  request: {
644
899
  headers: requestHeaders
@@ -691,28 +946,11 @@ function withOAuth42Auth(options = {}) {
691
946
  chunked: newJwt.length > CHUNK_SIZE,
692
947
  existingBaseCooke: req.cookies.has(cookieName),
693
948
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
694
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
695
949
  newExpiresAt: refreshed.expiresAt,
696
950
  cookieName
697
951
  });
698
952
  return response;
699
953
  } else {
700
- if (shouldPreserveSessionOnRefreshFailure({
701
- error: refreshed.error,
702
- expiresAt,
703
- now
704
- })) {
705
- console.warn(
706
- "[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
707
- {
708
- expiresAt,
709
- now,
710
- expiredAgo: now - expiresAt,
711
- path: pathname
712
- }
713
- );
714
- return NextResponse.next();
715
- }
716
954
  console.log("[OAuth42 Middleware] Refresh failed, clearing session");
717
955
  const signInUrl = options.pages?.signIn || "/auth/signin";
718
956
  const url = new URL(signInUrl, req.url);
@@ -730,16 +968,13 @@ function withOAuth42Auth(options = {}) {
730
968
  return response;
731
969
  }
732
970
  }
733
- if (options.callbacks?.authorized) {
734
- const isAuthorized = await options.callbacks.authorized({ token, req });
735
- if (!isAuthorized) {
736
- const signInUrl = options.pages?.signIn || "/auth/signin";
737
- const url = new URL(signInUrl, req.url);
738
- url.searchParams.set("callbackUrl", pathname);
739
- return NextResponse.redirect(url);
740
- }
971
+ if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
972
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
973
+ url.searchParams.set("callbackUrl", pathname);
974
+ return NextResponse.redirect(url);
741
975
  }
742
- return NextResponse.next();
976
+ if (!await verifyAndAuthorize(token)) return denied();
977
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
743
978
  };
744
979
  }
745
980
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -761,7 +996,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
761
996
 
762
997
  // src/server/hosted-auth-callback.ts
763
998
  import { NextResponse as NextResponse2 } from "next/server";
764
- import https from "https";
999
+ import { timingSafeEqual } from "crypto";
765
1000
  import { encode as encode2 } from "next-auth/jwt";
766
1001
  function createHostedAuthCallback(options = {}) {
767
1002
  return async function GET(request) {
@@ -774,7 +1009,7 @@ function createHostedAuthCallback(options = {}) {
774
1009
  redirectUrl = "/dashboard",
775
1010
  errorUrl = "/"
776
1011
  } = options;
777
- if (!clientId || !clientSecret) {
1012
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
778
1013
  console.error("OAuth42 client credentials are required");
779
1014
  return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
780
1015
  }
@@ -784,6 +1019,14 @@ function createHostedAuthCallback(options = {}) {
784
1019
  }
785
1020
  const searchParams = request.nextUrl.searchParams;
786
1021
  const code = searchParams.get("code");
1022
+ const cookiePrefix = process.env.NODE_ENV === "production" ? "__Host-" : request.nextUrl.protocol === "https:" ? "__Host-" : "";
1023
+ const expectedState = request.cookies.get(`${cookiePrefix}oauth42-state`)?.value;
1024
+ const returnedState = searchParams.get("state");
1025
+ const verifier = request.cookies.get(`${cookiePrefix}oauth42-pkce`)?.value;
1026
+ const validState = expectedState && returnedState && expectedState.length >= 32 && Buffer.byteLength(expectedState) === Buffer.byteLength(returnedState) && timingSafeEqual(Buffer.from(expectedState), Buffer.from(returnedState));
1027
+ if (!validState || !verifier || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
1028
+ return NextResponse2.redirect(new URL(`${errorUrl}?error=invalid_state`, request.url));
1029
+ }
787
1030
  if (!code) {
788
1031
  return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
789
1032
  }
@@ -791,39 +1034,38 @@ function createHostedAuthCallback(options = {}) {
791
1034
  const tokenEndpoint = `${issuer}/oauth2/token`;
792
1035
  const body = new URLSearchParams({
793
1036
  grant_type: "authorization_code",
1037
+ code_verifier: verifier,
794
1038
  code,
795
1039
  redirect_uri: `${baseUrl}/api/oauth/callback`,
796
1040
  client_id: clientId,
797
- client_secret: clientSecret
1041
+ client_secret: clientSecret || ""
798
1042
  });
799
1043
  const fetchOptions = {
800
1044
  method: "POST",
1045
+ redirect: "error",
1046
+ cache: "no-store",
1047
+ signal: AbortSignal.timeout(3e4),
801
1048
  headers: {
802
1049
  "Content-Type": "application/x-www-form-urlencoded"
803
1050
  },
804
1051
  body: body.toString()
805
1052
  };
806
- if (process.env.NODE_ENV !== "production") {
807
- const agent = new https.Agent({
808
- rejectUnauthorized: false
809
- });
810
- fetchOptions.agent = agent;
811
- }
812
- const tokenResponse = await fetch(tokenEndpoint, fetchOptions);
1053
+ 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);
813
1054
  const tokens = await tokenResponse.json();
814
1055
  if (!tokenResponse.ok) {
815
- console.error("Token exchange failed:", tokens);
1056
+ console.error("Token exchange failed:", tokenResponse.status);
816
1057
  return NextResponse2.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
817
1058
  }
1059
+ options.dpop?.assertTokenResponse(tokens);
818
1060
  const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
819
- const userInfoResponse = await fetch(userInfoEndpoint, {
820
- headers: {
821
- "Authorization": `Bearer ${tokens.access_token}`
822
- },
823
- ...process.env.NODE_ENV !== "production" ? {
824
- agent: new https.Agent({ rejectUnauthorized: false })
825
- } : {}
826
- });
1061
+ const infoOptions = {
1062
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
1063
+ redirect: "error",
1064
+ cache: "no-store",
1065
+ signal: AbortSignal.timeout(3e4)
1066
+ };
1067
+ const userInfoResponse = options.dpop ? await options.dpop.fetch(userInfoEndpoint, infoOptions, tokens.access_token) : await fetch(userInfoEndpoint, infoOptions);
1068
+ if (!userInfoResponse.ok) throw new Error("User info request failed");
827
1069
  const userInfo = await userInfoResponse.json();
828
1070
  const sessionToken = await encode2({
829
1071
  token: {
@@ -848,6 +1090,14 @@ function createHostedAuthCallback(options = {}) {
848
1090
  // 30 days
849
1091
  path: "/"
850
1092
  });
1093
+ for (const name of ["oauth42-state", "oauth42-pkce"]) {
1094
+ redirectResponse.cookies.set(`${cookiePrefix}${name}`, "", {
1095
+ path: "/",
1096
+ secure: cookiePrefix === "__Host-",
1097
+ sameSite: "lax",
1098
+ maxAge: 0
1099
+ });
1100
+ }
851
1101
  return redirectResponse;
852
1102
  } catch (error) {
853
1103
  console.error("OAuth callback error:", error);
@@ -855,8 +1105,78 @@ function createHostedAuthCallback(options = {}) {
855
1105
  }
856
1106
  };
857
1107
  }
1108
+
1109
+ // src/server/dpop.ts
1110
+ import { createHash as createHash2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2, randomBytes as randomBytes2, sign as sign2 } from "crypto";
1111
+ var OAuth42DPoP = class _OAuth42DPoP {
1112
+ constructor(key) {
1113
+ this.nonces = /* @__PURE__ */ new Map();
1114
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
1115
+ throw new Error("DPoP requires a private P-256 key");
1116
+ }
1117
+ this.key = key;
1118
+ const publicKey = createPublicKey2(key).export({ format: "jwk" });
1119
+ this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
1120
+ this.thumbprint = createHash2("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
1121
+ }
1122
+ /** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
1123
+ static generate() {
1124
+ return new _OAuth42DPoP(generateKeyPairSync2("ec", { namedCurve: "prime256v1" }).privateKey);
1125
+ }
1126
+ /** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
1127
+ get oidcKey() {
1128
+ return this.key;
1129
+ }
1130
+ proof(method, target, nonce, accessToken) {
1131
+ const url = new URL(target);
1132
+ if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
1133
+ throw new Error("DPoP requires HTTPS outside loopback development");
1134
+ }
1135
+ if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
1136
+ url.search = "";
1137
+ url.hash = "";
1138
+ const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
1139
+ const claims = {
1140
+ htm: method.toUpperCase(),
1141
+ htu: url.href,
1142
+ iat: Math.floor(Date.now() / 1e3),
1143
+ jti: randomBytes2(32).toString("base64url"),
1144
+ ...nonce ? { nonce } : {},
1145
+ ...accessToken !== void 0 ? { ath: createHash2("sha256").update(accessToken).digest("base64url") } : {}
1146
+ };
1147
+ const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
1148
+ return `${input}.${sign2("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
1149
+ }
1150
+ assertTokenResponse(tokens) {
1151
+ if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
1152
+ throw new Error("Server did not issue the requested DPoP-bound token");
1153
+ }
1154
+ }
1155
+ async fetch(target, init = {}, accessToken, prepare) {
1156
+ if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
1157
+ const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
1158
+ for (let attempt = 0; attempt < 2; attempt++) {
1159
+ const request = prepare ? prepare() : init;
1160
+ const headers = new Headers(request.headers);
1161
+ headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
1162
+ if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
1163
+ const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
1164
+ const nonce = response.headers.get("DPoP-Nonce");
1165
+ if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
1166
+ if (this.nonces.size >= 256) this.nonces.clear();
1167
+ this.nonces.set(cacheKey, nonce);
1168
+ } else return response;
1169
+ if (attempt || ![400, 401].includes(response.status)) return response;
1170
+ const error = await response.clone().json().catch(() => null);
1171
+ if (error?.error !== "use_dpop_nonce") return response;
1172
+ }
1173
+ throw new Error("DPoP nonce negotiation failed");
1174
+ }
1175
+ };
858
1176
  export {
859
1177
  default2 as NextAuth,
1178
+ OAuth42ClientAssertion,
1179
+ OAuth42DPoP,
860
1180
  OAuth42Provider,
861
1181
  createAuth,
862
1182
  createBackendSession,
@@ -865,11 +1185,13 @@ export {
865
1185
  createMiddlewareConfig,
866
1186
  getOAuth42Session,
867
1187
  getServerSession,
1188
+ pushAuthorizationURL,
868
1189
  refreshAccessToken,
869
1190
  resolveBackendSession,
870
1191
  revokeBackendSession,
871
1192
  withOAuth42Auth,
872
1193
  withOAuth42ServerSideProps,
873
- withOAuth42Session
1194
+ withOAuth42Session,
1195
+ withPushedAuthorization
874
1196
  };
875
1197
  //# sourceMappingURL=index.mjs.map