@nifrajs/auth 3.3.0 → 4.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/dist/csrf.d.ts +4 -4
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +17 -4
- package/dist/csrf.js.map +1 -1
- package/dist/session.d.ts +2 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +9 -4
- package/dist/session.js.map +1 -1
- package/package.json +5 -2
- package/src/csrf.ts +16 -8
- package/src/session.ts +19 -4
package/dist/csrf.d.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
|
|
5
5
|
* methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type Middleware } from "@nifrajs/core/server";
|
|
8
8
|
export interface CsrfOptions {
|
|
9
9
|
/**
|
|
10
|
-
* Allowed origins (e.g. `["https://example.com"]`).
|
|
11
|
-
*
|
|
12
|
-
* proxy
|
|
10
|
+
* Allowed origins (e.g. `["https://example.com"]`). When omitted, the request must be same-origin by
|
|
11
|
+
* host (an `https:` page reaching an `http:` request URL is accepted, so a TLS-terminating proxy
|
|
12
|
+
* works). Set it when the proxy rewrites `Host`, so the public origin differs from the worker's.
|
|
13
13
|
*/
|
|
14
14
|
readonly origins?: readonly string[];
|
|
15
15
|
}
|
package/dist/csrf.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAQ3E,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAKD,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CA+B1D"}
|
package/dist/csrf.js
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF protection via an **Origin / Referer check** on state-changing requests - OWASP's recommended
|
|
3
|
+
* defense for cookie-authenticated apps. A browser always attaches `Origin` (or at least `Referer`) to
|
|
4
|
+
* a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
|
|
5
|
+
* methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
|
|
6
|
+
*/
|
|
7
|
+
import { isSameOriginRequest } from "@nifrajs/core/server";
|
|
1
8
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
9
|
+
// A name per instance: `use()` skips a name it already applied, which would drop a second, stricter
|
|
10
|
+
// `csrf()` inside a `group()`.
|
|
11
|
+
let instances = 0;
|
|
2
12
|
const forbidden = () => Response.json({ ok: false, error: "csrf_failed" }, { status: 403 });
|
|
3
13
|
export function csrf(options = {}) {
|
|
4
14
|
const configured = options.origins !== undefined ? new Set(options.origins) : undefined;
|
|
15
|
+
instances += 1;
|
|
5
16
|
return {
|
|
6
|
-
name:
|
|
17
|
+
name: `csrf#${instances}`,
|
|
7
18
|
onRequest(req) {
|
|
8
19
|
if (SAFE_METHODS.has(req.method))
|
|
9
20
|
return undefined;
|
|
10
|
-
|
|
21
|
+
// The same-origin default uses core's check, which accepts an `https:` page reaching an `http:`
|
|
22
|
+
// request URL on the same host - what every TLS-terminating proxy looks like from here.
|
|
23
|
+
const allowed = (candidate) => configured !== undefined ? configured.has(candidate) : isSameOriginRequest(candidate, req);
|
|
11
24
|
const origin = req.headers.get("origin");
|
|
12
25
|
if (origin !== null)
|
|
13
|
-
return allowed
|
|
26
|
+
return allowed(origin) ? undefined : forbidden();
|
|
14
27
|
// Some same-origin requests omit `Origin` - fall back to the `Referer`'s origin.
|
|
15
28
|
const referer = req.headers.get("referer");
|
|
16
29
|
if (referer !== null) {
|
|
@@ -21,7 +34,7 @@ export function csrf(options = {}) {
|
|
|
21
34
|
catch {
|
|
22
35
|
return forbidden(); // malformed Referer
|
|
23
36
|
}
|
|
24
|
-
return allowed
|
|
37
|
+
return allowed(refererOrigin) ? undefined : forbidden();
|
|
25
38
|
}
|
|
26
39
|
// A state-changing request with neither header → reject (fail closed; a browser always sends one).
|
|
27
40
|
return forbidden();
|
package/dist/csrf.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAmB,MAAM,sBAAsB,CAAA;AAE3E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;AAExD,oGAAoG;AACpG,+BAA+B;AAC/B,IAAI,SAAS,GAAG,CAAC,CAAA;AAWjB,MAAM,SAAS,GAAG,GAAa,EAAE,CAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;AAErE,MAAM,UAAU,IAAI,CAAC,UAAuB,EAAE;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACvF,SAAS,IAAI,CAAC,CAAA;IACd,OAAO;QACL,IAAI,EAAE,QAAQ,SAAS,EAAE;QACzB,SAAS,CAAC,GAAG;YACX,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,SAAS,CAAA;YAClD,gGAAgG;YAChG,wFAAwF;YACxF,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAW,EAAE,CAC7C,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;YAE5F,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACxC,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YAErE,iFAAiF;YACjF,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC1C,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,IAAI,aAAqB,CAAA;gBACzB,IAAI,CAAC;oBACH,aAAa,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAA;gBACzC,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,SAAS,EAAE,CAAA,CAAC,oBAAoB;gBACzC,CAAC;gBACD,OAAO,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YACzD,CAAC;YAED,mGAAmG;YACnG,OAAO,SAAS,EAAE,CAAA;QACpB,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/session.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type { SessionStore } from "./store.js";
|
|
|
15
15
|
* `c` satisfies it and it's testable with a stub. */
|
|
16
16
|
export interface SessionContext {
|
|
17
17
|
readonly cookies: Readonly<Record<string, string>>;
|
|
18
|
+
/** Optional raw request, used to reject ambiguous duplicate session cookies. */
|
|
19
|
+
readonly request?: Request;
|
|
18
20
|
readonly set: {
|
|
19
21
|
cookie(name: string, value: string, options?: CookieOptions): void;
|
|
20
22
|
deleteCookie(name: string, options?: {
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/dist/session.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* cookie is always `HttpOnly`. Reads **fail closed** - a tampered/missing/expired cookie yields a fresh
|
|
10
10
|
* anonymous session, never an error.
|
|
11
11
|
*/
|
|
12
|
-
import { parseCookies, signValue, unsignValue } from "@nifrajs/core/cookies";
|
|
12
|
+
import { hasDuplicateCookie, parseCookies, signValue, unsignValue, } from "@nifrajs/core/cookies";
|
|
13
13
|
const DAY_SECONDS = 86_400;
|
|
14
14
|
// 256-bit floor, matching @nifrajs/core's signed-cookie/webhook guard. Sessions sign via core's
|
|
15
15
|
// signValue (which enforces the same bar); this early check gives an auth-specific message before
|
|
@@ -124,10 +124,15 @@ export function createSessions(options) {
|
|
|
124
124
|
return make({});
|
|
125
125
|
return make({ data: payload.data, expiresAt: payload.expiresAt });
|
|
126
126
|
};
|
|
127
|
-
const
|
|
127
|
+
const sessionCookie = (c) => c.request !== undefined && hasDuplicateCookie(c.request.headers.get("cookie"), cookieName)
|
|
128
|
+
? undefined
|
|
129
|
+
: c.cookies[cookieName];
|
|
130
|
+
const get = (c) => loadFromRaw(sessionCookie(c));
|
|
128
131
|
// Read-only load from a raw Request - for @nifrajs/web loaders, which have the request but can't write
|
|
129
132
|
// cookies (commit/destroy belong in a route/action with the full Context). Same verify + expiry.
|
|
130
|
-
const read = (request) =>
|
|
133
|
+
const read = (request) => hasDuplicateCookie(request.headers.get("cookie"), cookieName)
|
|
134
|
+
? loadFromRaw(undefined)
|
|
135
|
+
: loadFromRaw(parseCookies(request.headers.get("cookie"))[cookieName]);
|
|
131
136
|
const writeCookie = (c, value, expiresAt) => {
|
|
132
137
|
const maxAgeSeconds = Math.max(0, Math.floor((expiresAt - now()) / 1000));
|
|
133
138
|
c.set.cookie(cookieName, value, { ...cookieOpts, maxAge: maxAgeSeconds });
|
|
@@ -163,7 +168,7 @@ export function createSessions(options) {
|
|
|
163
168
|
else {
|
|
164
169
|
// Revoke the record addressed by the signed request cookie even when the caller did not
|
|
165
170
|
// first load a Session object. The signature check prevents deleting attacker-chosen IDs.
|
|
166
|
-
const raw = c
|
|
171
|
+
const raw = sessionCookie(c);
|
|
167
172
|
if (raw !== undefined)
|
|
168
173
|
id = (await unsignValue(raw, secret)) ?? undefined;
|
|
169
174
|
}
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Session primitives for nifra - signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"route-guards"
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
|
+
"nifra": {
|
|
18
|
+
"environment": "library"
|
|
19
|
+
},
|
|
17
20
|
"main": "./dist/index.js",
|
|
18
21
|
"module": "./dist/index.js",
|
|
19
22
|
"types": "./dist/index.d.ts",
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"access": "public"
|
|
38
41
|
},
|
|
39
42
|
"peerDependencies": {
|
|
40
|
-
"@nifrajs/core": "^
|
|
43
|
+
"@nifrajs/core": "^4.0.0"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"@nifrajs/core": "workspace:*"
|
package/src/csrf.ts
CHANGED
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
|
|
5
5
|
* methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { isSameOriginRequest, type Middleware } from "@nifrajs/core/server"
|
|
8
8
|
|
|
9
9
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"])
|
|
10
10
|
|
|
11
|
+
// A name per instance: `use()` skips a name it already applied, which would drop a second, stricter
|
|
12
|
+
// `csrf()` inside a `group()`.
|
|
13
|
+
let instances = 0
|
|
14
|
+
|
|
11
15
|
export interface CsrfOptions {
|
|
12
16
|
/**
|
|
13
|
-
* Allowed origins (e.g. `["https://example.com"]`).
|
|
14
|
-
*
|
|
15
|
-
* proxy
|
|
17
|
+
* Allowed origins (e.g. `["https://example.com"]`). When omitted, the request must be same-origin by
|
|
18
|
+
* host (an `https:` page reaching an `http:` request URL is accepted, so a TLS-terminating proxy
|
|
19
|
+
* works). Set it when the proxy rewrites `Host`, so the public origin differs from the worker's.
|
|
16
20
|
*/
|
|
17
21
|
readonly origins?: readonly string[]
|
|
18
22
|
}
|
|
@@ -22,14 +26,18 @@ const forbidden = (): Response =>
|
|
|
22
26
|
|
|
23
27
|
export function csrf(options: CsrfOptions = {}): Middleware {
|
|
24
28
|
const configured = options.origins !== undefined ? new Set(options.origins) : undefined
|
|
29
|
+
instances += 1
|
|
25
30
|
return {
|
|
26
|
-
name:
|
|
31
|
+
name: `csrf#${instances}`,
|
|
27
32
|
onRequest(req) {
|
|
28
33
|
if (SAFE_METHODS.has(req.method)) return undefined
|
|
29
|
-
|
|
34
|
+
// The same-origin default uses core's check, which accepts an `https:` page reaching an `http:`
|
|
35
|
+
// request URL on the same host - what every TLS-terminating proxy looks like from here.
|
|
36
|
+
const allowed = (candidate: string): boolean =>
|
|
37
|
+
configured !== undefined ? configured.has(candidate) : isSameOriginRequest(candidate, req)
|
|
30
38
|
|
|
31
39
|
const origin = req.headers.get("origin")
|
|
32
|
-
if (origin !== null) return allowed
|
|
40
|
+
if (origin !== null) return allowed(origin) ? undefined : forbidden()
|
|
33
41
|
|
|
34
42
|
// Some same-origin requests omit `Origin` - fall back to the `Referer`'s origin.
|
|
35
43
|
const referer = req.headers.get("referer")
|
|
@@ -40,7 +48,7 @@ export function csrf(options: CsrfOptions = {}): Middleware {
|
|
|
40
48
|
} catch {
|
|
41
49
|
return forbidden() // malformed Referer
|
|
42
50
|
}
|
|
43
|
-
return allowed
|
|
51
|
+
return allowed(refererOrigin) ? undefined : forbidden()
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
// A state-changing request with neither header → reject (fail closed; a browser always sends one).
|
package/src/session.ts
CHANGED
|
@@ -9,13 +9,21 @@
|
|
|
9
9
|
* cookie is always `HttpOnly`. Reads **fail closed** - a tampered/missing/expired cookie yields a fresh
|
|
10
10
|
* anonymous session, never an error.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
type CookieOptions,
|
|
14
|
+
hasDuplicateCookie,
|
|
15
|
+
parseCookies,
|
|
16
|
+
signValue,
|
|
17
|
+
unsignValue,
|
|
18
|
+
} from "@nifrajs/core/cookies"
|
|
13
19
|
import type { SessionRecord, SessionStore } from "./store.ts"
|
|
14
20
|
|
|
15
21
|
/** The cookie + response surface the manager needs - a structural subset of nifra's `Context`, so any
|
|
16
22
|
* `c` satisfies it and it's testable with a stub. */
|
|
17
23
|
export interface SessionContext {
|
|
18
24
|
readonly cookies: Readonly<Record<string, string>>
|
|
25
|
+
/** Optional raw request, used to reject ambiguous duplicate session cookies. */
|
|
26
|
+
readonly request?: Request
|
|
19
27
|
readonly set: {
|
|
20
28
|
cookie(name: string, value: string, options?: CookieOptions): void
|
|
21
29
|
deleteCookie(name: string, options?: { readonly path?: string; readonly domain?: string }): void
|
|
@@ -210,12 +218,19 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
210
218
|
return make({ data: payload.data, expiresAt: payload.expiresAt })
|
|
211
219
|
}
|
|
212
220
|
|
|
213
|
-
const
|
|
221
|
+
const sessionCookie = (c: SessionContext): string | undefined =>
|
|
222
|
+
c.request !== undefined && hasDuplicateCookie(c.request.headers.get("cookie"), cookieName)
|
|
223
|
+
? undefined
|
|
224
|
+
: c.cookies[cookieName]
|
|
225
|
+
|
|
226
|
+
const get = (c: SessionContext): Promise<Session<Data>> => loadFromRaw(sessionCookie(c))
|
|
214
227
|
|
|
215
228
|
// Read-only load from a raw Request - for @nifrajs/web loaders, which have the request but can't write
|
|
216
229
|
// cookies (commit/destroy belong in a route/action with the full Context). Same verify + expiry.
|
|
217
230
|
const read = (request: Request): Promise<Session<Data>> =>
|
|
218
|
-
|
|
231
|
+
hasDuplicateCookie(request.headers.get("cookie"), cookieName)
|
|
232
|
+
? loadFromRaw(undefined)
|
|
233
|
+
: loadFromRaw(parseCookies(request.headers.get("cookie"))[cookieName])
|
|
219
234
|
|
|
220
235
|
const writeCookie = (c: SessionContext, value: string, expiresAt: number): void => {
|
|
221
236
|
const maxAgeSeconds = Math.max(0, Math.floor((expiresAt - now()) / 1000))
|
|
@@ -257,7 +272,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
257
272
|
} else {
|
|
258
273
|
// Revoke the record addressed by the signed request cookie even when the caller did not
|
|
259
274
|
// first load a Session object. The signature check prevents deleting attacker-chosen IDs.
|
|
260
|
-
const raw = c
|
|
275
|
+
const raw = sessionCookie(c)
|
|
261
276
|
if (raw !== undefined) id = (await unsignValue(raw, secret)) ?? undefined
|
|
262
277
|
}
|
|
263
278
|
if (id !== undefined) await store.delete(id)
|