@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.
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __typeError = (msg) => {
9
+ throw TypeError(msg);
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -26,33 +29,121 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
29
  mod
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
33
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
34
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
35
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
29
36
 
30
37
  // src/index.ts
31
38
  var src_exports = {};
32
39
  __export(src_exports, {
40
+ OAuth42ClientAssertion: () => OAuth42ClientAssertion,
41
+ OAuth42DPoP: () => OAuth42DPoP,
33
42
  OAuth42Provider: () => OAuth42Provider,
34
43
  createAuth: () => createAuth,
35
44
  createMiddlewareConfig: () => createMiddlewareConfig,
36
45
  getOAuth42Session: () => getOAuth42Session,
37
46
  getServerSession: () => getServerSession,
47
+ pushAuthorizationURL: () => pushAuthorizationURL,
38
48
  refreshAccessToken: () => refreshAccessToken,
39
49
  withOAuth42Auth: () => withOAuth42Auth,
40
50
  withOAuth42ServerSideProps: () => withOAuth42ServerSideProps,
41
- withOAuth42Session: () => withOAuth42Session
51
+ withOAuth42Session: () => withOAuth42Session,
52
+ withPushedAuthorization: () => withPushedAuthorization
42
53
  });
43
54
  module.exports = __toCommonJS(src_exports);
44
55
 
56
+ // src/server/client-assertion.ts
57
+ var import_node_crypto = require("crypto");
58
+ var _key, _public;
59
+ var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
60
+ constructor(kid, key) {
61
+ __privateAdd(this, _key);
62
+ __privateAdd(this, _public);
63
+ if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
64
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
65
+ throw new Error("Client assertions require a private P-256 key");
66
+ }
67
+ __privateSet(this, _key, key);
68
+ this.kid = kid;
69
+ const publicKey = (0, import_node_crypto.createPublicKey)(key).export({ format: "jwk" });
70
+ __privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
71
+ this.thumbprint = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
72
+ }
73
+ static generate(kid) {
74
+ return new _OAuth42ClientAssertion(kid, (0, import_node_crypto.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
75
+ }
76
+ get publicJwk() {
77
+ return { ...__privateGet(this, _public), kid: this.kid };
78
+ }
79
+ /** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
80
+ get oidcJwks() {
81
+ return { keys: [{
82
+ kty: "EC",
83
+ crv: "P-256",
84
+ x: __privateGet(this, _public).x,
85
+ y: __privateGet(this, _public).y,
86
+ d: __privateGet(this, _key).export({ format: "jwk" }).d,
87
+ kid: this.kid
88
+ }] };
89
+ }
90
+ static validateEndpoint(target) {
91
+ const url = new URL(target);
92
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
93
+ throw new Error("Client authentication requires HTTPS outside loopback development");
94
+ }
95
+ }
96
+ assertion(clientId, audience) {
97
+ _OAuth42ClientAssertion.validateEndpoint(audience);
98
+ if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
99
+ const now = Math.floor(Date.now() / 1e3);
100
+ const header = { alg: "ES256", typ: "JWT", kid: this.kid };
101
+ const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: (0, import_node_crypto.randomBytes)(32).toString("base64url") };
102
+ const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
103
+ return `${data}.${(0, import_node_crypto.sign)("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
104
+ }
105
+ async fetch(clientId, target, init, dpop) {
106
+ if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
107
+ throw new Error("Client assertions require a form POST");
108
+ }
109
+ const prepare = () => {
110
+ const body = new URLSearchParams(init.body);
111
+ body.delete("client_secret");
112
+ body.set("client_id", clientId);
113
+ body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
114
+ body.set("client_assertion", this.assertion(clientId, target));
115
+ const headers = new Headers(init.headers);
116
+ headers.delete("Authorization");
117
+ headers.delete("Content-Length");
118
+ headers.delete("Transfer-Encoding");
119
+ headers.set("Content-Type", "application/x-www-form-urlencoded");
120
+ return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
121
+ };
122
+ return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
123
+ }
124
+ };
125
+ _key = new WeakMap();
126
+ _public = new WeakMap();
127
+ var OAuth42ClientAssertion = _OAuth42ClientAssertion;
128
+
45
129
  // src/provider.ts
46
130
  function OAuth42Provider(options) {
47
131
  const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
48
132
  const baseUrl = issuer.replace(/\/$/, "");
133
+ const { dpop, clientAssertionKey, ...providerOptions } = options;
134
+ if (clientAssertionKey) {
135
+ OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
136
+ providerOptions.clientSecret = void 0;
137
+ }
138
+ if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
49
139
  return {
50
140
  id: "oauth42",
51
141
  name: "OAuth42",
52
142
  type: "oauth",
53
143
  version: "2.0",
54
- // Use OIDC discovery to automatically find endpoints
55
- wellKnown: `${baseUrl}/.well-known/openid-configuration`,
144
+ // Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
145
+ // Constructing its OIDC client from these trusted endpoints preserves issuer binding.
146
+ jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
56
147
  // Use ID token for user profile (standard OIDC pattern)
57
148
  // This avoids an extra HTTP call to /userinfo on every auth flow
58
149
  // Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
@@ -65,17 +156,26 @@ function OAuth42Provider(options) {
65
156
  response_type: "code"
66
157
  }
67
158
  },
68
- token: `${baseUrl}/oauth2/token`,
159
+ token: dpop ? {
160
+ url: `${baseUrl}/oauth2/token`,
161
+ async request({ client, provider, params, checks }) {
162
+ const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
163
+ dpop.assertTokenResponse(tokens);
164
+ return { tokens };
165
+ }
166
+ } : `${baseUrl}/oauth2/token`,
69
167
  userinfo: `${baseUrl}/oauth2/userinfo`,
168
+ ...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
70
169
  client: {
71
170
  id: options.clientId,
72
- secret: options.clientSecret,
73
- token_endpoint_auth_method: "client_secret_post",
171
+ secret: clientAssertionKey ? void 0 : options.clientSecret,
172
+ token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
173
+ ...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
74
174
  id_token_signed_response_alg: "RS256"
75
175
  // OAuth42 uses RS256 for ID tokens
76
176
  },
77
- issuer: baseUrl,
78
- checks: options.pkceEnabled !== false ? ["pkce", "state"] : ["state"],
177
+ issuer,
178
+ checks: ["pkce", "state", "nonce"],
79
179
  profile(profile, tokens) {
80
180
  return {
81
181
  id: profile.sub || profile.id || profile.email,
@@ -91,45 +191,127 @@ function OAuth42Provider(options) {
91
191
  bg: "#1e40af",
92
192
  text: "#ffffff"
93
193
  },
94
- options
194
+ // NextAuth v4 types require a string even when private_key_jwt sends no secret.
195
+ options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
95
196
  };
96
197
  }
97
198
 
199
+ // src/server/par.ts
200
+ async function pushAuthorizationURL(value, config) {
201
+ const base = config.issuer.replace(/\/$/, "");
202
+ let url;
203
+ try {
204
+ url = new URL(value);
205
+ } catch {
206
+ return value;
207
+ }
208
+ if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
209
+ if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
210
+ const body = new URLSearchParams(url.search);
211
+ 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))) {
212
+ throw new Error("Invalid PAR authorization transaction");
213
+ }
214
+ const target = `${base}/oauth2/par`;
215
+ OAuth42ClientAssertion.validateEndpoint(target);
216
+ if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
217
+ const init = {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
220
+ body,
221
+ redirect: "error",
222
+ cache: "no-store",
223
+ signal: AbortSignal.timeout(3e4)
224
+ };
225
+ 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);
226
+ if (response.status !== 201) throw new Error("Pushed authorization failed");
227
+ const pushed = await response.json();
228
+ 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");
229
+ url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
230
+ return url.href;
231
+ }
232
+ function withPushedAuthorization(handler, config) {
233
+ return async (request, context) => {
234
+ if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
235
+ const response = await handler(request, context);
236
+ const location = response.headers.get("Location");
237
+ if (location) {
238
+ const next = await pushAuthorizationURL(location, config);
239
+ if (next === location) return response;
240
+ const headers = new Headers(response.headers);
241
+ headers.set("Location", next);
242
+ headers.delete("content-length");
243
+ return new Response(response.body, { status: response.status, headers });
244
+ }
245
+ if (response.headers.get("content-type")?.includes("application/json")) {
246
+ const body = await response.clone().json();
247
+ if (typeof body?.url === "string") {
248
+ const next = await pushAuthorizationURL(body.url, config);
249
+ if (next !== body.url) {
250
+ const headers = new Headers(response.headers);
251
+ headers.delete("content-length");
252
+ return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
253
+ }
254
+ }
255
+ }
256
+ return response;
257
+ };
258
+ }
259
+
260
+ // src/server/verified-userinfo.ts
261
+ async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
262
+ if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
263
+ try {
264
+ const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
265
+ const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
266
+ const response = await fetcher(target, {
267
+ headers: { Authorization: `Bearer ${accessToken}` },
268
+ redirect: "error",
269
+ cache: "no-store",
270
+ signal: AbortSignal.timeout(3e4)
271
+ });
272
+ if (!response.ok) return null;
273
+ const user = await response.json();
274
+ return parseVerifiedUserInfo(user, subject);
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ function parseVerifiedUserInfo(value, subject) {
280
+ if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
281
+ const user = value;
282
+ if (user.sub !== subject) return null;
283
+ if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
284
+ if (user.email !== void 0 && typeof user.email !== "string") return null;
285
+ if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
286
+ const profile = { sub: user.sub };
287
+ for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
288
+ if (user[field] != null) {
289
+ if (typeof user[field] !== "string") return null;
290
+ profile[field] = user[field];
291
+ }
292
+ }
293
+ if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
294
+ if (user.groups !== void 0) profile.groups = user.groups;
295
+ return profile;
296
+ }
297
+ async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
298
+ if (typeof session.access_token !== "string" || !session.access_token) return null;
299
+ if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
300
+ return parseVerifiedUserInfo(session.user_info, subject);
301
+ }
302
+ return verifiedUserInfo(issuer, session.access_token, subject, dpop);
303
+ }
304
+
98
305
  // src/server/auth.ts
99
306
  var import_next_auth2 = __toESM(require("next-auth"));
100
307
 
101
308
  // src/server/session.ts
102
309
  var import_next_auth = require("next-auth");
103
- var import_headers = require("next/headers");
104
- var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
105
310
  async function getOAuth42Session(...args) {
106
311
  const session = await (0, import_next_auth.getServerSession)(...args);
107
- if (!session) {
312
+ if (!session || session.error) {
108
313
  return null;
109
314
  }
110
- let refreshedToken = null;
111
- try {
112
- if (args.length === 1) {
113
- const headersList = await (0, import_headers.headers)();
114
- refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
115
- } else if (args.length === 3) {
116
- const req = args[0];
117
- refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
118
- }
119
- } catch {
120
- }
121
- if (refreshedToken) {
122
- const sessionToken = session.accessToken;
123
- console.log("[OAuth42 Session] Using refreshed token from middleware:", {
124
- refreshedPrefix: refreshedToken.substring(0, 20),
125
- sessionPrefix: sessionToken?.substring(0, 20),
126
- tokensMatch: refreshedToken === sessionToken
127
- });
128
- return {
129
- ...session,
130
- accessToken: refreshedToken
131
- };
132
- }
133
315
  return session;
134
316
  }
135
317
  function withOAuth42Session(handler, authOptions) {
@@ -160,13 +342,66 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
160
342
  };
161
343
  }
162
344
 
345
+ // src/server/backend-session.ts
346
+ function isBackendSessionMode(mode) {
347
+ return mode === "backend";
348
+ }
349
+ function sessionEndpoint(issuer) {
350
+ return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
351
+ }
352
+ async function sessionFetch(params, target, body, credential, method = "POST") {
353
+ OAuth42ClientAssertion.validateEndpoint(target);
354
+ const prepare = () => ({
355
+ method,
356
+ redirect: "error",
357
+ cache: "no-store",
358
+ signal: AbortSignal.timeout(3e4),
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify({
361
+ ...body,
362
+ client_id: params.clientId,
363
+ ...params.clientAssertionKey ? {
364
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
365
+ client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
366
+ } : { client_secret: params.clientSecret }
367
+ })
368
+ });
369
+ return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
370
+ }
371
+ async function sessionResponse(response, params) {
372
+ if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
373
+ const session = await response.json();
374
+ if (params.dpop) params.dpop.assertTokenResponse(session);
375
+ else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
376
+ return session;
377
+ }
378
+ async function createBackendSession(params) {
379
+ return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
380
+ access_token: params.accessToken,
381
+ refresh_token: params.refreshToken,
382
+ id_token: params.idToken,
383
+ user: params.user
384
+ }, params.accessToken), params);
385
+ }
386
+ async function resolveBackendSession(params) {
387
+ const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
388
+ if ([401, 403, 404].includes(response.status)) return null;
389
+ return sessionResponse(response, params);
390
+ }
391
+ async function revokeBackendSession(params) {
392
+ const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
393
+ if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
394
+ }
395
+
163
396
  // src/server/auth.ts
164
397
  var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
165
- var activeRefresh = null;
398
+ var activeRefreshes = /* @__PURE__ */ new Map();
166
399
  function createAuth(options = {}) {
167
400
  const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
168
- const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
169
- if (!clientId || !clientSecret) {
401
+ const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
402
+ const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
403
+ const useBackendSessions = isBackendSessionMode(options.sessionStore);
404
+ if (!clientId || !clientSecret && !options.clientAssertionKey) {
170
405
  throw new Error(
171
406
  "OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
172
407
  );
@@ -175,24 +410,57 @@ function createAuth(options = {}) {
175
410
  providers: [
176
411
  OAuth42Provider({
177
412
  clientId,
178
- clientSecret,
179
- issuer: options.issuer,
413
+ clientSecret: clientSecret || "",
414
+ issuer,
180
415
  scopes: options.scopes,
181
- pkceEnabled: options.pkceEnabled
416
+ pkceEnabled: options.pkceEnabled,
417
+ dpop: options.dpop,
418
+ clientAssertionKey: options.clientAssertionKey
182
419
  })
183
420
  ],
184
421
  callbacks: {
185
422
  async jwt({ token, account, profile }) {
423
+ if (options.clientAssertionKey) delete token.clientSecret;
186
424
  console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
187
425
  if (account) {
188
- console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
189
- token.accessToken = account.access_token;
190
- token.refreshToken = account.refresh_token;
191
- token.expiresAt = account.expires_at;
192
- token.idToken = account.id_token;
193
- token.clientId = clientId;
194
- token.clientSecret = clientSecret;
195
- token.issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER;
426
+ if (useBackendSessions) {
427
+ if (!account.access_token || !account.refresh_token) {
428
+ throw new Error("OAuth42 backend sessions require access and refresh tokens");
429
+ }
430
+ const oauth42Profile = profile;
431
+ const backendSession = await createBackendSession({
432
+ dpop: options.dpop,
433
+ clientAssertionKey: options.clientAssertionKey,
434
+ issuer,
435
+ clientId,
436
+ clientSecret: clientSecret || "",
437
+ accessToken: account.access_token,
438
+ refreshToken: account.refresh_token,
439
+ expiresAt: account.expires_at,
440
+ idToken: account.id_token,
441
+ user: {
442
+ sub: oauth42Profile?.sub,
443
+ email: oauth42Profile?.email,
444
+ username: oauth42Profile?.username,
445
+ name: token.name,
446
+ emailVerified: oauth42Profile?.email_verified,
447
+ groups: oauth42Profile?.groups
448
+ }
449
+ });
450
+ token.oauth42SessionId = backendSession.session_id;
451
+ token.expiresAt = backendSession.expires_at;
452
+ token.issuer = issuer;
453
+ } else {
454
+ console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
455
+ token.accessToken = account.access_token;
456
+ token.refreshToken = account.refresh_token;
457
+ token.expiresAt = account.expires_at;
458
+ token.idToken = account.id_token;
459
+ token.clientId = clientId;
460
+ if (options.clientAssertionKey) delete token.clientSecret;
461
+ else token.clientSecret = clientSecret;
462
+ token.issuer = issuer;
463
+ }
196
464
  }
197
465
  if (profile) {
198
466
  const oauth42Profile = profile;
@@ -209,12 +477,68 @@ function createAuth(options = {}) {
209
477
  },
210
478
  async session({ session, token }) {
211
479
  console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
212
- session.accessToken = token.accessToken;
213
- session.idToken = token.idToken;
480
+ const revokedSession = () => {
481
+ session.error = "SessionRevoked";
482
+ delete session.accessToken;
483
+ delete session.idToken;
484
+ delete session.userInfo;
485
+ delete session.user;
486
+ return session;
487
+ };
488
+ if (token.error) return revokedSession();
489
+ let verified;
490
+ if (useBackendSessions) {
491
+ const sessionId = token.oauth42SessionId;
492
+ if (!sessionId) {
493
+ return revokedSession();
494
+ }
495
+ const backendSession = await resolveBackendSession({
496
+ dpop: options.dpop,
497
+ clientAssertionKey: options.clientAssertionKey,
498
+ issuer,
499
+ clientId,
500
+ clientSecret: clientSecret || "",
501
+ sessionId
502
+ });
503
+ if (!backendSession) {
504
+ return revokedSession();
505
+ }
506
+ session.accessToken = backendSession.access_token;
507
+ session.idToken = backendSession.id_token;
508
+ token.expiresAt = backendSession.expires_at;
509
+ verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
510
+ if (backendSession.user && session.user) {
511
+ session.user.email = backendSession.user.email;
512
+ session.user.name = backendSession.user.name;
513
+ session.user.username = backendSession.user.username;
514
+ session.user.emailVerified = backendSession.user.emailVerified;
515
+ session.user.groups = backendSession.user.groups;
516
+ }
517
+ } else {
518
+ session.accessToken = token.accessToken;
519
+ session.idToken = token.idToken;
520
+ verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
521
+ }
522
+ if (!verified) {
523
+ return revokedSession();
524
+ }
525
+ session.userInfo = verified;
526
+ token.email = verified.email;
527
+ token.name = verified.name;
528
+ token.username = verified.preferred_username ?? verified.username;
529
+ token.emailVerified = verified.email_verified;
530
+ token.groups = verified.groups ?? [];
531
+ if (session.user) {
532
+ session.user.email = verified.email;
533
+ session.user.name = verified.name;
534
+ session.user.username = verified.preferred_username ?? verified.username;
535
+ session.user.emailVerified = verified.email_verified;
536
+ session.user.groups = verified.groups ?? [];
537
+ }
214
538
  if (token.error) {
215
539
  session.error = token.error;
216
540
  }
217
- if (session.user) {
541
+ if (!useBackendSessions && session.user) {
218
542
  session.user.email = token.email;
219
543
  session.user.name = token.name;
220
544
  session.user.username = token.username;
@@ -238,6 +562,28 @@ function createAuth(options = {}) {
238
562
  strategy: "jwt",
239
563
  ...options.session
240
564
  },
565
+ events: {
566
+ async signOut(message) {
567
+ if (useBackendSessions) {
568
+ const sessionId = message.token?.oauth42SessionId;
569
+ if (sessionId) {
570
+ await revokeBackendSession({
571
+ dpop: options.dpop,
572
+ clientAssertionKey: options.clientAssertionKey,
573
+ issuer,
574
+ clientId,
575
+ clientSecret: clientSecret || "",
576
+ sessionId
577
+ }).catch((error) => {
578
+ console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
579
+ });
580
+ }
581
+ }
582
+ if (options.events?.signOut) {
583
+ await options.events.signOut(message);
584
+ }
585
+ }
586
+ },
241
587
  debug: options.debug || process.env.NODE_ENV === "development",
242
588
  secret: process.env.NEXTAUTH_SECRET,
243
589
  // Configure unique cookie names per app to prevent session conflicts on localhost
@@ -313,31 +659,48 @@ function createAuth(options = {}) {
313
659
  }
314
660
  }
315
661
  };
316
- const handler = NextAuth(authOptions);
662
+ const nextAuthHandler = NextAuth(authOptions);
663
+ const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
317
664
  return {
318
665
  auth: authOptions,
319
666
  handlers: { GET: handler, POST: handler }
320
667
  };
321
668
  }
322
669
  var getServerSession = getOAuth42Session;
323
- async function refreshAccessToken(token, clientId, clientSecret, issuer) {
324
- if (activeRefresh) {
325
- console.log("[OAuth42] Refresh already in progress, waiting...");
326
- return await activeRefresh;
670
+ async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
671
+ if (typeof token.refreshToken !== "string" || !token.refreshToken) {
672
+ return { ...token, error: "RefreshAccessTokenError" };
673
+ }
674
+ 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]);
675
+ let refresh = activeRefreshes.get(key);
676
+ if (!refresh) {
677
+ refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
678
+ activeRefreshes.set(key, refresh);
327
679
  }
328
- activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
329
680
  try {
330
- return await activeRefresh;
681
+ const result = await refresh;
682
+ const updated = {
683
+ ...token,
684
+ accessToken: result.accessToken,
685
+ refreshToken: result.refreshToken,
686
+ expiresAt: result.expiresAt,
687
+ error: result.error
688
+ };
689
+ if (clientAssertionKey) delete updated.clientSecret;
690
+ return updated;
331
691
  } finally {
332
- activeRefresh = null;
692
+ if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
333
693
  }
334
694
  }
335
- async function doRefresh(token, clientId, clientSecret, issuer) {
695
+ async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
336
696
  try {
337
697
  const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
338
698
  const tokenUrl = `${baseUrl}/oauth2/token`;
339
699
  const fetchOptions = {
340
700
  method: "POST",
701
+ redirect: "error",
702
+ cache: "no-store",
703
+ signal: AbortSignal.timeout(3e4),
341
704
  headers: {
342
705
  "Content-Type": "application/x-www-form-urlencoded"
343
706
  },
@@ -348,17 +711,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
348
711
  client_secret: clientSecret
349
712
  })
350
713
  };
351
- if (process.env.NODE_ENV !== "production" && tokenUrl.startsWith("https://")) {
352
- const https = await import("https");
353
- fetchOptions.agent = new https.Agent({
354
- rejectUnauthorized: false
355
- });
356
- }
357
- const response = await fetch(tokenUrl, fetchOptions);
714
+ const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
358
715
  const refreshedTokens = await response.json();
359
716
  if (!response.ok) {
360
717
  throw refreshedTokens;
361
718
  }
719
+ dpop?.assertTokenResponse(refreshedTokens);
362
720
  console.log("[OAuth42] Token refreshed successfully");
363
721
  return {
364
722
  ...token,
@@ -370,7 +728,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
370
728
  error: void 0
371
729
  };
372
730
  } catch (error) {
373
- console.error("[OAuth42] Failed to refresh access token:", error);
731
+ console.error("[OAuth42] Failed to refresh access token");
374
732
  return {
375
733
  ...token,
376
734
  error: "RefreshAccessTokenError"
@@ -384,12 +742,37 @@ var import_jwt = require("next-auth/jwt");
384
742
  var ALLOWED_COOKIE_SIZE = 4096;
385
743
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
386
744
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
745
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
387
746
  var pendingRefreshes = /* @__PURE__ */ new Map();
388
- async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
747
+ function forwardedHeaders(req, cookieName, jwt) {
748
+ const headers = new Headers(req.headers);
749
+ headers.delete("x-oauth42-refreshed-token");
750
+ if (cookieName && jwt) {
751
+ const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
752
+ if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
753
+ else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
754
+ cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
755
+ }
756
+ headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
757
+ }
758
+ return headers;
759
+ }
760
+ function resolveRefreshConfig(options, token) {
761
+ return {
762
+ clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
763
+ clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
764
+ issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
765
+ };
766
+ }
767
+ async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
389
768
  try {
390
769
  const tokenUrl = `${issuer}/oauth2/token`;
391
- const response = await fetch(tokenUrl, {
770
+ const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
771
+ const response = await fetcher(tokenUrl, {
392
772
  method: "POST",
773
+ redirect: "error",
774
+ cache: "no-store",
775
+ signal: AbortSignal.timeout(3e4),
393
776
  headers: {
394
777
  "Content-Type": "application/x-www-form-urlencoded"
395
778
  },
@@ -402,9 +785,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
402
785
  });
403
786
  const data = await response.json();
404
787
  if (!response.ok) {
405
- console.error("[OAuth42 Middleware] Token refresh failed:", data);
788
+ console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
406
789
  return { success: false, error: data.error || "refresh_failed" };
407
790
  }
791
+ dpop?.assertTokenResponse(data);
408
792
  console.log("[OAuth42 Middleware] Token refreshed successfully");
409
793
  return {
410
794
  success: true,
@@ -413,17 +797,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
413
797
  expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
414
798
  };
415
799
  } catch (error) {
416
- console.error("[OAuth42 Middleware] Token refresh error:", error);
800
+ console.error("[OAuth42 Middleware] Token refresh request failed");
417
801
  return { success: false, error: "refresh_error" };
418
802
  }
419
803
  }
420
804
  function withOAuth42Auth(options = {}) {
421
805
  const secret = process.env.NEXTAUTH_SECRET;
422
- const clientId = process.env.OAUTH42_CLIENT_ID;
423
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
424
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
425
806
  if (!secret) {
426
- console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
807
+ throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
427
808
  }
428
809
  return async function middleware(req) {
429
810
  const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
@@ -434,18 +815,76 @@ function withOAuth42Auth(options = {}) {
434
815
  });
435
816
  const pathname = req.nextUrl.pathname;
436
817
  if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
437
- return import_server.NextResponse.next();
818
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
438
819
  }
439
820
  const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
440
821
  if (!needsProtection) {
441
- return import_server.NextResponse.next();
822
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
442
823
  }
443
- if (!token) {
824
+ if (!token || token.error) {
444
825
  const signInUrl = options.pages?.signIn || "/auth/signin";
445
826
  const url = new URL(signInUrl, req.url);
446
827
  url.searchParams.set("callbackUrl", pathname);
447
828
  return import_server.NextResponse.redirect(url);
448
829
  }
830
+ const refreshConfig = resolveRefreshConfig(options, token);
831
+ const denied = () => {
832
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
833
+ url.searchParams.set("callbackUrl", pathname);
834
+ const response = import_server.NextResponse.redirect(url);
835
+ for (const cookie of req.cookies.getAll()) {
836
+ if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
837
+ }
838
+ return response;
839
+ };
840
+ const verifyAndAuthorize = async (current, resolved) => {
841
+ const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
842
+ if (!user) return false;
843
+ current.groups = user.groups ?? [];
844
+ current.email = user.email;
845
+ current.name = user.name;
846
+ current.username = user.preferred_username ?? user.username;
847
+ current.emailVerified = user.email_verified;
848
+ return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
849
+ };
850
+ if (isBackendSessionMode(options.sessionStore)) {
851
+ const sessionId = token.oauth42SessionId;
852
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
853
+ const signInUrl = options.pages?.signIn || "/auth/signin";
854
+ const url = new URL(signInUrl, req.url);
855
+ url.searchParams.set("callbackUrl", pathname);
856
+ const response = import_server.NextResponse.redirect(url);
857
+ response.cookies.delete(cookieName);
858
+ return response;
859
+ }
860
+ const backendSession = await resolveBackendSession({
861
+ dpop: options.dpop,
862
+ clientAssertionKey: options.clientAssertionKey,
863
+ issuer: refreshConfig.issuer,
864
+ clientId: refreshConfig.clientId,
865
+ clientSecret: refreshConfig.clientSecret || "",
866
+ sessionId
867
+ }).catch((error) => {
868
+ console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
869
+ return null;
870
+ });
871
+ if (!backendSession) {
872
+ const signInUrl = options.pages?.signIn || "/auth/signin";
873
+ const url = new URL(signInUrl, req.url);
874
+ url.searchParams.set("callbackUrl", pathname);
875
+ const response = import_server.NextResponse.redirect(url);
876
+ response.cookies.delete(cookieName);
877
+ return response;
878
+ }
879
+ const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
880
+ if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
881
+ const requestHeaders = forwardedHeaders(req);
882
+ return import_server.NextResponse.next({
883
+ request: {
884
+ headers: requestHeaders
885
+ }
886
+ });
887
+ }
449
888
  const expiresAt = token.expiresAt;
450
889
  const now = Math.floor(Date.now() / 1e3);
451
890
  const bufferSeconds = 15;
@@ -455,26 +894,30 @@ function withOAuth42Auth(options = {}) {
455
894
  expiresAt,
456
895
  now,
457
896
  expiredAgo: expiresAt ? now - expiresAt : "n/a",
458
- accessTokenPrefix: token.accessToken?.substring(0, 20),
459
897
  path: pathname
460
898
  });
461
899
  }
462
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
463
- const userId = token.sub;
464
- let refreshPromise = pendingRefreshes.get(userId);
900
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
901
+ const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
902
+ let refreshPromise = pendingRefreshes.get(grantKey);
465
903
  if (refreshPromise) {
466
904
  console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
467
905
  } else {
468
906
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
469
907
  refreshPromise = refreshTokens(
470
908
  token.refreshToken,
471
- clientId,
472
- clientSecret,
473
- issuer
909
+ refreshConfig.clientId,
910
+ refreshConfig.clientSecret || "",
911
+ refreshConfig.issuer,
912
+ options.dpop,
913
+ options.clientAssertionKey
474
914
  ).finally(() => {
475
- setTimeout(() => pendingRefreshes.delete(userId), 3e4);
915
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
916
+ if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
917
+ cleanupTimer.unref();
918
+ }
476
919
  });
477
- pendingRefreshes.set(userId, refreshPromise);
920
+ pendingRefreshes.set(grantKey, refreshPromise);
478
921
  }
479
922
  const refreshed = await refreshPromise;
480
923
  if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
@@ -484,12 +927,13 @@ function withOAuth42Auth(options = {}) {
484
927
  refreshToken: refreshed.refreshToken,
485
928
  expiresAt: refreshed.expiresAt
486
929
  };
930
+ if (options.clientAssertionKey) delete updatedToken.clientSecret;
931
+ if (!await verifyAndAuthorize(updatedToken)) return denied();
487
932
  const newJwt = await (0, import_jwt.encode)({
488
933
  token: updatedToken,
489
934
  secret
490
935
  });
491
- const requestHeaders = new Headers(req.headers);
492
- requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
936
+ const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
493
937
  const response = import_server.NextResponse.next({
494
938
  request: {
495
939
  headers: requestHeaders
@@ -499,7 +943,8 @@ function withOAuth42Auth(options = {}) {
499
943
  httpOnly: true,
500
944
  sameSite: "lax",
501
945
  path: "/",
502
- secure: process.env.NODE_ENV === "production"
946
+ secure: process.env.NODE_ENV === "production",
947
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
503
948
  };
504
949
  if (newJwt.length <= CHUNK_SIZE) {
505
950
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -541,13 +986,12 @@ function withOAuth42Auth(options = {}) {
541
986
  chunked: newJwt.length > CHUNK_SIZE,
542
987
  existingBaseCooke: req.cookies.has(cookieName),
543
988
  existingChunks: existingChunks.length > 0 ? existingChunks : "none",
544
- newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
545
989
  newExpiresAt: refreshed.expiresAt,
546
990
  cookieName
547
991
  });
548
992
  return response;
549
993
  } else {
550
- console.log("[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), clearing session");
994
+ console.log("[OAuth42 Middleware] Refresh failed, clearing session");
551
995
  const signInUrl = options.pages?.signIn || "/auth/signin";
552
996
  const url = new URL(signInUrl, req.url);
553
997
  url.searchParams.set("callbackUrl", pathname);
@@ -564,16 +1008,13 @@ function withOAuth42Auth(options = {}) {
564
1008
  return response;
565
1009
  }
566
1010
  }
567
- if (options.callbacks?.authorized) {
568
- const isAuthorized = await options.callbacks.authorized({ token, req });
569
- if (!isAuthorized) {
570
- const signInUrl = options.pages?.signIn || "/auth/signin";
571
- const url = new URL(signInUrl, req.url);
572
- url.searchParams.set("callbackUrl", pathname);
573
- return import_server.NextResponse.redirect(url);
574
- }
1011
+ if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
1012
+ const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
1013
+ url.searchParams.set("callbackUrl", pathname);
1014
+ return import_server.NextResponse.redirect(url);
575
1015
  }
576
- return import_server.NextResponse.next();
1016
+ if (!await verifyAndAuthorize(token)) return denied();
1017
+ return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
577
1018
  };
578
1019
  }
579
1020
  function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
@@ -592,16 +1033,88 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
592
1033
  publicPaths
593
1034
  };
594
1035
  }
1036
+
1037
+ // src/server/dpop.ts
1038
+ var import_node_crypto2 = require("crypto");
1039
+ var OAuth42DPoP = class _OAuth42DPoP {
1040
+ constructor(key) {
1041
+ this.nonces = /* @__PURE__ */ new Map();
1042
+ if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
1043
+ throw new Error("DPoP requires a private P-256 key");
1044
+ }
1045
+ this.key = key;
1046
+ const publicKey = (0, import_node_crypto2.createPublicKey)(key).export({ format: "jwk" });
1047
+ this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
1048
+ this.thumbprint = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
1049
+ }
1050
+ /** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
1051
+ static generate() {
1052
+ return new _OAuth42DPoP((0, import_node_crypto2.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
1053
+ }
1054
+ /** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
1055
+ get oidcKey() {
1056
+ return this.key;
1057
+ }
1058
+ proof(method, target, nonce, accessToken) {
1059
+ const url = new URL(target);
1060
+ if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
1061
+ throw new Error("DPoP requires HTTPS outside loopback development");
1062
+ }
1063
+ if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
1064
+ url.search = "";
1065
+ url.hash = "";
1066
+ const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
1067
+ const claims = {
1068
+ htm: method.toUpperCase(),
1069
+ htu: url.href,
1070
+ iat: Math.floor(Date.now() / 1e3),
1071
+ jti: (0, import_node_crypto2.randomBytes)(32).toString("base64url"),
1072
+ ...nonce ? { nonce } : {},
1073
+ ...accessToken !== void 0 ? { ath: (0, import_node_crypto2.createHash)("sha256").update(accessToken).digest("base64url") } : {}
1074
+ };
1075
+ const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
1076
+ return `${input}.${(0, import_node_crypto2.sign)("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
1077
+ }
1078
+ assertTokenResponse(tokens) {
1079
+ if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
1080
+ throw new Error("Server did not issue the requested DPoP-bound token");
1081
+ }
1082
+ }
1083
+ async fetch(target, init = {}, accessToken, prepare) {
1084
+ if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
1085
+ const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
1086
+ for (let attempt = 0; attempt < 2; attempt++) {
1087
+ const request = prepare ? prepare() : init;
1088
+ const headers = new Headers(request.headers);
1089
+ headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
1090
+ if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
1091
+ const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
1092
+ const nonce = response.headers.get("DPoP-Nonce");
1093
+ if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
1094
+ if (this.nonces.size >= 256) this.nonces.clear();
1095
+ this.nonces.set(cacheKey, nonce);
1096
+ } else return response;
1097
+ if (attempt || ![400, 401].includes(response.status)) return response;
1098
+ const error = await response.clone().json().catch(() => null);
1099
+ if (error?.error !== "use_dpop_nonce") return response;
1100
+ }
1101
+ throw new Error("DPoP nonce negotiation failed");
1102
+ }
1103
+ };
595
1104
  // Annotate the CommonJS export names for ESM import in node:
596
1105
  0 && (module.exports = {
1106
+ OAuth42ClientAssertion,
1107
+ OAuth42DPoP,
597
1108
  OAuth42Provider,
598
1109
  createAuth,
599
1110
  createMiddlewareConfig,
600
1111
  getOAuth42Session,
601
1112
  getServerSession,
1113
+ pushAuthorizationURL,
602
1114
  refreshAccessToken,
603
1115
  withOAuth42Auth,
604
1116
  withOAuth42ServerSideProps,
605
- withOAuth42Session
1117
+ withOAuth42Session,
1118
+ withPushedAuthorization
606
1119
  });
607
1120
  //# sourceMappingURL=index.js.map