@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.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 +276 -102
- package/dist/esm/actions.js +35 -4
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +51 -20
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +82 -93
- package/dist/esm/authkit-callback-route.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +36 -15
- package/dist/esm/components/authkit-provider.js.map +1 -1
- package/dist/esm/components/impersonation.js +17 -15
- package/dist/esm/components/impersonation.js.map +1 -1
- package/dist/esm/components/min-max-button.js +1 -1
- package/dist/esm/components/min-max-button.js.map +1 -1
- package/dist/esm/components/tokenStore.js +28 -19
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +1 -1
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/components/useTokenClaims.js +1 -1
- package/dist/esm/components/useTokenClaims.js.map +1 -1
- package/dist/esm/cookie.js +16 -5
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/env-variables.js +6 -6
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/errors.js +36 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +51 -12
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces.js +7 -1
- package/dist/esm/interfaces.js.map +1 -1
- package/dist/esm/middleware-helpers.js +102 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/pkce.js +38 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +73 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +1 -1
- package/dist/esm/test-helpers.js.map +1 -1
- package/dist/esm/types/actions.d.ts +34 -5
- package/dist/esm/types/auth.d.ts +7 -15
- package/dist/esm/types/components/authkit-provider.d.ts +6 -2
- package/dist/esm/types/components/impersonation.d.ts +2 -1
- package/dist/esm/types/cookie.d.ts +8 -0
- package/dist/esm/types/env-variables.d.ts +2 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/get-authorization-url.d.ts +2 -2
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/interfaces.d.ts +12 -0
- package/dist/esm/types/jwt.d.ts +9 -9
- package/dist/esm/types/middleware-helpers.d.ts +27 -0
- package/dist/esm/types/middleware.d.ts +3 -1
- package/dist/esm/types/pkce.d.ts +12 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/utils.d.ts +5 -0
- package/dist/esm/types/validate-api-key.d.ts +1 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +10 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/validate-api-key.js +16 -0
- package/dist/esm/validate-api-key.js.map +1 -0
- package/dist/esm/workos.js +1 -1
- package/package.json +32 -34
- package/src/actions.spec.ts +94 -17
- package/src/actions.ts +44 -5
- package/src/auth.spec.ts +60 -29
- package/src/auth.ts +55 -41
- package/src/authkit-callback-route.spec.ts +310 -58
- package/src/authkit-callback-route.ts +106 -103
- package/src/components/authkit-provider.spec.tsx +264 -70
- package/src/components/authkit-provider.tsx +40 -15
- package/src/components/button.spec.tsx +4 -6
- package/src/components/impersonation.spec.tsx +152 -35
- package/src/components/impersonation.tsx +37 -30
- package/src/components/min-max-button.spec.tsx +2 -1
- package/src/components/tokenStore.spec.ts +59 -44
- package/src/components/tokenStore.ts +11 -3
- package/src/components/useAccessToken.spec.tsx +82 -83
- package/src/components/useTokenClaims.spec.tsx +23 -22
- package/src/cookie.spec.ts +14 -9
- package/src/cookie.ts +29 -0
- package/src/env-variables.ts +2 -0
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.spec.ts +170 -15
- package/src/get-authorization-url.ts +69 -23
- package/src/index.ts +20 -2
- package/src/interfaces.ts +15 -0
- package/src/jwt.ts +9 -9
- package/src/middleware-helpers.spec.ts +238 -0
- package/src/middleware-helpers.ts +134 -0
- package/src/middleware.spec.ts +25 -0
- package/src/middleware.ts +4 -1
- package/src/pkce.spec.ts +125 -0
- package/src/pkce.ts +42 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +91 -27
- package/src/test-helpers.ts +1 -1
- package/src/utils.spec.ts +14 -31
- package/src/utils.ts +9 -0
- package/src/validate-api-key.spec.ts +111 -0
- package/src/validate-api-key.ts +19 -0
- package/src/workos.spec.ts +2 -2
- package/src/workos.ts +1 -1
|
@@ -1,39 +1,15 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
|
+
import { getPKCECookieOptions } from './cookie.js';
|
|
2
3
|
import { WORKOS_CLIENT_ID } from './env-variables.js';
|
|
3
4
|
import { HandleAuthOptions } from './interfaces.js';
|
|
5
|
+
import { PKCE_COOKIE_NAME, getStateFromPKCECookieValue } from './pkce.js';
|
|
4
6
|
import { saveSession } from './session.js';
|
|
5
|
-
import { errorResponseWithFallback, redirectWithFallback } from './utils.js';
|
|
7
|
+
import { errorResponseWithFallback, redirectWithFallback, setCachePreventionHeaders } from './utils.js';
|
|
6
8
|
import { getWorkOS } from './workos.js';
|
|
7
9
|
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (state?.includes('.')) {
|
|
12
|
-
const [internal, ...rest] = state.split('.');
|
|
13
|
-
userState = rest.join('.');
|
|
14
|
-
try {
|
|
15
|
-
// Reverse URL-safe base64 encoding
|
|
16
|
-
const decoded = internal.replace(/-/g, '+').replace(/_/g, '/');
|
|
17
|
-
returnPathname = JSON.parse(atob(decoded)).returnPathname;
|
|
18
|
-
} catch {
|
|
19
|
-
// Malformed internal part, ignore it
|
|
20
|
-
}
|
|
21
|
-
} else if (state) {
|
|
22
|
-
try {
|
|
23
|
-
const decoded = JSON.parse(atob(state));
|
|
24
|
-
if (decoded.returnPathname) {
|
|
25
|
-
returnPathname = decoded.returnPathname;
|
|
26
|
-
} else {
|
|
27
|
-
userState = state;
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
userState = state;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
returnPathname,
|
|
35
|
-
state: userState,
|
|
36
|
-
};
|
|
10
|
+
function preventCaching(headers: Headers): void {
|
|
11
|
+
headers.set('Vary', 'Cookie');
|
|
12
|
+
setCachePreventionHeaders(headers);
|
|
37
13
|
}
|
|
38
14
|
|
|
39
15
|
export function handleAuth(options: HandleAuthOptions = {}) {
|
|
@@ -49,90 +25,117 @@ export function handleAuth(options: HandleAuthOptions = {}) {
|
|
|
49
25
|
}
|
|
50
26
|
|
|
51
27
|
return async function GET(request: NextRequest) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Cleanup params
|
|
72
|
-
url.searchParams.delete('code');
|
|
73
|
-
url.searchParams.delete('state');
|
|
74
|
-
|
|
75
|
-
// Redirect to the requested path and store the session
|
|
76
|
-
const returnPathname = returnPathnameState ?? returnPathnameOption;
|
|
77
|
-
|
|
78
|
-
// Extract the search params if they are present
|
|
79
|
-
if (returnPathname.includes('?')) {
|
|
80
|
-
const newUrl = new URL(returnPathname, 'https://example.com');
|
|
81
|
-
url.pathname = newUrl.pathname;
|
|
82
|
-
|
|
83
|
-
for (const [key, value] of newUrl.searchParams) {
|
|
84
|
-
url.searchParams.append(key, value);
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
url.pathname = returnPathname;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Fall back to standard Response if NextResponse is not available.
|
|
91
|
-
// This is to support Next.js 13.
|
|
92
|
-
const response = redirectWithFallback(url.toString());
|
|
93
|
-
|
|
94
|
-
if (!accessToken || !refreshToken) throw new Error('response is missing tokens');
|
|
95
|
-
|
|
96
|
-
await saveSession({ accessToken, refreshToken, user, impersonator }, request);
|
|
97
|
-
|
|
98
|
-
if (onSuccess) {
|
|
99
|
-
await onSuccess({
|
|
100
|
-
accessToken,
|
|
101
|
-
refreshToken,
|
|
102
|
-
user,
|
|
103
|
-
impersonator,
|
|
104
|
-
oauthTokens,
|
|
105
|
-
authenticationMethod,
|
|
106
|
-
organizationId,
|
|
107
|
-
state: customState,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return response;
|
|
112
|
-
} catch (error) {
|
|
113
|
-
const errorRes = {
|
|
114
|
-
error: error instanceof Error ? error.message : String(error),
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
console.error(errorRes);
|
|
118
|
-
|
|
119
|
-
return errorResponse(request, error);
|
|
28
|
+
// Always delete the PKCE cookie after handling the callback, regardless of success or error
|
|
29
|
+
// to avoid stale cookies affecting future auth attempts & prevent replays
|
|
30
|
+
const deleteCookie = `${PKCE_COOKIE_NAME}=; ${getPKCECookieOptions(request.url, true, true)}`;
|
|
31
|
+
|
|
32
|
+
// We want to catch any & all errors and respond the same way
|
|
33
|
+
// Firstly, by destroying the 1-use PKCE cookie to prevent replay attacks
|
|
34
|
+
// or stale cookies affecting future auth attempts
|
|
35
|
+
try {
|
|
36
|
+
// Fall back to standard URL parsing when nextUrl is not available (e.g., vinext)
|
|
37
|
+
const requestUrl = request.nextUrl ?? new URL(request.url);
|
|
38
|
+
|
|
39
|
+
// Gather mandatory information
|
|
40
|
+
const code = requestUrl.searchParams.get('code');
|
|
41
|
+
const state = requestUrl.searchParams.get('state');
|
|
42
|
+
const pkceCookie = request.cookies.get(PKCE_COOKIE_NAME)?.value;
|
|
43
|
+
|
|
44
|
+
if (!code || !state) {
|
|
45
|
+
throw new Error('Missing required auth parameter');
|
|
120
46
|
}
|
|
121
|
-
}
|
|
122
47
|
|
|
123
|
-
|
|
48
|
+
// CSRF verification: both channels (cookie + URL state) must be present and match
|
|
49
|
+
if (!pkceCookie) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'Auth cookie missing — cannot verify OAuth state. Ensure Set-Cookie headers are propagated on redirects.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (state !== pkceCookie) {
|
|
56
|
+
throw new Error('OAuth state mismatch');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
codeVerifier,
|
|
61
|
+
customState,
|
|
62
|
+
returnPathname: returnPathnameState,
|
|
63
|
+
} = await getStateFromPKCECookieValue(pkceCookie);
|
|
64
|
+
|
|
65
|
+
// Use the code returned to us by AuthKit and authenticate the user with WorkOS
|
|
66
|
+
const { accessToken, refreshToken, user, impersonator, oauthTokens, authenticationMethod, organizationId } =
|
|
67
|
+
await getWorkOS().userManagement.authenticateWithCode({
|
|
68
|
+
clientId: WORKOS_CLIENT_ID,
|
|
69
|
+
code,
|
|
70
|
+
codeVerifier,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!accessToken || !refreshToken) {
|
|
74
|
+
throw new Error('response is missing tokens');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If baseURL is provided, use it instead of request.nextUrl
|
|
78
|
+
// This is useful if the app is being run in a container like docker where
|
|
79
|
+
// the hostname can be different from the one in the request
|
|
80
|
+
const url = baseURL ? new URL(baseURL) : new URL(requestUrl.toString());
|
|
81
|
+
|
|
82
|
+
// Cleanup params
|
|
83
|
+
url.searchParams.delete('code');
|
|
84
|
+
url.searchParams.delete('state');
|
|
85
|
+
|
|
86
|
+
// Redirect to the requested path and store the session
|
|
87
|
+
const returnPathname = returnPathnameState ?? returnPathnameOption;
|
|
88
|
+
|
|
89
|
+
// Extract pathname and search params from returnPathname
|
|
90
|
+
const parsedReturnUrl = new URL(returnPathname, 'https://placeholder.com');
|
|
91
|
+
url.pathname = parsedReturnUrl.pathname;
|
|
92
|
+
url.search = parsedReturnUrl.search;
|
|
93
|
+
|
|
94
|
+
// Fall back to standard Response if NextResponse is not available.
|
|
95
|
+
// This is to support Next.js 13.
|
|
96
|
+
const response = redirectWithFallback(url.toString());
|
|
97
|
+
preventCaching(response.headers);
|
|
98
|
+
response.headers.append('Set-Cookie', deleteCookie);
|
|
99
|
+
|
|
100
|
+
await saveSession({ accessToken, refreshToken, user, impersonator }, request);
|
|
101
|
+
|
|
102
|
+
if (onSuccess) {
|
|
103
|
+
await onSuccess({
|
|
104
|
+
accessToken,
|
|
105
|
+
refreshToken,
|
|
106
|
+
user,
|
|
107
|
+
impersonator,
|
|
108
|
+
oauthTokens,
|
|
109
|
+
authenticationMethod,
|
|
110
|
+
organizationId,
|
|
111
|
+
state: customState,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return response;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('[AuthKit callback error]', error);
|
|
118
|
+
const response = await errorResponse(request, error);
|
|
119
|
+
response.headers.append('Set-Cookie', deleteCookie);
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
124
122
|
};
|
|
125
123
|
|
|
126
|
-
function errorResponse(request: NextRequest, error?: unknown) {
|
|
124
|
+
async function errorResponse(request: NextRequest, error?: unknown) {
|
|
127
125
|
if (onError) {
|
|
128
|
-
|
|
126
|
+
const response = await onError({ error, request });
|
|
127
|
+
preventCaching(response.headers);
|
|
128
|
+
return response;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
const response = errorResponseWithFallback({
|
|
132
132
|
error: {
|
|
133
133
|
message: 'Something went wrong',
|
|
134
134
|
description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.",
|
|
135
135
|
},
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
preventCaching(response.headers);
|
|
139
|
+
return response;
|
|
137
140
|
}
|
|
138
141
|
}
|