@oauth42/next 0.4.8 → 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.
@@ -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
@@ -394,7 +532,22 @@ var import_jwt = require("next-auth/jwt");
394
532
  var ALLOWED_COOKIE_SIZE = 4096;
395
533
  var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
396
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;
397
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
+ }
398
551
  async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
399
552
  try {
400
553
  const tokenUrl = `${issuer}/oauth2/token`;
@@ -429,9 +582,6 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
429
582
  }
430
583
  function withOAuth42Auth(options = {}) {
431
584
  const secret = process.env.NEXTAUTH_SECRET;
432
- const clientId = process.env.OAUTH42_CLIENT_ID;
433
- const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
434
- const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
435
585
  if (!secret) {
436
586
  console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
437
587
  }
@@ -456,6 +606,42 @@ function withOAuth42Auth(options = {}) {
456
606
  url.searchParams.set("callbackUrl", pathname);
457
607
  return import_server.NextResponse.redirect(url);
458
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
+ }
459
645
  const expiresAt = token.expiresAt;
460
646
  const now = Math.floor(Date.now() / 1e3);
461
647
  const bufferSeconds = 15;
@@ -469,7 +655,7 @@ function withOAuth42Auth(options = {}) {
469
655
  path: pathname
470
656
  });
471
657
  }
472
- if (needsRefresh && token.refreshToken && clientId && clientSecret) {
658
+ if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
473
659
  const userId = token.sub;
474
660
  let refreshPromise = pendingRefreshes.get(userId);
475
661
  if (refreshPromise) {
@@ -478,11 +664,14 @@ function withOAuth42Auth(options = {}) {
478
664
  console.log("[OAuth42 Middleware] Access token expired, refreshing...");
479
665
  refreshPromise = refreshTokens(
480
666
  token.refreshToken,
481
- clientId,
482
- clientSecret,
483
- issuer
667
+ refreshConfig.clientId,
668
+ refreshConfig.clientSecret,
669
+ refreshConfig.issuer
484
670
  ).finally(() => {
485
- 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
+ }
486
675
  });
487
676
  pendingRefreshes.set(userId, refreshPromise);
488
677
  }
@@ -509,7 +698,8 @@ function withOAuth42Auth(options = {}) {
509
698
  httpOnly: true,
510
699
  sameSite: "lax",
511
700
  path: "/",
512
- secure: process.env.NODE_ENV === "production"
701
+ secure: process.env.NODE_ENV === "production",
702
+ maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
513
703
  };
514
704
  if (newJwt.length <= CHUNK_SIZE) {
515
705
  response.cookies.set(cookieName, newJwt, cookieOptions);
@@ -557,7 +747,23 @@ function withOAuth42Auth(options = {}) {
557
747
  });
558
748
  return response;
559
749
  } else {
560
- 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");
561
767
  const signInUrl = options.pages?.signIn || "/auth/signin";
562
768
  const url = new URL(signInUrl, req.url);
563
769
  url.searchParams.set("callbackUrl", pathname);
@@ -704,12 +910,15 @@ function createHostedAuthCallback(options = {}) {
704
910
  NextAuth,
705
911
  OAuth42Provider,
706
912
  createAuth,
913
+ createBackendSession,
707
914
  createHandlers,
708
915
  createHostedAuthCallback,
709
916
  createMiddlewareConfig,
710
917
  getOAuth42Session,
711
918
  getServerSession,
712
919
  refreshAccessToken,
920
+ resolveBackendSession,
921
+ revokeBackendSession,
713
922
  withOAuth42Auth,
714
923
  withOAuth42ServerSideProps,
715
924
  withOAuth42Session