@nifrajs/auth 5.6.0 → 5.8.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/CHANGELOG.md +13 -0
- package/dist/csrf.d.ts +8 -6
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +18 -2
- package/dist/csrf.js.map +1 -1
- package/package.json +2 -2
- package/src/csrf.ts +24 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @nifrajs/auth
|
|
2
2
|
|
|
3
|
+
## 5.8.0
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 5.7.0
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- b141cf4: `csrf()` takes `exempt`: route patterns whose requests skip the Origin check, for a route no browser
|
|
12
|
+
calls that verifies its sender another way, such as a signed payment webhook arriving with no
|
|
13
|
+
`Origin`. Patterns are written as routes are (`/inbound/:vendor`) and matched against the path the
|
|
14
|
+
server routes, so any other path stays checked.
|
|
15
|
+
|
|
3
16
|
## 5.6.0
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/csrf.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
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
1
|
import { type Middleware } from "@nifrajs/core/server";
|
|
8
2
|
export interface CsrfOptions {
|
|
9
3
|
/**
|
|
@@ -12,6 +6,14 @@ export interface CsrfOptions {
|
|
|
12
6
|
* works). Set it when the proxy rewrites `Host`, so the public origin differs from the worker's.
|
|
13
7
|
*/
|
|
14
8
|
readonly origins?: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* Route patterns whose requests skip the check, for a route no browser calls that proves its sender
|
|
11
|
+
* another way - a signed payment webhook arrives with no `Origin` at all. Written as routes are
|
|
12
|
+
* (`/inbound/:vendor`) and matched against the path this server routes, so a different path to
|
|
13
|
+
* the same handler is still checked. Exempt nothing that acts on a session cookie: a cross-site page
|
|
14
|
+
* can post to it.
|
|
15
|
+
*/
|
|
16
|
+
readonly exempt?: readonly string[];
|
|
15
17
|
}
|
|
16
18
|
export declare function csrf(options?: CsrfOptions): Middleware;
|
|
17
19
|
//# sourceMappingURL=csrf.d.ts.map
|
package/dist/csrf.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AASA,OAAO,EAAuB,KAAK,UAAU,EAAc,MAAM,sBAAsB,CAAA;AAQvF,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAcD,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAqC1D"}
|
package/dist/csrf.js
CHANGED
|
@@ -2,22 +2,38 @@
|
|
|
2
2
|
* CSRF protection via an **Origin / Referer check** on state-changing requests - OWASP's recommended
|
|
3
3
|
* defense for cookie-authenticated apps. A browser always attaches `Origin` (or at least `Referer`) to
|
|
4
4
|
* a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
|
|
5
|
-
* methods (GET/HEAD/OPTIONS) pass
|
|
5
|
+
* methods (GET/HEAD/OPTIONS) pass, as do routes listed in `exempt`. Apply with
|
|
6
|
+
* `app.use(csrf({ origins: ["https://example.com"] }))`.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { expandOptionalParams } from "@nifrajs/core/pattern";
|
|
9
|
+
import { Router } from "@nifrajs/core/router";
|
|
10
|
+
import { isSameOriginRequest, pathnameOf } from "@nifrajs/core/server";
|
|
8
11
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
9
12
|
// A name per instance: `use()` skips a name it already applied, which would drop a second, stricter
|
|
10
13
|
// `csrf()` inside a `group()`.
|
|
11
14
|
let instances = 0;
|
|
12
15
|
const forbidden = () => Response.json({ ok: false, error: "csrf_failed" }, { status: 403 });
|
|
16
|
+
function exemptionOf(patterns) {
|
|
17
|
+
if (patterns.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const forms = new Set(patterns.flatMap((pattern) => expandOptionalParams(pattern)));
|
|
20
|
+
const router = new Router();
|
|
21
|
+
// An exemption names a path whatever the method, so every form sits under one method key.
|
|
22
|
+
for (const form of forms)
|
|
23
|
+
router.add("POST", form, true);
|
|
24
|
+
return (path) => router.find("POST", path).found;
|
|
25
|
+
}
|
|
13
26
|
export function csrf(options = {}) {
|
|
14
27
|
const configured = options.origins !== undefined ? new Set(options.origins) : undefined;
|
|
28
|
+
const exempt = exemptionOf(options.exempt ?? []);
|
|
15
29
|
instances += 1;
|
|
16
30
|
return {
|
|
17
31
|
name: `csrf#${instances}`,
|
|
18
32
|
onRequest(req) {
|
|
19
33
|
if (SAFE_METHODS.has(req.method))
|
|
20
34
|
return undefined;
|
|
35
|
+
if (exempt?.(pathnameOf(req.url)) === true)
|
|
36
|
+
return undefined;
|
|
21
37
|
// The same-origin default uses core's check, which accepts an `https:` page reaching an `http:`
|
|
22
38
|
// request URL on the same host - what every TLS-terminating proxy looks like from here.
|
|
23
39
|
const allowed = (candidate) => configured !== undefined ? configured.has(candidate) : isSameOriginRequest(candidate, req);
|
package/dist/csrf.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/auth",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@nifrajs/core": "^5.
|
|
44
|
+
"@nifrajs/core": "^5.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@nifrajs/core": "workspace:*"
|
package/src/csrf.ts
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* CSRF protection via an **Origin / Referer check** on state-changing requests - OWASP's recommended
|
|
3
3
|
* defense for cookie-authenticated apps. A browser always attaches `Origin` (or at least `Referer`) to
|
|
4
4
|
* a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
|
|
5
|
-
* methods (GET/HEAD/OPTIONS) pass
|
|
5
|
+
* methods (GET/HEAD/OPTIONS) pass, as do routes listed in `exempt`. Apply with
|
|
6
|
+
* `app.use(csrf({ origins: ["https://example.com"] }))`.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { expandOptionalParams } from "@nifrajs/core/pattern"
|
|
9
|
+
import { Router } from "@nifrajs/core/router"
|
|
10
|
+
import { isSameOriginRequest, type Middleware, pathnameOf } from "@nifrajs/core/server"
|
|
8
11
|
|
|
9
12
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"])
|
|
10
13
|
|
|
@@ -19,18 +22,37 @@ export interface CsrfOptions {
|
|
|
19
22
|
* works). Set it when the proxy rewrites `Host`, so the public origin differs from the worker's.
|
|
20
23
|
*/
|
|
21
24
|
readonly origins?: readonly string[]
|
|
25
|
+
/**
|
|
26
|
+
* Route patterns whose requests skip the check, for a route no browser calls that proves its sender
|
|
27
|
+
* another way - a signed payment webhook arrives with no `Origin` at all. Written as routes are
|
|
28
|
+
* (`/inbound/:vendor`) and matched against the path this server routes, so a different path to
|
|
29
|
+
* the same handler is still checked. Exempt nothing that acts on a session cookie: a cross-site page
|
|
30
|
+
* can post to it.
|
|
31
|
+
*/
|
|
32
|
+
readonly exempt?: readonly string[]
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
const forbidden = (): Response =>
|
|
25
36
|
Response.json({ ok: false, error: "csrf_failed" }, { status: 403 })
|
|
26
37
|
|
|
38
|
+
function exemptionOf(patterns: readonly string[]): ((path: string) => boolean) | undefined {
|
|
39
|
+
if (patterns.length === 0) return undefined
|
|
40
|
+
const forms = new Set(patterns.flatMap((pattern) => expandOptionalParams(pattern)))
|
|
41
|
+
const router = new Router<true>()
|
|
42
|
+
// An exemption names a path whatever the method, so every form sits under one method key.
|
|
43
|
+
for (const form of forms) router.add("POST", form, true)
|
|
44
|
+
return (path) => router.find("POST", path).found
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
export function csrf(options: CsrfOptions = {}): Middleware {
|
|
28
48
|
const configured = options.origins !== undefined ? new Set(options.origins) : undefined
|
|
49
|
+
const exempt = exemptionOf(options.exempt ?? [])
|
|
29
50
|
instances += 1
|
|
30
51
|
return {
|
|
31
52
|
name: `csrf#${instances}`,
|
|
32
53
|
onRequest(req) {
|
|
33
54
|
if (SAFE_METHODS.has(req.method)) return undefined
|
|
55
|
+
if (exempt?.(pathnameOf(req.url)) === true) return undefined
|
|
34
56
|
// The same-origin default uses core's check, which accepts an `https:` page reaching an `http:`
|
|
35
57
|
// request URL on the same host - what every TLS-terminating proxy looks like from here.
|
|
36
58
|
const allowed = (candidate: string): boolean =>
|