@oauth42/next 0.4.7 → 0.5.0

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,6 +1,6 @@
1
- export { C as CreateAuthOptions, a as OAuth42Provider, c as createAuth, f as createHandlers, g as getOAuth42Session, d as getServerSession, r as refreshAccessToken, w as withOAuth42ServerSideProps, e as withOAuth42Session } from '../auth-BThRQMzh.js';
1
+ export { C as CreateAuthOptions, a as OAuth42Provider, c as createAuth, f as createHandlers, g as getOAuth42Session, d as getServerSession, r as refreshAccessToken, w as withOAuth42ServerSideProps, e as withOAuth42Session } from '../auth-CYMPe3k0.js';
2
2
  export { default as NextAuth, NextAuthOptions } from 'next-auth';
3
- export { OAuth42AuthOptions, createMiddlewareConfig, withOAuth42Auth } from '../middleware/index.js';
3
+ export { B as BackendSessionConfig, a as BackendSessionResponse, b as BackendSessionUser, O as OAuth42AuthOptions, S as SessionStoreMode, d as createBackendSession, c as createMiddlewareConfig, r as resolveBackendSession, e as revokeBackendSession, w as withOAuth42Auth } from '../index-Cfqxl9r2.js';
4
4
  import { NextRequest, NextResponse } from 'next/server';
5
5
  import 'next-auth/providers/oauth';
6
6
  import 'next';
@@ -33,12 +33,15 @@ __export(server_exports, {
33
33
  NextAuth: () => import_next_auth3.default,
34
34
  OAuth42Provider: () => OAuth42Provider,
35
35
  createAuth: () => createAuth,
36
+ createBackendSession: () => createBackendSession,
36
37
  createHandlers: () => createHandlers,
37
38
  createHostedAuthCallback: () => createHostedAuthCallback,
38
39
  createMiddlewareConfig: () => createMiddlewareConfig,
39
40
  getOAuth42Session: () => getOAuth42Session,
40
41
  getServerSession: () => getServerSession,
41
42
  refreshAccessToken: () => refreshAccessToken,
43
+ resolveBackendSession: () => resolveBackendSession,
44
+ revokeBackendSession: () => revokeBackendSession,
42
45
  withOAuth42Auth: () => withOAuth42Auth,
43
46
  withOAuth42ServerSideProps: () => withOAuth42ServerSideProps,
44
47
  withOAuth42Session: () => withOAuth42Session
@@ -163,12 +166,72 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
163
166
  };
164
167
  }
165
168
 
169
+ // src/server/backend-session.ts
170
+ function isBackendSessionMode(mode) {
171
+ return mode === "backend";
172
+ }
173
+ function sessionEndpoint(issuer) {
174
+ return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
175
+ }
176
+ async function createBackendSession(params) {
177
+ const response = await fetch(sessionEndpoint(params.issuer), {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify({
181
+ access_token: params.accessToken,
182
+ refresh_token: params.refreshToken,
183
+ expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
184
+ id_token: params.idToken,
185
+ client_id: params.clientId,
186
+ client_secret: params.clientSecret,
187
+ issuer: params.issuer,
188
+ user: params.user
189
+ })
190
+ });
191
+ if (!response.ok) {
192
+ throw new Error(`OAuth42 backend session create failed: ${response.status}`);
193
+ }
194
+ return response.json();
195
+ }
196
+ async function resolveBackendSession(params) {
197
+ const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
198
+ method: "POST",
199
+ headers: { "Content-Type": "application/json" },
200
+ body: JSON.stringify({
201
+ session_id: params.sessionId,
202
+ client_id: params.clientId,
203
+ client_secret: params.clientSecret,
204
+ issuer: params.issuer
205
+ })
206
+ });
207
+ if (response.status === 401 || response.status === 403 || response.status === 404) {
208
+ return null;
209
+ }
210
+ if (!response.ok) {
211
+ throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
212
+ }
213
+ return response.json();
214
+ }
215
+ async function revokeBackendSession(params) {
216
+ await fetch(sessionEndpoint(params.issuer), {
217
+ method: "DELETE",
218
+ headers: { "Content-Type": "application/json" },
219
+ body: JSON.stringify({
220
+ session_id: params.sessionId,
221
+ client_id: params.clientId,
222
+ client_secret: params.clientSecret
223
+ })
224
+ });
225
+ }
226
+
166
227
  // src/server/auth.ts
167
228
  var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
