@everystack/auth 0.2.4 → 0.2.5
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/package.json +1 -1
- package/src/cookies.ts +7 -3
- package/src/index.ts +28 -0
package/package.json
CHANGED
package/src/cookies.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Access token cookie is sent on all requests (Path=/).
|
|
5
5
|
* Refresh token cookie is scoped to the refresh endpoint only (Path=/api/auth/refresh).
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Access token is SameSite=Lax — it must ride top-level SSR navigations into the
|
|
8
|
+
* app. Refresh token is SameSite=Strict — it is only ever hit by XHR at its scoped
|
|
9
|
+
* path, never a navigation target, so Strict kills its CSRF surface by construction
|
|
10
|
+
* (a cross-site page can't make the browser attach it at all).
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
export const COOKIE_NAMES = {
|
|
@@ -44,7 +48,7 @@ export function serializeAuthCookies(
|
|
|
44
48
|
|
|
45
49
|
return [
|
|
46
50
|
`${COOKIE_NAMES.token}=${token}; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Lax; Path=${basePath}; Max-Age=${tokenMaxAge}${domain}`,
|
|
47
|
-
`${COOKIE_NAMES.refresh}=${refreshToken}; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=
|
|
51
|
+
`${COOKIE_NAMES.refresh}=${refreshToken}; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Strict; Path=/api/auth/refresh; Max-Age=${refreshMaxAge}${domain}`,
|
|
48
52
|
];
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -58,7 +62,7 @@ export function clearAuthCookies(opts?: CookieOptions): string[] {
|
|
|
58
62
|
|
|
59
63
|
return [
|
|
60
64
|
`${COOKIE_NAMES.token}=; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Lax; Path=${basePath}; Max-Age=0${domain}`,
|
|
61
|
-
`${COOKIE_NAMES.refresh}=; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=
|
|
65
|
+
`${COOKIE_NAMES.refresh}=; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Strict; Path=/api/auth/refresh; Max-Age=0${domain}`,
|
|
62
66
|
];
|
|
63
67
|
}
|
|
64
68
|
|
package/src/index.ts
CHANGED
|
@@ -180,6 +180,32 @@ function csrfCheck(request: Request, cookies?: CookieConfig): Response | null {
|
|
|
180
180
|
return validateCsrfOrigin(request, cookies.allowedOrigins);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Fail-closed cookie config check, run once at handler construction.
|
|
185
|
+
*
|
|
186
|
+
* Cookie-authenticated requests carry a CSRF surface (the cookie is auto-sent).
|
|
187
|
+
* The Origin allowlist is the defense-in-depth behind SameSite — and it silently
|
|
188
|
+
* no-ops when `allowedOrigins` is empty, so enabling cookies without it would turn
|
|
189
|
+
* the CSRF check off without any signal. We refuse to start instead: a misconfig
|
|
190
|
+
* dies at deploy, not silently-allows until a request happens to hit the check.
|
|
191
|
+
*
|
|
192
|
+
* The `cookies: true` shorthand cannot carry an allowlist, so it is rejected —
|
|
193
|
+
* there is no construction-time same-origin to derive. Pass an explicit allowlist
|
|
194
|
+
* or omit cookies entirely for Bearer-only (mobile/API) auth.
|
|
195
|
+
*/
|
|
196
|
+
function assertSafeCookieConfig(cookies: CookieConfig | undefined): void {
|
|
197
|
+
if (!cookies) return; // cookie auth disabled (false/undefined) — Bearer-only, no CSRF surface
|
|
198
|
+
const allowedOrigins = cookies === true ? undefined : cookies.allowedOrigins;
|
|
199
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'[everystack/auth] Cookie authentication is enabled but cookies.allowedOrigins is empty. '
|
|
202
|
+
+ 'Cookie-authenticated requests carry a CSRF surface; an explicit Origin allowlist is '
|
|
203
|
+
+ 'required. Pass cookies: { allowedOrigins: ["https://your-web-origin"] } instead of '
|
|
204
|
+
+ 'cookies: true, or omit cookies for Bearer-only (mobile/API) auth.'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
183
209
|
/**
|
|
184
210
|
* Add mode-independent email confirmation and password management handlers.
|
|
185
211
|
* Called after base handlers are created (works with both node and postgres modes).
|
|
@@ -389,6 +415,8 @@ export function createAuthHandlers(
|
|
|
389
415
|
jwtSecret: Uint8Array,
|
|
390
416
|
options?: AuthOptions
|
|
391
417
|
): AuthHandlers {
|
|
418
|
+
assertSafeCookieConfig(options?.cookies);
|
|
419
|
+
|
|
392
420
|
if (options?.mode === 'postgres') {
|
|
393
421
|
const handlers = createPostgresAuthHandlers(db, schema, jwtSecret, options);
|
|
394
422
|
addEmailPasswordFlows(handlers, db, schema.users, options);
|