@workos-inc/authkit-nextjs 0.17.2 → 1.0.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.
Files changed (55) hide show
  1. package/README.md +102 -25
  2. package/dist/esm/actions.js +11 -0
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/components/authkit-provider.js +128 -0
  5. package/dist/esm/components/authkit-provider.js.map +1 -0
  6. package/dist/esm/components/button.js.map +1 -0
  7. package/dist/esm/{impersonation.js → components/impersonation.js} +12 -7
  8. package/dist/esm/components/impersonation.js.map +1 -0
  9. package/dist/esm/components/index.js +4 -0
  10. package/dist/esm/components/index.js.map +1 -0
  11. package/dist/esm/components/min-max-button.js.map +1 -0
  12. package/dist/esm/env-variables.js +5 -2
  13. package/dist/esm/env-variables.js.map +1 -1
  14. package/dist/esm/index.js +4 -8
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/middleware.js +5 -2
  17. package/dist/esm/middleware.js.map +1 -1
  18. package/dist/esm/session.js +104 -107
  19. package/dist/esm/session.js.map +1 -1
  20. package/dist/esm/types/actions.d.ts +6 -0
  21. package/dist/esm/types/components/authkit-provider.d.ts +33 -0
  22. package/dist/esm/types/{impersonation.d.ts → components/impersonation.d.ts} +1 -1
  23. package/dist/esm/types/components/index.d.ts +3 -0
  24. package/dist/esm/types/env-variables.d.ts +2 -2
  25. package/dist/esm/types/index.d.ts +3 -5
  26. package/dist/esm/types/interfaces.d.ts +11 -0
  27. package/dist/esm/types/middleware.d.ts +3 -2
  28. package/dist/esm/types/session.d.ts +5 -28
  29. package/dist/esm/types/workos.d.ts +1 -1
  30. package/dist/esm/workos.js +1 -1
  31. package/dist/esm/workos.js.map +1 -1
  32. package/package.json +11 -1
  33. package/src/actions.ts +20 -0
  34. package/src/components/authkit-provider.tsx +169 -0
  35. package/src/{impersonation.tsx → components/impersonation.tsx} +14 -7
  36. package/src/components/index.ts +4 -0
  37. package/src/env-variables.ts +7 -2
  38. package/src/index.ts +3 -8
  39. package/src/interfaces.ts +13 -0
  40. package/src/middleware.ts +8 -4
  41. package/src/session.ts +137 -124
  42. package/src/workos.ts +1 -1
  43. package/dist/esm/authkit-provider.js +0 -52
  44. package/dist/esm/authkit-provider.js.map +0 -1
  45. package/dist/esm/button.js.map +0 -1
  46. package/dist/esm/impersonation.js.map +0 -1
  47. package/dist/esm/min-max-button.js.map +0 -1
  48. package/dist/esm/types/authkit-provider.d.ts +0 -11
  49. package/src/authkit-provider.tsx +0 -66
  50. /package/dist/esm/{button.js → components/button.js} +0 -0
  51. /package/dist/esm/{min-max-button.js → components/min-max-button.js} +0 -0
  52. /package/dist/esm/types/{button.d.ts → components/button.d.ts} +0 -0
  53. /package/dist/esm/types/{min-max-button.d.ts → components/min-max-button.d.ts} +0 -0
  54. /package/src/{button.tsx → components/button.tsx} +0 -0
  55. /package/src/{min-max-button.tsx → components/min-max-button.tsx} +0 -0
