@oauth42/next 0.4.8 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -116,13 +295,66 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
116
295
  };
117
296
  }
118
297
 
298
+ // src/server/backend-session.ts
299
+ function isBackendSessionMode(mode) {
300
+ return mode === "backend";
301
+ }
302
+ function sessionEndpoint(issuer) {
303
+ return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
304
+ }
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),
312
+ headers: { "Content-Type": "application/json" },
313
+ body: JSON.stringify({
314
+ ...body,
315
+ client_id: params.clientId,
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 }
320
+ })
321
+ });
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);
338
+ }
339
+ async function resolveBackendSession(params) {
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);
343
+ }
344
+ async function revokeBackendSession(params) {
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}`);
347
+ }
348
+
119
349
  // src/server/auth.ts
120
350
  var NextAuth = NextAuthDefault.default || NextAuthDefault;
121
- var activeRefresh = null;
351
+ var activeRefreshes = /* @__PURE__ */ new Map();
122
352
  function createAuth(options = {}) {
123
353
  const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
124
- const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
125
- if (!clientId || !clientSecret) {
354
+ const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
355
+ const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
356
+ const useBackendSessions = isBackendSessionMode(options.sessionStore);
357
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
126
358
  throw new Error(
127
359
  "OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
128
360
  );
@@ -131,24 +363,57 @@ function createAuth(options = {}) {
131
363
  providers: [
132
364
  OAuth42Provider({
133
365
  clientId,
134
- clientSecret,
135
- issuer: options.issuer,
366
+ clientSecret: clientSecret || "",
367
+ issuer,
136
368
  scopes: options.scopes,
137
- pkceEnabled: options.pkceEnabled
369
+ pkceEnabled: options.pkceEnabled,
370
+ dpop: options.dpop,
371
+ clientAssertionKey: options.clientAssertionKey
138
372
  })
139
373
  ],
140
374
  callbacks: {
141
375
  async jwt({ token, account, profile }) {
376
+ if (options.clientAssertionKey) delete token.clientSecret;
142
377
  console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
143
378
  if (account) {
144
- console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
145
- token.accessToken = account.access_token;
146
- token.refreshToken = account.refresh_token;
147
- token.expiresAt = account.expires_at;
148
- token.idToken = account.id_token;
149
- token.clientId = clientId;
150
- token.clientSecret = clientSecret;
151
- token.issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER;
379
+ if (useBackendSessions) {
380
+ if (!account.access_token || !account.refresh_token) {
381
+ throw new Error("OAuth42 backend sessions require access and refresh tokens");
382
+ }
383
+ const oauth42Profile = profile;
384
+ const backendSession = await createBackendSession({
385
+ dpop: options.dpop,
386
+ clientAssertionKey: options.clientAssertionKey,
387
+ issuer,
388
+ clientId,
389
+ clientSecret: clientSecret || "",
390
+ accessToken: account.access_token,
391
+ refreshToken: account.refresh_token,
392
+ expiresAt: account.expires_at,
393
+ idToken: account.id_token,
394
+ user: {
395
+ sub: oauth42Profile?.sub,
396
+ email: oauth42Profile?.email,
397
+ username: oauth42Profile?.username,
398
+ name: token.name,
399
+ emailVerified: oauth42Profile?.email_verified,
400
+ groups: oauth42Profile?.groups
401
+ }
402
+ });
403
+ token.oauth42SessionId = backendSession.session_id;
404
+ token.expiresAt = backendSession.expires_at;
405
+ token.issuer = issuer;
406
+ } else {
407
+ console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
408
+ token.accessToken = account.access_token;
409
+ token.refreshToken = account.refresh_token;
410
+ token.expiresAt = account.expires_at;
411
+ token.idToken = account.id_token;
412
+ token.clientId = clientId;
413
+ if (options.clientAssertionKey) delete token.clientSecret;
414
+ else token.clientSecret = clientSecret;
415
+ token.issuer = issuer;
416
+ }
152
417
  }
153
418
  if (profile) {
154
419
  const oauth42Profile = profile;
@@ -165,12 +430,68 @@ function createAuth(options = {}) {
165
430
  },
166
431
  async session({ session, token }) {
167
432
  console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
168
- session.accessToken = token.accessToken;
169
- session.idToken = token.idToken;
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;
443
+ if (useBackendSessions) {
444
+ const sessionId = token.oauth42SessionId;
445
+ if (!sessionId) {
446
+ return revokedSession();
447
+ }
448
+ const backendSession = await resolveBackendSession({
449
+ dpop: options.dpop,
450
+ clientAssertionKey: options.clientAssertionKey,
451
+ issuer,
452
+ clientId,
453
+ clientSecret: clientSecret || "",
454
+ sessionId
455
+ });
456
+ if (!backendSession) {
457
+ return revokedSession();
458
+ }
459
+ session.accessToken = backendSession.access_token;
460
+ session.idToken = backendSession.id_token;
461
+ token.expiresAt = backendSession.expires_at;
462
+ verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
463
+ if (backendSession.user && session.user) {
464
+ session.user.email = backendSession.user.email;
465
+ session.user.name = backendSession.user.name;
466
+ session.user.username = backendSession.user.username;
467
+ session.user.emailVerified = backendSession.user.emailVerified;
468
+ session.user.groups = backendSession.user.groups;
469
+ }
470
+ } else {
471
+ session.accessToken = token.accessToken;
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 ?? [];
490
+ }
170
491
  if (token.error) {
171
492
  session.error = token.error;
172
493
  }
173
- if (session.user) {
494
+ if (!useBackendSessions && session.user) {
174
495
  session.user.email = token.email;
175
496
  session.user.name = token.name;
176
497
  session.user.username = token.username;
@@ -194,6 +515,28 @@ function createAuth(options = {}) {
194
515
  strategy: "jwt",
195
516
  ...options.session
196
517
  },
518
+ events: {
519
+ async signOut(message) {
520
+ if (useBackendSessions) {
521
+ const sessionId = message.token?.oauth42SessionId;
522
+ if (sessionId) {
523
+ await revokeBackendSession({
524
+ dpop: options.dpop,
525
+ clientAssertionKey: options.clientAssertionKey,
526
+ issuer,
527
+ clientId,
528
+ clientSecret: clientSecret || "",
529
+ sessionId
530
+ }).catch((error) => {
531
+ console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
532
+ });
533
+ }
534
+ }
535
+ if (options.events?.signOut) {
536
+ await options.events.signOut(message);
537
+ }
538
+ }
539
+ },
197
540
  debug: options.debug || process.env.NODE_ENV === "development",
198
541
  secret: process.env.NEXTAUTH_SECRET,
199
542
  // Configure unique cookie names per app to prevent session conflicts on localhost
@@ -269,7 +612,8 @@ function createAuth(options = {}) {
269
612
  }
270
613
  }
271
614
  };
272
- 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;
273
617
  return {
274
618
  auth: authOptions,
275
619
  handlers: { GET: handler, POST: handler }
@@ -280,24 +624,40 @@ function createHandlers(authOptions) {
280
624
  return { GET: handler, POST: handler };
281
625
  }
282
626
  var getServerSession = getOAuth42Session;
283
- async function refreshAccessToken(token, clientId, clientSecret, issuer) {
284
- if (activeRefresh) {
285
- console.log("[OAuth42] Refresh already in progress, waiting...");
286
- 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);
287
636
  }
288
- activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
289
637
  try {
290
- 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;
291
648
  } finally {
292
- activeRefresh = null;
649
+ if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
293
650
  }
294
651
  }
295
- async function doRefresh(token, clientId, clientSecret, issuer) {
652
+ async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
296
653
  try {
297
654
  const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
298
655
  const tokenUrl = `${baseUrl}/oauth2/token`;
299
656
  const fetchOptions = {
300
657
  method: "POST",
658
+ redirect: "error",
659
+ cache: "no-store",
660
+ signal: AbortSignal.timeout(3e4),
301
661
  headers: {
302
662
  "Content-Type": "application/x-www-form-urlencoded"
303
663
  },
@@ -308,17 +668,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
308
668
  client_secret: clientSecret
309
669
  })
310
670
  };
311
- if (process.env.NODE_ENV !== "production" && tokenUrl.startsWith("https://")) {
312
- const https2 = await import("https");
313
- fetchOptions.agent = new https2.Agent({
314
- rejectUnauthorized: false
315
- });
316
- }
317
- 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);
318
672
  const refreshedTokens = await response.json();
319
673
  if (!response.ok) {
320
674
  throw refreshedTokens;
321
675
  }
676
+ dpop?.assertTokenResponse(refreshedTokens);
322
677
  console.log("[OAuth42] Token refreshed successfully");
323
678
  return {
324
679
  ...token,
@@ -330,7 +685,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
330
685
  error: void 0
331
686
  };
332
687
  } catch (error) {
333
- console.error("[OAuth42] Failed to refresh access token:", error);
688
+ console.error("[OAuth42] Failed to refresh access token");
334
689
  return {
335
690
  ...token,
336
691
  error: "RefreshAccessTokenError"
@@ -347,12 +702,37 @@ import { getToken, encode } from "next-auth/jwt";
347
702
  var ALLOWED_COOKIE_SIZE = 4096;
348
703
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
349
704
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
705
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
350
706
  var pendingRefreshes = /* @__PURE__ */ new Map();
351
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
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("; "));
717
+ }
718
+ return headers;
719
+ }
720
+ function resolveRefreshConfig(options, token) {
721
+ return {
722
+ clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
723
+ clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
724
+ issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
725
+ };
726
+ }
727
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
352
728
  try {
353
729
  const tokenUrl = `${issuer}/oauth2/token`;
354
- 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, {
355
732
  method: "POST",
733
+ redirect: "error",
734
+ cache: "no-store",
735
+ signal: AbortSignal.timeout(3e4),
356
736
  headers: {
357
737
  "Content-Type": "application/x-www-form-urlencoded"
358
738
  },
@@ -365,9 +745,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
365
745
  });
366
746
  const data = await response.json();
367
747
  if (!response.ok) {
368
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
748
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
369
749
  return { success: false, error: data.error || "refresh_failed" };
370
750
  }
751
+ dpop?.assertTokenResponse(data);
371
752
  console.log("[OAuth42 Middleware] Token refreshed successfully");
372
753
  return {
373
754
  success: true,
@@ -376,17 +757,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
376
757
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
377
758
  };
378
759
  } catch (error) {
379
- console.error("[OAuth42 Middleware] Token refresh error:", error);
760
+ console.error("[OAuth42 Middleware] Token refresh request failed");
380
761
  return { success: false, error: "refresh_error" };
381
762
  }
382
763
  }
383
764
  function withOAuth42Auth(options = {}) {
384
765
  const secret = process.env.NEXTAUTH_SECRET;
385
- const clientId = process.env.OAUTH42_CLIENT_ID;
386
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
387
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
388
766
  if (!secret) {
389
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
767
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
390
768
  }
391
769
  return async function middleware(req) {
392
770
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -397,18 +775,76 @@ function withOAuth42Auth(options = {}) {
397
775
  });
398
776
  const pathname = req.nextUrl.pathname;
399
777
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
400
- return NextResponse.next();
778
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
401
779
  }
402
780
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
403
781
  if (!needsProtection) {
404
- return NextResponse.next();
782
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
405
783
  }
406
- if (!token) {
784
+ if (!token || token.error) {
407
785
  const signInUrl = options.pages?.signIn || "/auth/signin";
408
786
  const url = new URL(signInUrl, req.url);
409
787
  url.searchParams.set("callbackUrl", pathname);
410
788
  return NextResponse.redirect(url);
411
789
  }
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
+ };
810
+ if (isBackendSessionMode(options.sessionStore)) {
811
+ const sessionId = token.oauth42SessionId;
812
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
813
+ const signInUrl = options.pages?.signIn || "/auth/signin";
814
+ const url = new URL(signInUrl, req.url);
815
+ url.searchParams.set("callbackUrl", pathname);
816
+ const response = NextResponse.redirect(url);
817
+ response.cookies.delete(cookieName);
818
+ return response;
819
+ }
820
+ const backendSession = await resolveBackendSession({
821
+ dpop: options.dpop,
822
+ clientAssertionKey: options.clientAssertionKey,
823
+ issuer: refreshConfig.issuer,
824
+ clientId: refreshConfig.clientId,
825
+ clientSecret: refreshConfig.clientSecret || "",
826
+ sessionId
827
+ }).catch((error) => {
828
+ console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
829
+ return null;
830
+ });
831
+ if (!backendSession) {
832
+ const signInUrl = options.pages?.signIn || "/auth/signin";
833
+ const url = new URL(signInUrl, req.url);
834
+ url.searchParams.set("callbackUrl", pathname);
835
+ const response = NextResponse.redirect(url);
836
+ response.cookies.delete(cookieName);
837
+ return response;
838
+ }
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);
842
+ return NextResponse.next({
843
+ request: {
844
+ headers: requestHeaders
845
+ }
846
+ });
847
+ }
412
848
  const expiresAt = token.expiresAt;
413
849
  const now = Math.floor(Date.now() / 1e3);
414
850
  const bufferSeconds = 15;
@@ -418,26 +854,30 @@ function withOAuth42Auth(options = {}) {
418
854
  expiresAt,
419
855
  now,
420
856
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
421
- accessTokenPrefix: token.accessToken?.substring(0, 20),
422
857
  path: pathname
423
858
  });
424
859
  }
425
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
426
- const userId = token.sub;
427
- 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);
428
863
  if (refreshPromise) {
429
864
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
430
865
  } else {
431
866
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
432
867
  refreshPromise = refreshTokens(
433
868
  token.refreshToken,
434
- clientId,
435
- clientSecret,
436
- issuer
869
+ refreshConfig.clientId,
870
+ refreshConfig.clientSecret || "",
871
+ refreshConfig.issuer,
872
+ options.dpop,
873
+ options.clientAssertionKey
437
874
  ).finally(() => {
438
- setTimeout(() => pendingRefreshes.delete(userId), 3e4);
875
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
876
+ if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
877
+ cleanupTimer.unref();
878
+ }
439
879
  });
440
- pendingRefreshes.set(userId, refreshPromise);
880
+ pendingRefreshes.set(grantKey, refreshPromise);
441
881
  }
442
882
  const refreshed = await refreshPromise;
443
883
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -447,12 +887,13 @@ function withOAuth42Auth(options = {}) {
447
887
  refreshToken: refreshed.refreshToken,
448
888
  expiresAt: refreshed.expiresAt
449
889
  };
890
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
891
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
450
892
  const newJwt = await encode({
451
893
  token: updatedToken,
452
894
  secret
453
895
  });
454
- const requestHeaders = new Headers(req.headers);
455
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
896
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
456
897
  const response = NextResponse.next({
457
898
  request: {
458
899
  headers: requestHeaders
@@ -462,7 +903,8 @@ function withOAuth42Auth(options = {}) {
462
903
  httpOnly: true,
463
904
  sameSite: "lax",
464
905
  path: "/",
465
- secure: process.env.NODE_ENV === "production"
906
+ secure: process.env.NODE_ENV === "production",
907
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
466
908
  };
467
909
  if (newJwt.length <= CHUNK_SIZE) {
468
910
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -504,13 +946,12 @@ function withOAuth42Auth(options = {}) {
504
946
  chunked: newJwt.length > CHUNK_SIZE,
505
947
  existingBaseCooke: req.cookies.has(cookieName),
506
948
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
507
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
508
949
  newExpiresAt: refreshed.expiresAt,
509
950
  cookieName
510
951
  });
511
952
  return response;
512
953
  } else {
513
- console.log("[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), clearing session");
954
+ console.log("[OAuth42 Middleware] Refresh failed, clearing session");
514
955
  const signInUrl = options.pages?.signIn || "/auth/signin";
515
956
  const url = new URL(signInUrl, req.url);
516
957
  url.searchParams.set("callbackUrl", pathname);
@@ -527,16 +968,13 @@ function withOAuth42Auth(options = {}) {
527
968
  return response;
528
969
  }
529
970
  }
530
- if (options.callbacks?.authorized) {
531
- const isAuthorized = await options.callbacks.authorized({ token, req });
532
- if (!isAuthorized) {
533
- const signInUrl = options.pages?.signIn || "/auth/signin";
534
- const url = new URL(signInUrl, req.url);
535
- url.searchParams.set("callbackUrl", pathname);
536
- return NextResponse.redirect(url);
537
- }
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);
538
975
  }
539
- return NextResponse.next();
976
+ if (!await verifyAndAuthorize(token)) return denied();
977
+ return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
540
978
  };
541
979
  }
542
980
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -558,7 +996,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
558
996
 
559
997
  // src/server/hosted-auth-callback.ts
560
998
  import { NextResponse as NextResponse2 } from "next/server";
561
- import https from "https";
999
+ import { timingSafeEqual } from "crypto";
562
1000
  import { encode as encode2 } from "next-auth/jwt";
563
1001
  function createHostedAuthCallback(options = {}) {
564
1002
  return async function GET(request) {
@@ -571,7 +1009,7 @@ function createHostedAuthCallback(options = {}) {
571
1009
  redirectUrl = "/dashboard",
572
1010
  errorUrl = "/"
573
1011
  } = options;
574
- if (!clientId || !clientSecret) {
1012
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
575
1013
  console.error("OAuth42 client credentials are required");
576
1014
  return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
577
1015
  }
@@ -581,6 +1019,14 @@ function createHostedAuthCallback(options = {}) {
581
1019
  }
582
1020
  const searchParams = request.nextUrl.searchParams;
583
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
+ }
584
1030
  if (!code) {
585
1031
  return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
586
1032
  }
@@ -588,39 +1034,38 @@ function createHostedAuthCallback(options = {}) {
588
1034
  const tokenEndpoint = `${issuer}/oauth2/token`;
589
1035
  const body = new URLSearchParams({
590
1036
  grant_type: "authorization_code",
1037
+ code_verifier: verifier,
591
1038
  code,
592
1039
  redirect_uri: `${baseUrl}/api/oauth/callback`,
593
1040
  client_id: clientId,
594
- client_secret: clientSecret
1041
+ client_secret: clientSecret || ""
595
1042
  });
596
1043
  const fetchOptions = {
597
1044
  method: "POST",
1045
+ redirect: "error",
1046
+ cache: "no-store",
1047
+ signal: AbortSignal.timeout(3e4),
598
1048
  headers: {
599
1049
  "Content-Type": "application/x-www-form-urlencoded"
600
1050
  },
601
1051
  body: body.toString()
602
1052
  };
603
- if (process.env.NODE_ENV !== "production") {
604
- const agent = new https.Agent({
605
- rejectUnauthorized: false
606
- });
607
- fetchOptions.agent = agent;
608
- }
609
- 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);
610
1054
  const tokens = await tokenResponse.json();
611
1055
  if (!tokenResponse.ok) {
612
- console.error("Token exchange failed:", tokens);
1056
+ console.error("Token exchange failed:", tokenResponse.status);
613
1057
  return NextResponse2.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
614
1058
  }
1059
+ options.dpop?.assertTokenResponse(tokens);
615
1060
  const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
616
- const userInfoResponse = await fetch(userInfoEndpoint, {
617
- headers: {
618
- "Authorization": `Bearer ${tokens.access_token}`
619
- },
620
- ...process.env.NODE_ENV !== "production" ? {
621
- agent: new https.Agent({ rejectUnauthorized: false })
622
- } : {}
623
- });
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");
624
1069
  const userInfo = await userInfoResponse.json();
625
1070
  const sessionToken = await encode2({
626
1071
  token: {
@@ -645,6 +1090,14 @@ function createHostedAuthCallback(options = {}) {
645
1090
  // 30 days
646
1091
  path: "/"
647
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
+ }
648
1101
  return redirectResponse;
649
1102
  } catch (error) {
650
1103
  console.error("OAuth callback error:", error);
@@ -652,18 +1105,93 @@ function createHostedAuthCallback(options = {}) {
652
1105
  }
653
1106
  };
654
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
+ };
655
1176
  export {
656
1177
  default2 as NextAuth,
1178
+ OAuth42ClientAssertion,
1179
+ OAuth42DPoP,
657
1180
  OAuth42Provider,
658
1181
  createAuth,
1182
+ createBackendSession,
659
1183
  createHandlers,
660
1184
  createHostedAuthCallback,
661
1185
  createMiddlewareConfig,
662
1186
  getOAuth42Session,
663
1187
  getServerSession,
1188
+ pushAuthorizationURL,
664
1189
  refreshAccessToken,
1190
+ resolveBackendSession,
1191
+ revokeBackendSession,
665
1192
  withOAuth42Auth,
666
1193
  withOAuth42ServerSideProps,
667
- withOAuth42Session
1194
+ withOAuth42Session,
1195
+ withPushedAuthorization
668
1196
  };
669
1197
  //# sourceMappingURL=index.mjs.map