@nifrajs/auth 5.5.1 → 5.7.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 +21 -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/dist/guards.d.ts +3 -0
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +3 -0
- package/dist/guards.js.map +1 -1
- package/package.json +2 -2
- package/src/csrf.ts +24 -2
- package/src/guards.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @nifrajs/auth
|
|
2
2
|
|
|
3
|
+
## 5.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b141cf4: `csrf()` takes `exempt`: route patterns whose requests skip the Origin check, for a route no browser
|
|
8
|
+
calls that verifies its sender another way, such as a signed payment webhook arriving with no
|
|
9
|
+
`Origin`. Patterns are written as routes are (`/inbound/:vendor`) and matched against the path the
|
|
10
|
+
server routes, so any other path stays checked.
|
|
11
|
+
|
|
12
|
+
## 5.6.0
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 2eabd83: `etag()` keeps an `ETag` the handler already set and answers `If-None-Match` against it, instead of replacing an app-managed version with a content hash. `etag()`, `conditionalResponse()` and `rangeResponse()` read an `If-None-Match` list with a comma inside a quoted ETag as one tag.
|
|
17
|
+
|
|
18
|
+
`env.number()` reads plain decimal text only (`0x10`, `1e3` and `Infinity` are refused), and `env.number()` and `env.port()` treat a value of only spaces as unset, so a `default` applies. `env.boolean({ default })` returns the default for a variable that is set but empty.
|
|
19
|
+
|
|
20
|
+
`serverFunctions()` mounts a second module under a namespace already in use, so `admin/todos.fn.ts` and `user/todos.fn.ts` are both served; an export name both modules use fails at boot as a duplicate route. Before, the second module was skipped and its functions answered 404. A module no app uses any more is not kept in memory by its namespace. The client build fails on a `*.fn.ts` file whose name `serverFunctions()` would refuse as a namespace, naming the file.
|
|
21
|
+
|
|
22
|
+
`requireSession()` documents that it passes any non-empty session, which a visitor can have before signing in; `requireUser()` is the signed-in guard.
|
|
23
|
+
|
|
3
24
|
## 5.5.1
|
|
4
25
|
|
|
5
26
|
No changes in this release.
|
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/dist/guards.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface GuardOptions {
|
|
|
19
19
|
* (302/401) - control flow, not a `Response`; see the module docs above. Use at
|
|
20
20
|
* the top of a protected loader:
|
|
21
21
|
* `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
|
|
22
|
+
*
|
|
23
|
+
* Non-empty is not signed in: a session that holds anything (a cart, a return path, CSRF state)
|
|
24
|
+
* passes. Guard a page for signed-in visitors with {@link requireUser} and the key a login sets.
|
|
22
25
|
*/
|
|
23
26
|
export declare function requireSession<Data extends Record<string, unknown>>(session: Session<Data>, options?: GuardOptions): Session<Data>;
|
|
24
27
|
/**
|
package/dist/guards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,8FAA8F;AAC9F,MAAM,WAAW,uBAAuB;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,oGAAoG;IACpG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACpD;AAED;iGACiG;AACjG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAA;CACnD;AA4CD
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,8FAA8F;AAC9F,MAAM,WAAW,uBAAuB;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,oGAAoG;IACpG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACpD;AAED;iGACiG;AACjG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAA;CACnD;AA4CD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,EACpF,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,GAAG,EAAE,CAAC,EACN,OAAO,GAAE,YAAiB,GACzB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAMtB"}
|
package/dist/guards.js
CHANGED
|
@@ -52,6 +52,9 @@ const rejection = (options) => {
|
|
|
52
52
|
* (302/401) - control flow, not a `Response`; see the module docs above. Use at
|
|
53
53
|
* the top of a protected loader:
|
|
54
54
|
* `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
|
|
55
|
+
*
|
|
56
|
+
* Non-empty is not signed in: a session that holds anything (a cart, a return path, CSRF state)
|
|
57
|
+
* passes. Guard a page for signed-in visitors with {@link requireUser} and the key a login sets.
|
|
55
58
|
*/
|
|
56
59
|
export function requireSession(session, options = {}) {
|
|
57
60
|
assertUnauthenticated(options);
|
package/dist/guards.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,gBAAgB,EAAuB,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAoBpF,MAAM,YAAY,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAW,CAAA;AAElE,uGAAuG;AACvG,SAAS,qBAAqB,CAAC,OAG9B;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,CAAA;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAM;IAChC,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAA;IAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,UAAU,CAClB,6EAA6E,MAAM,CAAC,IAAI,CAAC,GAAG,CAC7F,CAAA;IACH,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,MAA2C,EAAkB,EAAE;IAC5F,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;IAC1D,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS;QACjC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,CAAC;QACpC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;AACrE,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,OAAqB,EAAkB,EAAE;IAC1D,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,qBAAqB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;IAC3E,iGAAiG;IACjG,iGAAiG;IACjG,oEAAoE;IACpE,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,sIAAsI,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAC5J,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAC9D,CAAC,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,gBAAgB,EAAuB,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAoBpF,MAAM,YAAY,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAW,CAAA;AAElE,uGAAuG;AACvG,SAAS,qBAAqB,CAAC,OAG9B;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,CAAA;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAM;IAChC,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAA;IAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,UAAU,CAClB,6EAA6E,MAAM,CAAC,IAAI,CAAC,GAAG,CAC7F,CAAA;IACH,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,MAA2C,EAAkB,EAAE;IAC5F,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;IAC1D,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS;QACjC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,CAAC;QACpC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;AACrE,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,OAAqB,EAAkB,EAAE;IAC1D,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,qBAAqB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;IAC3E,iGAAiG;IACjG,iGAAiG;IACjG,oEAAoE;IACpE,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,sIAAsI,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAC5J,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAC9D,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAsB,EACtB,UAAwB,EAAE;IAE1B,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,OAAO,CAAA;IACpC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,OAAsB,EACtB,GAAM,EACN,UAAwB,EAAE;IAE1B,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,uGAAuG;IACvG,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAA6B,CAAA;IAC/E,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/auth",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.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.7.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 =>
|
package/src/guards.ts
CHANGED
|
@@ -79,6 +79,9 @@ const rejection = (options: GuardOptions): ResponseResult => {
|
|
|
79
79
|
* (302/401) - control flow, not a `Response`; see the module docs above. Use at
|
|
80
80
|
* the top of a protected loader:
|
|
81
81
|
* `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
|
|
82
|
+
*
|
|
83
|
+
* Non-empty is not signed in: a session that holds anything (a cart, a return path, CSRF state)
|
|
84
|
+
* passes. Guard a page for signed-in visitors with {@link requireUser} and the key a login sets.
|
|
82
85
|
*/
|
|
83
86
|
export function requireSession<Data extends Record<string, unknown>>(
|
|
84
87
|
session: Session<Data>,
|