168
229
  var activeRefresh = null;
169
230
  function createAuth(options = {}) {
170
231
  const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
171
232
  const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
233
+ const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
234
+ const useBackendSessions = isBackendSessionMode(options.sessionStore);
172
235
  if (!clientId || !clientSecret) {
173
236
  throw new Error(
174
237
  "OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
@@ -179,7 +242,7 @@ function createAuth(options = {}) {
179
242
  OAuth42Provider({
180
243
  clientId,
181
244
  clientSecret,
182
- issuer: options.issuer,
245
+ issuer,
183
246
  scopes: options.scopes,
184
247
  pkceEnabled: options.pkceEnabled
185
248
  })
@@ -188,14 +251,41 @@ function createAuth(options = {}) {
188
251
  async jwt({ token, account, profile }) {
189
252
  console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
190
253
  if (account) {
191
- console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
192
- token.accessToken = account.access_token;
193
- token.refreshToken = account.refresh_token;
194
- token.expiresAt = account.expires_at;
195
- token.idToken = account.id_token;
196
- token.clientId = clientId;
197
- token.clientSecret = clientSecret;
198
- token.issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER;
254
+ if (useBackendSessions) {
255
+ if (!account.access_token || !account.refresh_token) {
256
+ throw new Error("OAuth42 backend sessions require access and refresh tokens");
257
+ }
258
+ const oauth42Profile = profile;
259
+ const backendSession = await createBackendSession({
260
+ issuer,
261
+ clientId,
262
+ clientSecret,
263
+ accessToken: account.access_token,
264
+ refreshToken: account.refresh_token,
265
+ expiresAt: account.expires_at,
266
+ idToken: account.id_token,
267
+ user: {
268
+ sub: oauth42Profile?.sub,
269
+ email: oauth42Profile?.email,
270
+ username: oauth42Profile?.username,
271
+ name: token.name,
272
+ emailVerified: oauth42Profile?.email_verified,
273
+ groups: oauth42Profile?.groups
274
+ }
275
+ });
276
+ token.oauth42SessionId = backendSession.session_id;
277
+ token.expiresAt = backendSession.expires_at;
278
+ token.issuer = issuer;
279
+ } else {
280
+ console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
281
+ token.accessToken = account.access_token;
282
+ token.refreshToken = account.refresh_token;
283
+ token.expiresAt = account.expires_at;
284
+ token.idToken = account.id_token;
285
+ token.clientId = clientId;
286
+ token.clientSecret = clientSecret;
287
+ token.issuer = issuer;
288
+ }
199
289
  }
200
290
  if (profile) {
201
291
  const oauth42Profile = profile;
@@ -212,12 +302,40 @@ function createAuth(options = {}) {
212
302
  },
213
303
  async session({ session, token }) {
214
304
  console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
215
- session.accessToken = token.accessToken;
216
- session.idToken = token.idToken;
305
+ if (useBackendSessions) {
306
+ const sessionId = token.oauth42SessionId;
307
+ if (!sessionId) {
308
+ session.error = "SessionRevoked";
309
+ return session;
310
+ }
311
+ const backendSession = await resolveBackendSession({
312
+ issuer,
313
+ clientId,
314
+ clientSecret,
315
+ sessionId
316
+ });
317
+ if (!backendSession) {
318
+ session.error = "SessionRevoked";
319
+ return session;
320
+ }
321
+ session.accessToken = backendSession.access_token;
322
+ session.idToken = backendSession.id_token;
323
+ token.expiresAt = backendSession.expires_at;
324
+ if (backendSession.user && session.user) {
325
+ session.user.email = backendSession.user.email;
326
+ session.user.name = backendSession.user.name;
327
+ session.user.username = backendSession.user.username;
328
+ session.user.emailVerified = backendSession.user.emailVerified;
329
+ session.user.groups = backendSession.user.groups;
330
+ }
331
+ } else {
332
+ session.accessToken = token.accessToken;
333
+ session.idToken = token.idToken;
334
+ }
217
335
  if (token.error) {
218
336
  session.error = token.error;
219
337
  }
220
- if (session.user) {
338
+ if (!useBackendSessions && session.user) {
221
339
  session.user.email = token.email;
222
340
  session.user.name = token.name;
223
341
  session.user.username = token.username;
@@ -241,6 +359,26 @@ function createAuth(options = {}) {
241
359
  strategy: "jwt",
242
360
  ...options.session
243
361
  },
362
+ events: {
363
+ async signOut(message) {
364
+ if (useBackendSessions) {
365
+ const sessionId = message.token?.oauth42SessionId;
366
+ if (sessionId) {
367
+ await revokeBackendSession({
368
+ issuer,
369
+ clientId,
370
+ clientSecret,
371
+ sessionId
372
+ }).catch((error) => {
373
+ console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
374
+ });
375
+ }
376
+ }
377
+ if (options.events?.signOut) {
378
+ await options.events.signOut(message);
379
+ }
380
+ }
381
+ },
244
382
  debug: options.debug || process.env.NODE_ENV === "development",
245
383
  secret: process.env.NEXTAUTH_SECRET,
246
384
  // Configure unique cookie names per app to prevent session conflicts on localhost
@@ -273,7 +411,11 @@ function createAuth(options = {}) {
273
411
  secure: process.env.NODE_ENV === "production"
274
412
  }
275
413
  },
276
- // PKCE code_verifier cookie - essential for PKCE flow
414
+ // PKCE code_verifier cookie - essential for PKCE flow.
415
+ // TTL covers the full OAuth round-trip including user-in-the-loop steps
416
+ // like passkey ceremonies, MFA enrollment, and any debugging pauses.
417
+ // The previous 15-minute value was too tight for realistic ceremony-
418
+ // heavy flows — see #524 for the incident where state expired mid-debug.
277
419
  pkceCodeVerifier: {
278
420
  name: `${options.cookiePrefix}.pkce.code_verifier`,
279
421
  options: {
@@ -281,11 +423,13 @@ function createAuth(options = {}) {
281
423
  sameSite: "lax",
282
424
  path: "/",
283
425
  secure: process.env.NODE_ENV === "production",
284
- maxAge: 900
285
- // 15 minutes
426
+ maxAge: 3600
427
+ // 1 hour (was 900 — 15 min was too short for passkey/MFA flows)
286
428
  }
287
429
  },
288
- // State cookie for OAuth CSRF protection
430
+ // State cookie for OAuth CSRF protection. Same TTL rationale as
431
+ // pkceCodeVerifier above. State is single-use CSRF and cannot be
432
+ // replayed, so a longer TTL does not weaken the security story.
289
433
  state: {
290
434
  name: `${options.cookiePrefix}.state`,
291
435
  options: {
@@ -293,8 +437,8 @@ function createAuth(options = {}) {
293
437
  sameSite: "lax",
294
438
  path: "/",
295
439
  secure: process.env.NODE_ENV === "production",
296
- maxAge: 900
297
- // 15 minutes
440
+ maxAge: 3600
441
+ // 1 hour (was 900 — 15 min was too short for passkey/MFA flows)
298
442
  }
299
443
  },
300
444
  // Nonce cookie for OpenID Connect
@@ -388,7 +532,22 @@ var import_jwt = require("next-auth/jwt");
388
532
  var ALLOWED_COOKIE_SIZE = 4096;
389
533
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
390
534
  var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
535
+ var REFRESH_RACE_GRACE_SECONDS = 60;
536
+ var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
391
537
  var pendingRefreshes = /* @__PURE__ */ new Map();
538
+ function shouldPreserveSessionOnRefreshFailure(params) {
539
+ if (params.error !== "invalid_grant" || !params.expiresAt) {
540
+ return false;
541
+ }
542
+ return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
543
+ }
544
+ function resolveRefreshConfig(options, token) {
545
+ return {
546
+ clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
547
+ clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
548
+ issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
549
+ };
550
+ }
392
551
  async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
393
552
  try {
394
553
  const tokenUrl = `${issuer}/oauth2/token`;
@@ -423,9 +582,6 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
423
582
  }
424
583
  function withOAuth42Auth(options = {}) {
425
584
  const secret = process.env.NEXTAUTH_SECRET;
426
- const clientId = process.env.OAUTH42_CLIENT_ID;
427
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
428
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
429
585
  if (!secret) {
430
586
  console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
431
587
  }
@@ -450,6 +606,42 @@ function withOAuth42Auth(options = {}) {
450
606
  url.searchParams.set("callbackUrl", pathname);
451
607
  return import_server.NextResponse.redirect(url);
452
608
  }
609
+ const refreshConfig = resolveRefreshConfig(options, token);
610
+ if (isBackendSessionMode(options.sessionStore)) {
611
+ const sessionId = token.oauth42SessionId;
612
+ if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
613
+ const signInUrl = options.pages?.signIn || "/auth/signin";
614
+ const url = new URL(signInUrl, req.url);
615
+ url.searchParams.set("callbackUrl", pathname);
616
+ const response = import_server.NextResponse.redirect(url);
617
+ response.cookies.delete(cookieName);
618
+ return response;
619
+ }
620
+ const backendSession = await resolveBackendSession({
621
+ issuer: refreshConfig.issuer,
622
+ clientId: refreshConfig.clientId,
623
+ clientSecret: refreshConfig.clientSecret,
624
+ sessionId
625
+ }).catch((error) => {
626
+ console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
627
+ return null;
628
+ });
629
+ if (!backendSession) {
630
+ const signInUrl = options.pages?.signIn || "/auth/signin";
631
+ const url = new URL(signInUrl, req.url);
632
+ url.searchParams.set("callbackUrl", pathname);
633
+ const response = import_server.NextResponse.redirect(url);
634
+ response.cookies.delete(cookieName);
635
+ return response;
636
+ }
637
+ const requestHeaders = new Headers(req.headers);
638
+ requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
639
+ return import_server.NextResponse.next({
640
+ request: {
641
+ headers: requestHeaders
642
+ }
643
+ });
644
+ }
453
645
  const expiresAt = token.expiresAt;
454
646
  const now = Math.floor(Date.now() / 1e3);
455
647
  const bufferSeconds = 15;
@@ -463,7 +655,7 @@ function withOAuth42Auth(options = {}) {
463
655
  path: pathname
464
656
  });
465
657
  }
466
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
658
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
467
659
  const userId = token.sub;
468
660
  let refreshPromise = pendingRefreshes.get(userId);
469
661
  if (refreshPromise) {
@@ -472,11 +664,14 @@ function withOAuth42Auth(options = {}) {
472
664
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
473
665
  refreshPromise = refreshTokens(
474
666
  token.refreshToken,
475
- clientId,
476
- clientSecret,
477
- issuer
667
+ refreshConfig.clientId,
668
+ refreshConfig.clientSecret,
669
+ refreshConfig.issuer
478
670
  ).finally(() => {
479
- setTimeout(() => pendingRefreshes.delete(userId), 3e4);
671
+ const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
672
+ if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
673
+ cleanupTimer.unref();
674
+ }
480
675
  });
481
676
  pendingRefreshes.set(userId, refreshPromise);
482
677
  }
@@ -503,7 +698,8 @@ function withOAuth42Auth(options = {}) {
503
698
  httpOnly: true,
504
699
  sameSite: "lax",
505
700
  path: "/",
506
- secure: process.env.NODE_ENV === "production"
701
+ secure: process.env.NODE_ENV === "production",
702
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
507
703
  };
508
704
  if (newJwt.length <= CHUNK_SIZE) {
509
705
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -551,7 +747,23 @@ function withOAuth42Auth(options = {}) {
551
747
  });
552
748
  return response;
553
749
  } else {
554
- console.log("[OAuth42 Middleware] Refresh failed (likely token blacklisted after deploy), clearing session");
750
+ if (shouldPreserveSessionOnRefreshFailure({
751
+ error: refreshed.error,
752
+ expiresAt,
753
+ now
754
+ })) {
755
+ console.warn(
756
+ "[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
757
+ {
758
+ expiresAt,
759
+ now,
760
+ expiredAgo: now - expiresAt,
761
+ path: pathname
762
+ }
763
+ );
764
+ return import_server.NextResponse.next();
765
+ }
766
+ console.log("[OAuth42 Middleware] Refresh failed, clearing session");
555
767
  const signInUrl = options.pages?.signIn || "/auth/signin";
556
768
  const url = new URL(signInUrl, req.url);
557
769
  url.searchParams.set("callbackUrl", pathname);
@@ -698,12 +910,15 @@ function createHostedAuthCallback(options = {}) {
698
910
  NextAuth,
699
911
  OAuth42Provider,
700
912
  createAuth,
913
+ createBackendSession,
701
914
  createHandlers,
702
915
  createHostedAuthCallback,
703
916
  createMiddlewareConfig,
704
917
  getOAuth42Session,
705
918
  getServerSession,
706
919
  refreshAccessToken,
920
+ resolveBackendSession,
921
+ revokeBackendSession,
707
922
  withOAuth42Auth,
708
923
  withOAuth42ServerSideProps,
709
924
  withOAuth42Session