@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.
- package/README.md +102 -25
- package/dist/esm/actions.js +11 -0
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +128 -0
- package/dist/esm/components/authkit-provider.js.map +1 -0
- package/dist/esm/components/button.js.map +1 -0
- package/dist/esm/{impersonation.js → components/impersonation.js} +12 -7
- package/dist/esm/components/impersonation.js.map +1 -0
- package/dist/esm/components/index.js +4 -0
- package/dist/esm/components/index.js.map +1 -0
- package/dist/esm/components/min-max-button.js.map +1 -0
- package/dist/esm/env-variables.js +5 -2
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/index.js +4 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middleware.js +5 -2
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/session.js +104 -107
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/types/actions.d.ts +6 -0
- package/dist/esm/types/components/authkit-provider.d.ts +33 -0
- package/dist/esm/types/{impersonation.d.ts → components/impersonation.d.ts} +1 -1
- package/dist/esm/types/components/index.d.ts +3 -0
- package/dist/esm/types/env-variables.d.ts +2 -2
- package/dist/esm/types/index.d.ts +3 -5
- package/dist/esm/types/interfaces.d.ts +11 -0
- package/dist/esm/types/middleware.d.ts +3 -2
- package/dist/esm/types/session.d.ts +5 -28
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/workos.js +1 -1
- package/dist/esm/workos.js.map +1 -1
- package/package.json +11 -1
- package/src/actions.ts +20 -0
- package/src/components/authkit-provider.tsx +169 -0
- package/src/{impersonation.tsx → components/impersonation.tsx} +14 -7
- package/src/components/index.ts +4 -0
- package/src/env-variables.ts +7 -2
- package/src/index.ts +3 -8
- package/src/interfaces.ts +13 -0
- package/src/middleware.ts +8 -4
- package/src/session.ts +137 -124
- package/src/workos.ts +1 -1
- package/dist/esm/authkit-provider.js +0 -52
- package/dist/esm/authkit-provider.js.map +0 -1
- package/dist/esm/button.js.map +0 -1
- package/dist/esm/impersonation.js.map +0 -1
- package/dist/esm/min-max-button.js.map +0 -1
- package/dist/esm/types/authkit-provider.d.ts +0 -11
- package/src/authkit-provider.tsx +0 -66
- /package/dist/esm/{button.js → components/button.js} +0 -0
- /package/dist/esm/{min-max-button.js → components/min-max-button.js} +0 -0
- /package/dist/esm/types/{button.d.ts → components/button.d.ts} +0 -0
- /package/dist/esm/types/{min-max-button.d.ts → components/min-max-button.d.ts} +0 -0
- /package/src/{button.tsx → components/button.tsx} +0 -0
- /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 '
|
|
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
|
|
13
|
-
const {
|
|
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
|
-
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
if (!organizationId) return;
|
|
21
|
+
getOrganizationAction(organizationId).then(setOrganization);
|
|
22
|
+
}, [organizationId]);
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
if (loading || !impersonator || !user) return null;
|
|
18
25
|
|
|
19
26
|
return (
|
|
20
27
|
<div
|
package/src/env-variables.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
const { session, headers, authorizationUrl } = await updateSession(request, {
|
|
85
|
+
debug,
|
|
86
|
+
redirectUri,
|
|
87
|
+
screenHint: getScreenHint(signUpPaths, request.nextUrl.pathname),
|
|
88
|
+
});
|
|
100
89
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
96
|
+
return redirectWithFallback(authorizationUrl as string);
|
|
108
97
|
}
|
|
109
98
|
|
|
110
|
-
//
|
|
111
|
-
if (
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
104
|
+
return NextResponse.next({
|
|
105
|
+
request: { headers },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
119
108
|
|
|
120
|
-
|
|
109
|
+
async function updateSession(
|
|
110
|
+
request: NextRequest,
|
|
111
|
+
options: AuthkitOptions = { debug: false },
|
|
112
|
+
): Promise<AuthkitResponse> {
|
|
113
|
+
const session = await getSessionFromCookie();
|
|
121
114
|
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
+
if (!session) {
|
|
128
|
+
if (options.debug) {
|
|
129
|
+
console.log('No session found from cookie');
|
|
130
|
+
}
|
|
144
131
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
143
|
+
const hasValidSession = await verifyAccessToken(session.accessToken);
|
|
154
144
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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(
|
|
371
|
+
async function getSessionFromCookie() {
|
|
327
372
|
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
328
373
|
const nextCookies = await cookies();
|
|
329
|
-
const cookie =
|
|
374
|
+
const cookie = nextCookies.get(cookieName);
|
|
330
375
|
|
|
331
376
|
if (cookie) {
|
|
332
|
-
return unsealData<Session>(cookie.value
|
|
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,
|
|
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.
|
|
4
|
+
export const VERSION = '1.0.0';
|
|
5
5
|
|
|
6
6
|
const options = {
|
|
7
7
|
apiHostname: WORKOS_API_HOSTNAME,
|