@@ -0,0 +1,169 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
4
+ import { checkSessionAction, getAuthAction, refreshAuthAction } from '../actions.js';
5
+ import type { Impersonator, User } from '@workos-inc/node';
6
+
7
+ type AuthContextType = {
8
+ user: User | null;
9
+ sessionId: string | undefined;
10
+ organizationId: string | undefined;
11
+ role: string | undefined;
12
+ permissions: string[] | undefined;
13
+ entitlements: string[] | undefined;
14
+ impersonator: Impersonator | undefined;
15
+ accessToken: string | undefined;
16
+ loading: boolean;
17
+ getAuth: (options?: { ensureSignedIn?: boolean }) => Promise<void>;
18
+ refreshAuth: (options?: { ensureSignedIn?: boolean; organizationId?: string }) => Promise<void | { error: string }>;
19
+ };
20
+
21
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
22
+
23
+ interface AuthKitProviderProps {
24
+ children: ReactNode;
25
+ /**
26
+ * Customize what happens when a session is expired. By default,the entire page will be reloaded.
27
+ * You can also pass this as `false` to disable the expired session checks.
28
+ */
29
+ onSessionExpired?: false | (() => void);
30
+ }
31
+
32
+ export const AuthKitProvider = ({ children, onSessionExpired }: AuthKitProviderProps) => {
33
+ const [user, setUser] = useState<User | null>(null);
34
+ const [sessionId, setSessionId] = useState<string | undefined>(undefined);
35
+ const [organizationId, setOrganizationId] = useState<string | undefined>(undefined);
36
+ const [role, setRole] = useState<string | undefined>(undefined);
37
+ const [permissions, setPermissions] = useState<string[] | undefined>(undefined);
38
+ const [entitlements, setEntitlements] = useState<string[] | undefined>(undefined);
39
+ const [impersonator, setImpersonator] = useState<Impersonator | undefined>(undefined);
40
+ const [accessToken, setAccessToken] = useState<string | undefined>(undefined);
41
+ const [loading, setLoading] = useState(true);
42
+
43
+ const getAuth = async ({ ensureSignedIn = false }: { ensureSignedIn?: boolean } = {}) => {
44
+ try {
45
+ const auth = await getAuthAction(ensureSignedIn);
46
+ setUser(auth.user);
47
+ setSessionId(auth.sessionId);
48
+ setOrganizationId(auth.organizationId);
49
+ setRole(auth.role);
50
+ setPermissions(auth.permissions);
51
+ setEntitlements(auth.entitlements);
52
+ setImpersonator(auth.impersonator);
53
+ setAccessToken(auth.accessToken);
54
+ } catch (error) {
55
+ setUser(null);
56
+ setSessionId(undefined);
57
+ setOrganizationId(undefined);
58
+ setRole(undefined);
59
+ setPermissions(undefined);
60
+ setEntitlements(undefined);
61
+ setImpersonator(undefined);
62
+ setAccessToken(undefined);
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ };
67
+
68
+ const refreshAuth = async ({
69
+ ensureSignedIn = false,
70
+ organizationId,
71
+ }: { ensureSignedIn?: boolean; organizationId?: string } = {}) => {
72
+ try {
73
+ setLoading(true);
74
+ const auth = await refreshAuthAction({ ensureSignedIn, organizationId });
75
+
76
+ setUser(auth.user);
77
+ setSessionId(auth.sessionId);
78
+ setOrganizationId(auth.organizationId);
79
+ setRole(auth.role);
80
+ setPermissions(auth.permissions);
81
+ setEntitlements(auth.entitlements);
82
+ setImpersonator(auth.impersonator);
83
+ setAccessToken(auth.accessToken);
84
+ } catch (error) {
85
+ return error instanceof Error ? { error: error.message } : { error: String(error) };
86
+ } finally {
87
+ setLoading(false);
88
+ }
89
+ };
90
+
91
+ useEffect(() => {
92
+ getAuth();
93
+
94
+ // Return early if the session expired checks are disabled.
95
+ if (onSessionExpired === false) {
96
+ return;
97
+ }
98
+
99
+ let visibilityChangedCalled = false;
100
+
101
+ const handleVisibilityChange = async () => {
102
+ if (visibilityChangedCalled) {
103
+ return;
104
+ }
105
+
106
+ // In the case where we're using middleware auth mode, a user that has signed out in a different tab
107
+ // will run into an issue if they attempt to hit a server action in the original tab.
108
+ // This will force a refresh of the page in that case, which will redirect them to the sign-in page.
109
+ if (document.visibilityState === 'visible') {
110
+ visibilityChangedCalled = true;
111
+
112
+ try {
113
+ const hasSession = await checkSessionAction();
114
+ if (!hasSession) {
115
+ throw new Error('Session expired');
116
+ }
117
+ } catch (error) {
118
+ // 'Failed to fetch' is the error we are looking for if the action fails
119
+ // If any other error happens, for other reasons, we should not reload the page
120
+ if (error instanceof Error && error.message.includes('Failed to fetch')) {
121
+ if (onSessionExpired) {
122
+ onSessionExpired();
123
+ } else {
124
+ window.location.reload();
125
+ }
126
+ }
127
+ } finally {
128
+ visibilityChangedCalled = false;
129
+ }
130
+ }
131
+ };
132
+
133
+ window.addEventListener('visibilitychange', handleVisibilityChange);
134
+ window.addEventListener('focus', handleVisibilityChange);
135
+
136
+ return () => {
137
+ window.removeEventListener('focus', handleVisibilityChange);
138
+ window.removeEventListener('visibilitychange', handleVisibilityChange);
139
+ };
140
+ }, [onSessionExpired]);
141
+
142
+ return (
143
+ <AuthContext.Provider
144
+ value={{
145
+ user,
146
+ sessionId,
147
+ organizationId,
148
+ role,
149
+ permissions,
150
+ entitlements,
151
+ impersonator,
152
+ accessToken,
153
+ loading,
154
+ getAuth,
155
+ refreshAuth,
156
+ }}
157
+ >
158
+ {children}
159
+ </AuthContext.Provider>
160
+ );
161
+ };
162
+
163
+ export function useAuth() {
164
+ const context = useContext(AuthContext);
165
+ if (!context) {
166
+ throw new Error('useAuth must be used within an AuthKitProvider');
167
+ }
168
+ return context;
169
+ }
@@ -1,20 +1,27 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
- import { withAuth } from './session.js';
3
- import { workos } from './workos.js';
4
4
  import { Button } from './button.js';
5
5
  import { MinMaxButton } from './min-max-button.js';
6
- import { handleSignOutAction } from './actions.js';
6
+ import { getOrganizationAction, handleSignOutAction } from '../actions.js';
7
+ import type { Organization } from '@workos-inc/node';
8
+ import { useAuth } from './authkit-provider.js';
7
9
 
8
10
  interface ImpersonationProps extends React.ComponentPropsWithoutRef<'div'> {
9
11
  side?: 'top' | 'bottom';
10
12
  }
11
13
 
12
- export async function Impersonation({ side = 'bottom', ...props }: ImpersonationProps) {
13
- const { impersonator, user, organizationId } = await withAuth();
14
+ export function Impersonation({ side = 'bottom', ...props }: ImpersonationProps) {
15
+ const { user, impersonator, organizationId, loading } = useAuth();
16
+
17
+ const [organization, setOrganization] = React.useState<Organization | null>(null);
14
18
 
15
- if (!impersonator) return null;
19
+ React.useEffect(() => {
20
+ if (!organizationId) return;
21
+ getOrganizationAction(organizationId).then(setOrganization);
22
+ }, [organizationId]);
16
23
 
17
- const organization = organizationId ? await workos.organizations.getOrganization(organizationId) : null;
24
+ if (loading || !impersonator || !user) return null;
18
25
 
19
26
  return (
20
27
  <div
@@ -0,0 +1,4 @@
1
+ import { Impersonation } from './impersonation.js';
2
+ import { AuthKitProvider, useAuth } from './authkit-provider.js';
3
+
4
+ export { Impersonation, AuthKitProvider, useAuth };
@@ -1,15 +1,20 @@
1
+ /* istanbul ignore file */
2
+
1
3
  function getEnvVariable(name: string): string | undefined {
2
4
  return process.env[name];
3
5
  }
4
6
 
7
+ // Optional env variables
5
8
  const WORKOS_API_HOSTNAME = getEnvVariable('WORKOS_API_HOSTNAME');
6
9
  const WORKOS_API_HTTPS = getEnvVariable('WORKOS_API_HTTPS');
7
- const WORKOS_API_KEY = getEnvVariable('WORKOS_API_KEY') ?? '';
8
10
  const WORKOS_API_PORT = getEnvVariable('WORKOS_API_PORT');
9
- const WORKOS_CLIENT_ID = getEnvVariable('WORKOS_CLIENT_ID') ?? '';
10
11
  const WORKOS_COOKIE_DOMAIN = getEnvVariable('WORKOS_COOKIE_DOMAIN');
11
12
  const WORKOS_COOKIE_MAX_AGE = getEnvVariable('WORKOS_COOKIE_MAX_AGE');
12
13
  const WORKOS_COOKIE_NAME = getEnvVariable('WORKOS_COOKIE_NAME');
14
+
15
+ // Required env variables
16
+ const WORKOS_API_KEY = getEnvVariable('WORKOS_API_KEY') ?? '';
17
+ const WORKOS_CLIENT_ID = getEnvVariable('WORKOS_CLIENT_ID') ?? '';
13
18
  const WORKOS_COOKIE_PASSWORD = getEnvVariable('WORKOS_COOKIE_PASSWORD') ?? '';
14
19
  const WORKOS_REDIRECT_URI = process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI ?? '';
15
20
 
package/src/index.ts CHANGED
@@ -1,22 +1,17 @@
1
1
  import { handleAuth } from './authkit-callback-route.js';
2
- import { authkitMiddleware } from './middleware.js';
3
- import { withAuth, refreshSession, getSession } from './session.js';
2
+ import { authkit, authkitMiddleware } from './middleware.js';
3
+ import { withAuth, refreshSession } from './session.js';
4
4
  import { getSignInUrl, getSignUpUrl, signOut } from './auth.js';
5
- import { Impersonation } from './impersonation.js';
6
- import { AuthKitProvider } from './authkit-provider.js';
7
5
 
8
6
  export {
9
7
  handleAuth,
10
8
  //
11
9
  authkitMiddleware,
12
- getSession,
10
+ authkit,
13
11
  //
14
12
  getSignInUrl,
15
13
  getSignUpUrl,
16
14
  withAuth,
17
15
  refreshSession,
18
16
  signOut,
19
- //
20
- Impersonation,
21
- AuthKitProvider,
22
17
  };
package/src/interfaces.ts CHANGED
@@ -37,6 +37,7 @@ export interface NoUserInfo {
37
37
  organizationId?: undefined;
38
38
  role?: undefined;
39
39
  permissions?: undefined;
40
+ entitlements?: undefined;
40
41
  impersonator?: undefined;
41
42
  accessToken?: undefined;
42
43
  }
@@ -68,6 +69,18 @@ export interface AuthkitMiddlewareOptions {
68
69
  signUpPaths?: string[];
69
70
  }
70
71
 
72
+ export interface AuthkitOptions {
73
+ debug?: boolean;
74
+ redirectUri?: string;
75
+ screenHint?: 'sign-up' | 'sign-in';
76
+ }
77
+
78
+ export interface AuthkitResponse {
79
+ session: UserInfo | NoUserInfo;
80
+ headers: Headers;
81
+ authorizationUrl?: string;
82
+ }
83
+
71
84
  export interface CookieOptions {
72
85
  path: '/';
73
86
  httpOnly: true;
package/src/middleware.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { NextMiddleware } from 'next/server';
2
- import { updateSession } from './session.js';
3
- import { AuthkitMiddlewareOptions } from './interfaces.js';
1
+ import { NextMiddleware, NextRequest } from 'next/server';
2
+ import { updateSessionMiddleware, updateSession } from './session.js';
3
+ import { AuthkitMiddlewareOptions, AuthkitOptions, AuthkitResponse } from './interfaces.js';
4
4
  import { WORKOS_REDIRECT_URI } from './env-variables.js';
5
5
 
6
6
  export function authkitMiddleware({
@@ -10,6 +10,10 @@ export function authkitMiddleware({
10
10
  signUpPaths = [],
11
11
  }: AuthkitMiddlewareOptions = {}): NextMiddleware {
12
12
  return function (request) {
13
- return updateSession(request, debug, middlewareAuth, redirectUri, signUpPaths);
13
+ return updateSessionMiddleware(request, debug, middlewareAuth, redirectUri, signUpPaths);
14
14
  };
15
15
  }
16
+
17
+ export async function authkit(request: NextRequest, options: AuthkitOptions = {}): Promise<AuthkitResponse> {
18
+ return await updateSession(request, options);
19
+ }
package/src/session.ts CHANGED
@@ -9,14 +9,21 @@ import { getCookieOptions } from './cookie.js';
9
9
  import { workos } from './workos.js';
10
10
  import { WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD, WORKOS_COOKIE_NAME, WORKOS_REDIRECT_URI } from './env-variables.js';
11
11
  import { getAuthorizationUrl } from './get-authorization-url.js';
12
- import { AccessToken, AuthkitMiddlewareAuth, NoUserInfo, Session, UserInfo } from './interfaces.js';
12
+ import {
13
+ AccessToken,
14
+ AuthkitMiddlewareAuth,
15
+ AuthkitOptions,
16
+ AuthkitResponse,
17
+ NoUserInfo,
18
+ Session,
19
+ UserInfo,
20
+ } from './interfaces.js';
13
21
 
14
22
  import { parse, tokensToRegexp } from 'path-to-regexp';
15
23
  import { redirectWithFallback } from './utils.js';
16
24
 
17
25
  const sessionHeaderName = 'x-workos-session';
18
26
  const middlewareHeaderName = 'x-workos-middleware';
19
- const redirectUriHeaderName = 'x-redirect-uri';
20
27
  const signUpPathsHeaderName = 'x-sign-up-paths';
21
28
 
22
29
  const JWKS = createRemoteJWKSet(new URL(workos.userManagement.getJwksUrl(WORKOS_CLIENT_ID)));
@@ -28,7 +35,7 @@ async function encryptSession(session: Session) {
28
35
  });
29
36
  }
30
37
 
31
- async function updateSession(
38
+ async function updateSessionMiddleware(
32
39
  request: NextRequest,
33
40
  debug: boolean,
34
41
  middlewareAuth: AuthkitMiddlewareAuth,
@@ -45,34 +52,14 @@ async function updateSession(
45
52
  );
46
53
  }
47
54
 
48
- const session = await getSessionFromCookie();
49
- const newRequestHeaders = new Headers(request.headers);
50
-
51
- // We store the current request url in a custom header, so we can always have access to it
52
- // This is because on hard navigations we don't have access to `next-url` but need to get the current
53
- // `pathname` to be able to return the users where they came from before sign-in
54
- newRequestHeaders.set('x-url', request.url);
55
-
56
- // Record that the request was routed through the middleware so we can check later for DX purposes
57
- newRequestHeaders.set(middlewareHeaderName, 'true');
58
-
59
- // Record the sign up paths so we can use it later
60
- if (signUpPaths.length > 0) {
61
- newRequestHeaders.set(signUpPathsHeaderName, signUpPaths.join(','));
62
- }
63
-
64
55
  let url;
65
56
 
66
- // If the redirect URI is set, store it in the headers so we can use it later
67
57
  if (redirectUri) {
68
- newRequestHeaders.set(redirectUriHeaderName, redirectUri);
69
58
  url = new URL(redirectUri);
70
59
  } else {
71
60
  url = new URL(WORKOS_REDIRECT_URI);
72
61
  }
73
62
 
74
- newRequestHeaders.delete(sessionHeaderName);
75
-
76
63
  if (
77
64
  middlewareAuth.enabled &&
78
65
  url.pathname === request.nextUrl.pathname &&
@@ -94,81 +81,131 @@ async function updateSession(
94
81
  return pathRegex.exec(request.nextUrl.pathname);
95
82
  });
96
83
 
97
- // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
98
- if (middlewareAuth.enabled && matchedPaths.length === 0 && !session) {
99
- if (debug) console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
84
+ const { session, headers, authorizationUrl } = await updateSession(request, {
85
+ debug,
86
+ redirectUri,
87
+ screenHint: getScreenHint(signUpPaths, request.nextUrl.pathname),
88
+ });
100
89
 
101
- const redirectTo = await getAuthorizationUrl({
102
- returnPathname: getReturnPathname(request.url),
103
- redirectUri: redirectUri,
104
- screenHint: getScreenHint(signUpPaths, request.nextUrl.pathname),
105
- });
90
+ // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
91
+ if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
92
+ if (debug) {
93
+ console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
94
+ }
106
95
 
107
- return redirectWithFallback(redirectTo);
96
+ return redirectWithFallback(authorizationUrl as string);
108
97
  }
109
98
 
110
- // If no session, just continue
111
- if (!session) {
112
- return NextResponse.next({
113
- request: { headers: newRequestHeaders },
114
- });
99
+ // Record the sign up paths so we can use them later
100
+ if (signUpPaths.length > 0) {
101
+ headers.set(signUpPathsHeaderName, signUpPaths.join(','));
115
102
  }
116
103
 
117
- const hasValidSession = await verifyAccessToken(session.accessToken);
118
- const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
104
+ return NextResponse.next({
105
+ request: { headers },
106
+ });
107
+ }
119
108
 
120
- const nextCookies = await cookies();
109
+ async function updateSession(
110
+ request: NextRequest,
111
+ options: AuthkitOptions = { debug: false },
112
+ ): Promise<AuthkitResponse> {
113
+ const session = await getSessionFromCookie();
121
114
 
122
- if (hasValidSession) {
123
- if (debug) console.log('Session is valid');
124
- // set the x-workos-session header according to the current cookie value
125
- newRequestHeaders.set(sessionHeaderName, nextCookies.get(cookieName)!.value);
126
- return NextResponse.next({
127
- request: { headers: newRequestHeaders },
128
- });
129
- }
115
+ const newRequestHeaders = new Headers(request.headers);
130
116
 
131
- try {
132
- if (debug) console.log(`Session invalid. Refreshing access token that ends in ${session.accessToken.slice(-10)}`);
117
+ // Record that the request was routed through the middleware so we can check later for DX purposes
118
+ newRequestHeaders.set(middlewareHeaderName, 'true');
133
119
 
134
- const { org_id: organizationId } = decodeJwt<AccessToken>(session.accessToken);
120
+ // We store the current request url in a custom header, so we can always have access to it
121
+ // This is because on hard navigations we don't have access to `next-url` but need to get the current
122
+ // `pathname` to be able to return the users where they came from before sign-in
123
+ newRequestHeaders.set('x-url', request.url);
135
124
 
136
- // If the session is invalid (i.e. the access token has expired) attempt to re-authenticate with the refresh token
137
- const { accessToken, refreshToken, user, impersonator } = await workos.userManagement.authenticateWithRefreshToken({
138
- clientId: WORKOS_CLIENT_ID,
139
- refreshToken: session.refreshToken,
140
- organizationId,
141
- });
125
+ newRequestHeaders.delete(sessionHeaderName);
142
126
 
143
- if (debug) console.log(`Refresh successful. New access token ends in ${accessToken.slice(-10)}`);
127
+ if (!session) {
128
+ if (options.debug) {
129
+ console.log('No session found from cookie');
130
+ }
144
131
 
145
- // Encrypt session with new access and refresh tokens
146
- const encryptedSession = await encryptSession({
147
- accessToken,
148
- refreshToken,
149
- user,
150
- impersonator,
151
- });
132
+ return {
133
+ session: { user: null },
134
+ headers: newRequestHeaders,
135
+ authorizationUrl: await getAuthorizationUrl({
136
+ returnPathname: getReturnPathname(request.url),
137
+ redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
138
+ screenHint: options.screenHint,
139
+ }),
140
+ };
141
+ }
152
142
 
153
- newRequestHeaders.set(sessionHeaderName, encryptedSession);
143
+ const hasValidSession = await verifyAccessToken(session.accessToken);
154
144
 
155
- const response = NextResponse.next({
156
- request: { headers: newRequestHeaders },
157
- });
158
- // update the cookie
159
- response.cookies.set(cookieName, encryptedSession, getCookieOptions(redirectUri));
160
- return response;
161
- } catch (e) {
162
- if (debug) console.log('Failed to refresh. Deleting cookie and redirecting.', e);
145
+ const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
146
+ const nextCookies = await cookies();
163
147
 
164
- nextCookies.delete(cookieName);
148
+ if (!hasValidSession) {
149
+ if (options.debug) {
150
+ console.log(`Session invalid. Refreshing access token that ends in ${session.accessToken.slice(-10)}`);
151
+ }
152
+
153
+ try {
154
+ const newSession = await refreshSession({
155
+ ensureSignedIn: false,
156
+ });
157
+
158
+ if (options.debug) {
159
+ console.log('Session successfully refreshed');
160
+ }
161
+
162
+ newRequestHeaders.set(sessionHeaderName, nextCookies.get(cookieName)!.value);
163
+
164
+ return {
165
+ session: newSession,
166
+ headers: newRequestHeaders,
167
+ };
168
+ } catch (e) {
169
+ if (options.debug) {
170
+ console.log('Failed to refresh. Deleting cookie.', e);
171
+ }
172
+
173
+ const nextCookies = await cookies();
174
+ nextCookies.delete(cookieName);
175
+
176
+ return {
177
+ session: { user: null },
178
+ headers: newRequestHeaders,
179
+ authorizationUrl: await getAuthorizationUrl({
180
+ returnPathname: getReturnPathname(request.url),
181
+ }),
182
+ };
183
+ }
165
184
  }
166
185
 
167
- // If we get here, the session is invalid and the user needs to sign in again.
168
- // We redirect to the current URL which will trigger the middleware again.
169
- // This is outside of the above block because you cannot redirect in Next.js
170
- // from inside a try/catch block.
171
- return redirectWithFallback(request.url);
186
+ newRequestHeaders.set(sessionHeaderName, nextCookies.get(cookieName)!.value);
187
+
188
+ const {
189
+ sid: sessionId,
190
+ org_id: organizationId,
191
+ role,
192
+ permissions,
193
+ entitlements,
194
+ } = decodeJwt<AccessToken>(session.accessToken);
195
+
196
+ return {
197
+ session: {
198
+ sessionId,
199
+ user: session.user,
200
+ organizationId,
201
+ role,
202
+ permissions,
203
+ entitlements,
204
+ impersonator: session.impersonator,
205
+ accessToken: session.accessToken,
206
+ },
207
+ headers: newRequestHeaders,
208
+ };
172
209
  }
173
210
 
174
211
  async function refreshSession(options: {
@@ -194,12 +231,21 @@ async function refreshSession({
194
231
 
195
232
  const { org_id: organizationIdFromAccessToken } = decodeJwt<AccessToken>(session.accessToken);
196
233
 
197
- const { accessToken, refreshToken, user, impersonator } = await workos.userManagement.authenticateWithRefreshToken({
198
- clientId: WORKOS_CLIENT_ID,
199
- refreshToken: session.refreshToken,
200
- organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
201
- });
234
+ let refreshResult;
202
235
 
236
+ try {
237
+ refreshResult = await workos.userManagement.authenticateWithRefreshToken({
238
+ clientId: WORKOS_CLIENT_ID,
239
+ refreshToken: session.refreshToken,
240
+ organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
241
+ });
242
+ } catch (error) {
243
+ throw new Error(`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`, {
244
+ cause: error,
245
+ });
246
+ }
247
+
248
+ const { accessToken, refreshToken, user, impersonator } = refreshResult;
203
249
  // Encrypt session with new access and refresh tokens
204
250
  const encryptedSession = await encryptSession({
205
251
  accessToken,
@@ -273,9 +319,8 @@ async function redirectToSignIn() {
273
319
  }
274
320
 
275
321
  async function withAuth(options?: { ensureSignedIn: false }): Promise<UserInfo | NoUserInfo>;
276
- // @ts-expect-error - TS complains about the overload signature when we have more than 2 optional properties
277
322
  async function withAuth(options: { ensureSignedIn: true }): Promise<UserInfo>;
278
- async function withAuth({ ensureSignedIn = false } = {}) {
323
+ async function withAuth({ ensureSignedIn = false } = {}): Promise<UserInfo | NoUserInfo> {
279
324
  const session = await getSessionFromHeader();
280
325
 
281
326
  if (!session) {
@@ -323,50 +368,18 @@ async function verifyAccessToken(accessToken: string) {
323
368
  }
324
369
  }
325
370
 
326
- async function getSessionFromCookie(response?: NextResponse) {
371
+ async function getSessionFromCookie() {
327
372
  const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
328
373
  const nextCookies = await cookies();
329
- const cookie = response ? response.cookies.get(cookieName) : nextCookies.get(cookieName);
374
+ const cookie = nextCookies.get(cookieName);
330
375
 
331
376
  if (cookie) {
332
- return unsealData<Session>(cookie.value ?? cookie, {
377
+ return unsealData<Session>(cookie.value, {
333
378
  password: WORKOS_COOKIE_PASSWORD,
334
379
  });
335
380
  }
336
381
  }
337
382
 
338
- /**
339
- * Retrieves the session from the cookie. Meant for use in the middleware, for client side use `withAuth` instead.
340
- *
341
- * @returns UserInfo | NoUserInfo
342
- */
343
- async function getSession(response?: NextResponse) {
344
- const session = await getSessionFromCookie(response);
345
-
346
- if (!session) return { user: null };
347
-
348
- if (await verifyAccessToken(session.accessToken)) {
349
- const {
350
- sid: sessionId,
351
- org_id: organizationId,
352
- role,
353
- permissions,
354
- entitlements,
355
- } = decodeJwt<AccessToken>(session.accessToken);
356
-
357
- return {
358
- sessionId,
359
- user: session.user,
360
- organizationId,
361
- role,
362
- permissions,
363
- entitlements,
364
- impersonator: session.impersonator,
365
- accessToken: session.accessToken,
366
- };
367
- }
368
- }
369
-
370
383
  async function getSessionFromHeader(): Promise<Session | undefined> {
371
384
  const headersList = await headers();
372
385
  const hasMiddleware = Boolean(headersList.get(middlewareHeaderName));
@@ -374,7 +387,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
374
387
  if (!hasMiddleware) {
375
388
  const url = headersList.get('x-url');
376
389
  throw new Error(
377
- `You are calling 'withAuth' on ${url} that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
390
+ `You are calling 'withAuth' on ${url ?? 'a route'} that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
378
391
  );
379
392
  }
380
393
 
@@ -401,4 +414,4 @@ function getScreenHint(signUpPaths: string[] | undefined, pathname: string) {
401
414
  return screenHintPaths.length > 0 ? 'sign-up' : 'sign-in';
402
415
  }
403
416
 
404
- export { encryptSession, withAuth, refreshSession, terminateSession, updateSession, getSession };
417
+ export { encryptSession, withAuth, refreshSession, terminateSession, updateSessionMiddleware, updateSession };
package/src/workos.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { WorkOS } from '@workos-inc/node';
2
2
  import { WORKOS_API_HOSTNAME, WORKOS_API_KEY, WORKOS_API_HTTPS, WORKOS_API_PORT } from './env-variables.js';
3
3
 
4
- export const VERSION = '0.17.2';
4
+ export const VERSION = '1.0.0';
5
5
 
6
6
  const options = {
7
7
  apiHostname: WORKOS_API_HOSTNAME,