@workos-inc/authkit-nextjs 2.12.1 → 2.13.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 +128 -70
- package/dist/esm/errors.js +33 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +7 -2
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middleware-helpers.js +99 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/session.js +11 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/index.d.ts +3 -1
- package/dist/esm/types/middleware-helpers.d.ts +25 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +0 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/workos.js +1 -1
- package/package.json +2 -2
- package/src/components/authkit-provider.spec.tsx +41 -45
- package/src/components/min-max-button.spec.tsx +1 -0
- package/src/cookie.spec.ts +7 -1
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.ts +6 -10
- package/src/index.ts +16 -2
- package/src/middleware-helpers.spec.ts +231 -0
- package/src/middleware-helpers.ts +130 -0
- package/src/session.spec.ts +7 -10
- package/src/session.ts +16 -38
- package/src/utils.ts +0 -2
- package/src/workos.ts +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
handleAuthkitHeaders,
|
|
4
|
+
partitionAuthkitHeaders,
|
|
5
|
+
applyResponseHeaders,
|
|
6
|
+
isAuthkitRequestHeader,
|
|
7
|
+
AUTHKIT_REQUEST_HEADERS,
|
|
8
|
+
} from './middleware-helpers.js';
|
|
9
|
+
|
|
10
|
+
describe('middleware-helpers', () => {
|
|
11
|
+
function createMockRequest(url = 'https://example.com/test', method = 'GET'): NextRequest {
|
|
12
|
+
return new NextRequest(url, { method });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createAuthkitHeaders(): Headers {
|
|
16
|
+
const headers = new Headers();
|
|
17
|
+
headers.set('x-workos-middleware', 'true');
|
|
18
|
+
headers.set('x-workos-session', 'encrypted-session-data');
|
|
19
|
+
headers.set('x-url', 'https://example.com/test');
|
|
20
|
+
headers.set('set-cookie', 'wos-session=abc123; Path=/; HttpOnly');
|
|
21
|
+
headers.set('cache-control', 'private, no-cache');
|
|
22
|
+
headers.set('vary', 'Cookie');
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('isAuthkitRequestHeader', () => {
|
|
27
|
+
it('should recognize known headers and x-workos-* pattern', () => {
|
|
28
|
+
expect(isAuthkitRequestHeader('x-workos-middleware')).toBe(true);
|
|
29
|
+
expect(isAuthkitRequestHeader('x-workos-session')).toBe(true);
|
|
30
|
+
expect(isAuthkitRequestHeader('x-url')).toBe(true);
|
|
31
|
+
expect(isAuthkitRequestHeader('x-workos-future-header')).toBe(true);
|
|
32
|
+
// Case insensitive
|
|
33
|
+
expect(isAuthkitRequestHeader('X-WorkOS-Session')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should reject non-authkit headers', () => {
|
|
37
|
+
expect(isAuthkitRequestHeader('set-cookie')).toBe(false);
|
|
38
|
+
expect(isAuthkitRequestHeader('content-type')).toBe(false);
|
|
39
|
+
expect(isAuthkitRequestHeader('x-custom-header')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('partitionAuthkitHeaders', () => {
|
|
44
|
+
it('should split headers into request-only and response headers', () => {
|
|
45
|
+
const request = createMockRequest();
|
|
46
|
+
const authkitHeaders = createAuthkitHeaders();
|
|
47
|
+
|
|
48
|
+
const { requestHeaders, responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
49
|
+
|
|
50
|
+
// Request headers contain internal authkit headers
|
|
51
|
+
expect(requestHeaders.get('x-workos-session')).toBe('encrypted-session-data');
|
|
52
|
+
expect(requestHeaders.get('x-workos-middleware')).toBe('true');
|
|
53
|
+
|
|
54
|
+
// Response headers contain browser-safe headers only
|
|
55
|
+
expect(responseHeaders.get('set-cookie')).toBe('wos-session=abc123; Path=/; HttpOnly');
|
|
56
|
+
expect(responseHeaders.get('cache-control')).toBe('private, no-cache');
|
|
57
|
+
expect(responseHeaders.get('vary')).toBe('Cookie');
|
|
58
|
+
|
|
59
|
+
// Internal headers NOT in response
|
|
60
|
+
expect(responseHeaders.get('x-workos-session')).toBeNull();
|
|
61
|
+
expect(responseHeaders.get('x-workos-middleware')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should preserve original request headers while adding authkit headers', () => {
|
|
65
|
+
const request = createMockRequest();
|
|
66
|
+
request.headers.set('authorization', 'Bearer token');
|
|
67
|
+
request.headers.set('x-custom', 'value');
|
|
68
|
+
|
|
69
|
+
const { requestHeaders } = partitionAuthkitHeaders(request, createAuthkitHeaders());
|
|
70
|
+
|
|
71
|
+
expect(requestHeaders.get('authorization')).toBe('Bearer token');
|
|
72
|
+
expect(requestHeaders.get('x-custom')).toBe('value');
|
|
73
|
+
expect(requestHeaders.get('x-workos-session')).toBe('encrypted-session-data');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should filter response headers to allowlist only', () => {
|
|
77
|
+
const request = createMockRequest();
|
|
78
|
+
const authkitHeaders = new Headers();
|
|
79
|
+
authkitHeaders.set('set-cookie', 'session=abc');
|
|
80
|
+
authkitHeaders.set('x-dangerous-header', 'leaked');
|
|
81
|
+
authkitHeaders.set('location', 'https://evil.com');
|
|
82
|
+
|
|
83
|
+
const { responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
84
|
+
|
|
85
|
+
expect(responseHeaders.get('set-cookie')).toBe('session=abc');
|
|
86
|
+
expect(responseHeaders.get('x-dangerous-header')).toBeNull();
|
|
87
|
+
expect(responseHeaders.get('location')).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle multiple Set-Cookie headers correctly', () => {
|
|
91
|
+
const request = createMockRequest();
|
|
92
|
+
const authkitHeaders = new Headers();
|
|
93
|
+
authkitHeaders.append('set-cookie', 'cookie1=value1');
|
|
94
|
+
authkitHeaders.append('set-cookie', 'cookie2=value2');
|
|
95
|
+
|
|
96
|
+
const { responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
97
|
+
|
|
98
|
+
expect(responseHeaders.getSetCookie()).toHaveLength(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should auto-add cache-control: no-store when cookies present without cache-control', () => {
|
|
102
|
+
const request = createMockRequest();
|
|
103
|
+
const authkitHeaders = new Headers();
|
|
104
|
+
authkitHeaders.set('set-cookie', 'session=abc');
|
|
105
|
+
|
|
106
|
+
const { responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
107
|
+
|
|
108
|
+
expect(responseHeaders.get('cache-control')).toBe('no-store');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should deduplicate and merge Vary header values', () => {
|
|
112
|
+
const request = createMockRequest();
|
|
113
|
+
const authkitHeaders = new Headers();
|
|
114
|
+
authkitHeaders.append('vary', 'Cookie');
|
|
115
|
+
authkitHeaders.append('vary', 'Cookie, Accept');
|
|
116
|
+
|
|
117
|
+
const { responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
118
|
+
|
|
119
|
+
expect(responseHeaders.get('vary')).toBe('Cookie, Accept');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should forward x-middleware-cache header', () => {
|
|
123
|
+
const request = createMockRequest();
|
|
124
|
+
const authkitHeaders = new Headers();
|
|
125
|
+
authkitHeaders.set('x-middleware-cache', 'no-cache');
|
|
126
|
+
|
|
127
|
+
const { responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
128
|
+
|
|
129
|
+
expect(responseHeaders.get('x-middleware-cache')).toBe('no-cache');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should strip client-injected x-workos-* headers and use trusted values', () => {
|
|
133
|
+
const request = createMockRequest();
|
|
134
|
+
request.headers.set('x-workos-session', 'malicious-session');
|
|
135
|
+
request.headers.set('x-workos-admin-bypass', 'true');
|
|
136
|
+
|
|
137
|
+
const authkitHeaders = new Headers();
|
|
138
|
+
authkitHeaders.set('x-workos-session', 'real-session');
|
|
139
|
+
|
|
140
|
+
const { requestHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
141
|
+
|
|
142
|
+
expect(requestHeaders.get('x-workos-session')).toBe('real-session');
|
|
143
|
+
expect(requestHeaders.get('x-workos-admin-bypass')).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('handleAuthkitHeaders', () => {
|
|
148
|
+
it('should return NextResponse with response headers applied', () => {
|
|
149
|
+
const request = createMockRequest();
|
|
150
|
+
const response = handleAuthkitHeaders(request, createAuthkitHeaders());
|
|
151
|
+
|
|
152
|
+
expect(response).toBeInstanceOf(NextResponse);
|
|
153
|
+
expect(response.status).toBe(200);
|
|
154
|
+
expect(response.headers.get('set-cookie')).toBe('wos-session=abc123; Path=/; HttpOnly');
|
|
155
|
+
expect(response.headers.get('vary')).toBe('Cookie');
|
|
156
|
+
|
|
157
|
+
// Internal headers NOT leaked
|
|
158
|
+
for (const header of AUTHKIT_REQUEST_HEADERS) {
|
|
159
|
+
expect(response.headers.get(header)).toBeNull();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should redirect with normalized absolute URL', () => {
|
|
164
|
+
const request = createMockRequest('https://example.com/page');
|
|
165
|
+
const response = handleAuthkitHeaders(request, createAuthkitHeaders(), {
|
|
166
|
+
redirect: '/login',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(response.status).toBe(307);
|
|
170
|
+
expect(response.headers.get('location')).toBe('https://example.com/login');
|
|
171
|
+
expect(response.headers.get('set-cookie')).toBe('wos-session=abc123; Path=/; HttpOnly');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should use 307 for GET and 303 for POST redirects by default', () => {
|
|
175
|
+
const getRequest = createMockRequest('https://example.com/test', 'GET');
|
|
176
|
+
const postRequest = createMockRequest('https://example.com/test', 'POST');
|
|
177
|
+
const headers = createAuthkitHeaders();
|
|
178
|
+
|
|
179
|
+
const getResponse = handleAuthkitHeaders(getRequest, headers, { redirect: '/login' });
|
|
180
|
+
const postResponse = handleAuthkitHeaders(postRequest, headers, { redirect: '/login' });
|
|
181
|
+
|
|
182
|
+
expect(getResponse.status).toBe(307);
|
|
183
|
+
expect(postResponse.status).toBe(303);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should allow overriding redirect status', () => {
|
|
187
|
+
const request = createMockRequest();
|
|
188
|
+
const response = handleAuthkitHeaders(request, createAuthkitHeaders(), {
|
|
189
|
+
redirect: '/login',
|
|
190
|
+
redirectStatus: 302,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(response.status).toBe(302);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should throw clear error on invalid redirect URL', () => {
|
|
197
|
+
const request = createMockRequest();
|
|
198
|
+
|
|
199
|
+
expect(() =>
|
|
200
|
+
handleAuthkitHeaders(request, createAuthkitHeaders(), {
|
|
201
|
+
redirect: 'http://[invalid',
|
|
202
|
+
}),
|
|
203
|
+
).toThrow('Invalid redirect URL: "http://[invalid". Must be a valid absolute or relative URL.');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should treat empty/undefined redirect as no redirect', () => {
|
|
207
|
+
const request = createMockRequest();
|
|
208
|
+
const headers = createAuthkitHeaders();
|
|
209
|
+
|
|
210
|
+
expect(handleAuthkitHeaders(request, headers, { redirect: '' }).status).toBe(200);
|
|
211
|
+
expect(handleAuthkitHeaders(request, headers, { redirect: undefined }).status).toBe(200);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('applyResponseHeaders', () => {
|
|
216
|
+
it('should merge headers onto existing response', () => {
|
|
217
|
+
const response = NextResponse.next();
|
|
218
|
+
response.headers.set('vary', 'Accept');
|
|
219
|
+
response.headers.set('set-cookie', 'existing=value');
|
|
220
|
+
|
|
221
|
+
const headers = new Headers();
|
|
222
|
+
headers.set('vary', 'Cookie');
|
|
223
|
+
headers.set('set-cookie', 'new=value');
|
|
224
|
+
|
|
225
|
+
applyResponseHeaders(response, headers);
|
|
226
|
+
|
|
227
|
+
expect(response.headers.get('vary')).toBe('Accept, Cookie');
|
|
228
|
+
expect(response.headers.getSetCookie()).toHaveLength(2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/** Internal AuthKit headers - forwarded to downstream requests but never sent to browser. */
|
|
4
|
+
export const AUTHKIT_REQUEST_HEADERS = [
|
|
5
|
+
'x-workos-middleware',
|
|
6
|
+
'x-url',
|
|
7
|
+
'x-redirect-uri',
|
|
8
|
+
'x-sign-up-paths',
|
|
9
|
+
'x-workos-session',
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type AuthkitRequestHeader = (typeof AUTHKIT_REQUEST_HEADERS)[number];
|
|
13
|
+
|
|
14
|
+
const ALLOWED_RESPONSE_HEADERS: readonly string[] = [
|
|
15
|
+
'set-cookie',
|
|
16
|
+
'cache-control',
|
|
17
|
+
'vary',
|
|
18
|
+
'www-authenticate',
|
|
19
|
+
'proxy-authenticate',
|
|
20
|
+
'link',
|
|
21
|
+
'x-middleware-cache',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const MULTI_VALUE_HEADERS: readonly string[] = ['set-cookie', 'www-authenticate', 'proxy-authenticate', 'link'];
|
|
25
|
+
|
|
26
|
+
export function isAuthkitRequestHeader(name: string): boolean {
|
|
27
|
+
const lower = name.toLowerCase();
|
|
28
|
+
return (AUTHKIT_REQUEST_HEADERS as readonly string[]).includes(lower) || lower.startsWith('x-workos-');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setHeader(headers: Headers, name: string, value: string): void {
|
|
32
|
+
const lower = name.toLowerCase();
|
|
33
|
+
if (MULTI_VALUE_HEADERS.includes(lower)) {
|
|
34
|
+
headers.append(name, value);
|
|
35
|
+
} else if (lower === 'vary') {
|
|
36
|
+
const existing = headers.get(name);
|
|
37
|
+
const merged = new Set([
|
|
38
|
+
...(existing ? existing.split(',').map((v) => v.trim()) : []),
|
|
39
|
+
...value.split(',').map((v) => v.trim()),
|
|
40
|
+
]);
|
|
41
|
+
headers.set(name, [...merged].join(', '));
|
|
42
|
+
} else {
|
|
43
|
+
headers.set(name, value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AuthkitHeadersResult {
|
|
48
|
+
requestHeaders: Headers;
|
|
49
|
+
responseHeaders: Headers;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Partitions AuthKit headers into request headers (for withAuth) and response headers (for browser).
|
|
54
|
+
*/
|
|
55
|
+
export function partitionAuthkitHeaders(request: NextRequest, authkitHeaders: Headers): AuthkitHeadersResult {
|
|
56
|
+
const headers = new Headers(authkitHeaders);
|
|
57
|
+
const requestHeaders = new Headers(request.headers);
|
|
58
|
+
|
|
59
|
+
// Strip any client-injected authkit headers, then apply trusted ones
|
|
60
|
+
for (const name of [...requestHeaders.keys()]) {
|
|
61
|
+
if (isAuthkitRequestHeader(name)) {
|
|
62
|
+
requestHeaders.delete(name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const headerName of AUTHKIT_REQUEST_HEADERS) {
|
|
66
|
+
const value = headers.get(headerName);
|
|
67
|
+
if (value != null) {
|
|
68
|
+
requestHeaders.set(headerName, value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Build response headers from allowlist only
|
|
73
|
+
const responseHeaders = new Headers();
|
|
74
|
+
for (const [name, value] of headers) {
|
|
75
|
+
const lower = name.toLowerCase();
|
|
76
|
+
if (!isAuthkitRequestHeader(lower) && ALLOWED_RESPONSE_HEADERS.includes(lower)) {
|
|
77
|
+
setHeader(responseHeaders, name, value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-add cache-control when setting cookies
|
|
82
|
+
if (responseHeaders.has('set-cookie') && !responseHeaders.has('cache-control')) {
|
|
83
|
+
responseHeaders.set('cache-control', 'no-store');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { requestHeaders, responseHeaders };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function applyResponseHeaders(response: NextResponse, responseHeaders: Headers): NextResponse {
|
|
90
|
+
for (const [name, value] of responseHeaders) {
|
|
91
|
+
setHeader(response.headers, name, value);
|
|
92
|
+
}
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type AuthkitRedirectStatus = 302 | 303 | 307 | 308;
|
|
97
|
+
|
|
98
|
+
export interface HandleAuthkitHeadersOptions {
|
|
99
|
+
/** URL to redirect to (relative or absolute). */
|
|
100
|
+
redirect?: string | URL;
|
|
101
|
+
|
|
102
|
+
/** Redirect status code. @default 307 for GET/HEAD, 303 for POST/PUT/DELETE */
|
|
103
|
+
redirectStatus?: AuthkitRedirectStatus;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a NextResponse with properly merged AuthKit headers.
|
|
108
|
+
*/
|
|
109
|
+
export function handleAuthkitHeaders(
|
|
110
|
+
request: NextRequest,
|
|
111
|
+
authkitHeaders: Headers,
|
|
112
|
+
options: HandleAuthkitHeadersOptions = {},
|
|
113
|
+
): NextResponse {
|
|
114
|
+
const { requestHeaders, responseHeaders } = partitionAuthkitHeaders(request, authkitHeaders);
|
|
115
|
+
const { redirect, redirectStatus } = options;
|
|
116
|
+
|
|
117
|
+
if (redirect != null && redirect !== '') {
|
|
118
|
+
let redirectUrl: URL;
|
|
119
|
+
try {
|
|
120
|
+
redirectUrl = redirect instanceof URL ? redirect : new URL(redirect, request.url);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new Error(`Invalid redirect URL: "${redirect}". Must be a valid absolute or relative URL.`);
|
|
123
|
+
}
|
|
124
|
+
const method = request.method.toUpperCase();
|
|
125
|
+
const status = redirectStatus ?? (method === 'GET' || method === 'HEAD' ? 307 : 303);
|
|
126
|
+
return applyResponseHeaders(NextResponse.redirect(redirectUrl, status), responseHeaders);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return applyResponseHeaders(NextResponse.next({ request: { headers: requestHeaders } }), responseHeaders);
|
|
130
|
+
}
|
package/src/session.spec.ts
CHANGED
|
@@ -374,10 +374,7 @@ describe('session.ts', () => {
|
|
|
374
374
|
);
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
-
it('should
|
|
378
|
-
const originalRedirect = NextResponse.redirect;
|
|
379
|
-
(NextResponse as Partial<typeof NextResponse>).redirect = undefined;
|
|
380
|
-
|
|
377
|
+
it('should return a redirect response when middlewareAuth is enabled and user is not authenticated', async () => {
|
|
381
378
|
const request = new NextRequest(new URL('http://example.com/protected'));
|
|
382
379
|
const result = await updateSessionMiddleware(
|
|
383
380
|
request,
|
|
@@ -390,10 +387,9 @@ describe('session.ts', () => {
|
|
|
390
387
|
[],
|
|
391
388
|
);
|
|
392
389
|
|
|
393
|
-
expect(result).toBeInstanceOf(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
(NextResponse as Partial<typeof NextResponse>).redirect = originalRedirect;
|
|
390
|
+
expect(result).toBeInstanceOf(NextResponse);
|
|
391
|
+
expect(result.status).toBe(307);
|
|
392
|
+
expect(result.headers.get('Location')).toContain('workos.com');
|
|
397
393
|
});
|
|
398
394
|
|
|
399
395
|
it('should automatically add the redirect URI to unauthenticatedPaths when middleware is enabled', async () => {
|
|
@@ -429,7 +425,7 @@ describe('session.ts', () => {
|
|
|
429
425
|
expect(result.headers.get('Location')).toContain('screen_hint=sign-up');
|
|
430
426
|
});
|
|
431
427
|
|
|
432
|
-
it('should
|
|
428
|
+
it('should not leak sign-up paths header to the browser', async () => {
|
|
433
429
|
const request = new NextRequest(new URL('http://example.com/protected-signup'));
|
|
434
430
|
const result = await updateSessionMiddleware(
|
|
435
431
|
request,
|
|
@@ -442,7 +438,8 @@ describe('session.ts', () => {
|
|
|
442
438
|
['/protected-signup'],
|
|
443
439
|
);
|
|
444
440
|
|
|
445
|
-
|
|
441
|
+
// x-sign-up-paths is an internal header that should not leak to the browser
|
|
442
|
+
expect(result.headers.get('x-sign-up-paths')).toBeNull();
|
|
446
443
|
});
|
|
447
444
|
|
|
448
445
|
it('should allow logged out users on unauthenticated paths', async () => {
|
package/src/session.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { sealData, unsealData } from 'iron-session';
|
|
|
4
4
|
import { JWTPayload, createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
|
|
5
5
|
import { cookies, headers } from 'next/headers';
|
|
6
6
|
import { redirect } from 'next/navigation';
|
|
7
|
-
import { NextRequest
|
|
7
|
+
import { NextRequest } from 'next/server';
|
|
8
8
|
import { getCookieOptions, getJwtCookie } from './cookie.js';
|
|
9
9
|
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_NAME, WORKOS_COOKIE_PASSWORD, WORKOS_REDIRECT_URI } from './env-variables.js';
|
|
10
|
+
import { TokenRefreshError, getSessionErrorContext } from './errors.js';
|
|
10
11
|
import { getAuthorizationUrl } from './get-authorization-url.js';
|
|
11
12
|
import {
|
|
12
13
|
AccessToken,
|
|
@@ -21,7 +22,8 @@ import { getWorkOS } from './workos.js';
|
|
|
21
22
|
|
|
22
23
|
import type { AuthenticationResponse } from '@workos-inc/node';
|
|
23
24
|
import { parse, tokensToRegexp } from 'path-to-regexp';
|
|
24
|
-
import {
|
|
25
|
+
import { handleAuthkitHeaders } from './middleware-helpers.js';
|
|
26
|
+
import { lazy, setCachePreventionHeaders } from './utils.js';
|
|
25
27
|
|
|
26
28
|
const sessionHeaderName = 'x-workos-session';
|
|
27
29
|
const middlewareHeaderName = 'x-workos-middleware';
|
|
@@ -149,15 +151,6 @@ async function updateSessionMiddleware(
|
|
|
149
151
|
eagerAuth,
|
|
150
152
|
});
|
|
151
153
|
|
|
152
|
-
// If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
|
|
153
|
-
if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
|
|
154
|
-
if (debug) {
|
|
155
|
-
console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return redirectWithFallback(authorizationUrl as string, headers);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
154
|
// Record the sign up paths so we can use them later
|
|
162
155
|
if (signUpPaths.length > 0) {
|
|
163
156
|
headers.set(signUpPathsHeaderName, signUpPaths.join(','));
|
|
@@ -165,33 +158,16 @@ async function updateSessionMiddleware(
|
|
|
165
158
|
|
|
166
159
|
applyCacheSecurityHeaders(headers, request, session);
|
|
167
160
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
requestHeaders.set('x-redirect-uri', headers.get('x-redirect-uri')!);
|
|
174
|
-
}
|
|
175
|
-
if (headers.has(signUpPathsHeaderName)) {
|
|
176
|
-
requestHeaders.set(signUpPathsHeaderName, headers.get(signUpPathsHeaderName)!);
|
|
177
|
-
}
|
|
161
|
+
// If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
|
|
162
|
+
if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
|
|
163
|
+
if (debug) {
|
|
164
|
+
console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
|
|
165
|
+
}
|
|
178
166
|
|
|
179
|
-
|
|
180
|
-
// This ensures handlers see refreshed sessions immediately (before Set-Cookie reaches browser)
|
|
181
|
-
const sessionHeader = headers.get(sessionHeaderName);
|
|
182
|
-
if (sessionHeader) {
|
|
183
|
-
requestHeaders.set(sessionHeaderName, sessionHeader);
|
|
167
|
+
return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl as string });
|
|
184
168
|
}
|
|
185
169
|
|
|
186
|
-
|
|
187
|
-
headers.delete(sessionHeaderName);
|
|
188
|
-
|
|
189
|
-
return NextResponse.next({
|
|
190
|
-
request: {
|
|
191
|
-
headers: requestHeaders,
|
|
192
|
-
},
|
|
193
|
-
headers,
|
|
194
|
-
});
|
|
170
|
+
return handleAuthkitHeaders(request, headers);
|
|
195
171
|
}
|
|
196
172
|
|
|
197
173
|
async function updateSession(
|
|
@@ -406,9 +382,11 @@ async function refreshSession({
|
|
|
406
382
|
organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
|
|
407
383
|
});
|
|
408
384
|
} catch (error) {
|
|
409
|
-
throw new
|
|
410
|
-
|
|
411
|
-
|
|
385
|
+
throw new TokenRefreshError(
|
|
386
|
+
`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
|
|
387
|
+
error,
|
|
388
|
+
getSessionErrorContext(session),
|
|
389
|
+
);
|
|
412
390
|
}
|
|
413
391
|
|
|
414
392
|
const headersList = await headers();
|
package/src/utils.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { NextResponse } from 'next/server';
|
|
|
6
6
|
*/
|
|
7
7
|
export function setCachePreventionHeaders(headers: Headers): void {
|
|
8
8
|
headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
|
|
9
|
-
headers.set('Pragma', 'no-cache');
|
|
10
|
-
headers.set('Expires', '0');
|
|
11
9
|
headers.set('x-middleware-cache', 'no-cache');
|
|
12
10
|
}
|
|
13
11
|
|
package/src/workos.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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
|
import { lazy } from './utils.js';
|
|
4
4
|
|
|
5
|
-
export const VERSION = '2.
|
|
5
|
+
export const VERSION = '2.13.0';
|
|
6
6
|
|
|
7
7
|
const options = {
|
|
8
8
|
apiHostname: WORKOS_API_HOSTNAME,
|