@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.1
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 +305 -102
- package/dist/esm/actions.js +35 -5
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +71 -21
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +90 -92
- 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 +20 -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 +52 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +82 -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 +9 -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 +17 -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 +33 -34
- package/src/actions.spec.ts +91 -18
- package/src/actions.ts +44 -6
- package/src/auth.spec.ts +79 -29
- package/src/auth.ts +74 -42
- package/src/authkit-callback-route.spec.ts +372 -58
- package/src/authkit-callback-route.ts +121 -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 +63 -9
- package/src/cookie.ts +35 -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 +146 -0
- package/src/pkce.ts +59 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +104 -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, getPKCECookieNameForState, 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,132 @@ export function handleAuth(options: HandleAuthOptions = {}) {
|
|
|
49
25
|
}
|
|
50
26
|
|
|
51
27
|
return async function GET(request: NextRequest) {
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
28
|
+
// Fall back to standard URL parsing when nextUrl is not available (e.g., vinext)
|
|
29
|
+
const requestUrl = request.nextUrl ?? new URL(request.url);
|
|
30
|
+
|
|
31
|
+
// Gather mandatory information
|
|
32
|
+
const code = requestUrl.searchParams.get('code');
|
|
33
|
+
const state = requestUrl.searchParams.get('state');
|
|
34
|
+
|
|
35
|
+
// We want to catch any & all errors and respond the same way, always
|
|
36
|
+
// destroying the 1-use PKCE cookie to prevent replay attacks or stale
|
|
37
|
+
// cookies affecting future auth attempts.
|
|
38
|
+
try {
|
|
39
|
+
if (!code || !state) {
|
|
40
|
+
throw new Error('Missing required auth parameter');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Derive the flow-specific cookie name from the state param so each
|
|
44
|
+
// concurrent auth flow reads/deletes its own cookie, not a shared one.
|
|
45
|
+
// Fall back to the legacy shared cookie name so in-flight OAuth flows
|
|
46
|
+
// started on v3.0.x don't fail on the first callback after upgrade.
|
|
47
|
+
// Safe to remove once v3.0.x is unsupported.
|
|
48
|
+
const pkceCookieName = getPKCECookieNameForState(state);
|
|
49
|
+
const pkceCookie = request.cookies.get(pkceCookieName)?.value ?? request.cookies.get(PKCE_COOKIE_NAME)?.value;
|
|
50
|
+
|
|
51
|
+
// CSRF verification: both channels (cookie + URL state) must be present and match
|
|
52
|
+
if (!pkceCookie) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'Auth cookie missing — cannot verify OAuth state. Ensure Set-Cookie headers are propagated on redirects.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (state !== pkceCookie) {
|
|
59
|
+
throw new Error('OAuth state mismatch');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const {
|
|
63
|
+
codeVerifier,
|
|
64
|
+
customState,
|
|
65
|
+
returnPathname: returnPathnameState,
|
|
66
|
+
} = await getStateFromPKCECookieValue(pkceCookie);
|
|
67
|
+
|
|
68
|
+
// Use the code returned to us by AuthKit and authenticate the user with WorkOS
|
|
69
|
+
const { accessToken, refreshToken, user, impersonator, oauthTokens, authenticationMethod, organizationId } =
|
|
70
|
+
await getWorkOS().userManagement.authenticateWithCode({
|
|
71
|
+
clientId: WORKOS_CLIENT_ID,
|
|
72
|
+
code,
|
|
73
|
+
codeVerifier,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!accessToken || !refreshToken) {
|
|
77
|
+
throw new Error('response is missing tokens');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If baseURL is provided, use it instead of request.nextUrl
|
|
81
|
+
// This is useful if the app is being run in a container like docker where
|
|
82
|
+
// the hostname can be different from the one in the request
|
|
83
|
+
const url = baseURL ? new URL(baseURL) : new URL(requestUrl.toString());
|
|
84
|
+
|
|
85
|
+
// Cleanup params
|
|
86
|
+
url.searchParams.delete('code');
|
|
87
|
+
url.searchParams.delete('state');
|
|
88
|
+
|
|
89
|
+
// Redirect to the requested path and store the session
|
|
90
|
+
const returnPathname = returnPathnameState ?? returnPathnameOption;
|
|
91
|
+
|
|
92
|
+
// Extract pathname and search params from returnPathname
|
|
93
|
+
const parsedReturnUrl = new URL(returnPathname, 'https://placeholder.com');
|
|
94
|
+
url.pathname = parsedReturnUrl.pathname;
|
|
95
|
+
url.search = parsedReturnUrl.search;
|
|
96
|
+
|
|
97
|
+
// Fall back to standard Response if NextResponse is not available.
|
|
98
|
+
// This is to support Next.js 13.
|
|
99
|
+
const response = redirectWithFallback(url.toString());
|
|
100
|
+
preventCaching(response.headers);
|
|
101
|
+
|
|
102
|
+
// Always delete the PKCE cookie after handling the callback, regardless of success or error
|
|
103
|
+
// to avoid stale cookies affecting future auth attempts & prevent replays
|
|
104
|
+
response.headers.append('Set-Cookie', `${pkceCookieName}=; ${getPKCECookieOptions(request.url, true, true)}`);
|
|
105
|
+
|
|
106
|
+
await saveSession({ accessToken, refreshToken, user, impersonator }, request);
|
|
107
|
+
|
|
108
|
+
if (onSuccess) {
|
|
109
|
+
await onSuccess({
|
|
110
|
+
accessToken,
|
|
111
|
+
refreshToken,
|
|
112
|
+
user,
|
|
113
|
+
impersonator,
|
|
114
|
+
oauthTokens,
|
|
115
|
+
authenticationMethod,
|
|
116
|
+
organizationId,
|
|
117
|
+
state: customState,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return response;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[AuthKit callback error]', error);
|
|
124
|
+
const response = await errorResponse(request, error);
|
|
125
|
+
|
|
126
|
+
// Always delete the PKCE cookie after handling the callback, regardless of success or error
|
|
127
|
+
// to avoid stale cookies affecting future auth attempts & prevent replays
|
|
128
|
+
if (state) {
|
|
129
|
+
response.headers.append(
|
|
130
|
+
'Set-Cookie',
|
|
131
|
+
`${getPKCECookieNameForState(state)}=; ${getPKCECookieOptions(request.url, true, true)}`,
|
|
132
|
+
);
|
|
120
133
|
}
|
|
121
|
-
}
|
|
122
134
|
|
|
123
|
-
|
|
135
|
+
return response;
|
|
136
|
+
}
|
|
124
137
|
};
|
|
125
138
|
|
|
126
|
-
function errorResponse(request: NextRequest, error?: unknown) {
|
|
139
|
+
async function errorResponse(request: NextRequest, error?: unknown) {
|
|
127
140
|
if (onError) {
|
|
128
|
-
|
|
141
|
+
const response = await onError({ error, request });
|
|
142
|
+
preventCaching(response.headers);
|
|
143
|
+
return response;
|
|
129
144
|
}
|
|
130
145
|
|
|
131
|
-
|
|
146
|
+
const response = errorResponseWithFallback({
|
|
132
147
|
error: {
|
|
133
148
|
message: 'Something went wrong',
|
|
134
149
|
description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.",
|
|
135
150
|
},
|
|
136
151
|
});
|
|
152
|
+
|
|
153
|
+
preventCaching(response.headers);
|
|
154
|
+
return response;
|
|
137
155
|
}
|
|
138
156
|
}
|