@nifrajs/auth 5.6.0 → 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 CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## 5.6.0
4
13
 
5
14
  ### 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
@@ -1 +1 @@
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,CAmC1D"}
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. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
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 { isSameOriginRequest } from "@nifrajs/core/server";
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;;;;;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,oFAAoF;YACpF,+FAA+F;YAC/F,IAAI,MAAM,KAAK,MAAM,IAAI,UAAU,KAAK,SAAS;gBAC/C,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YACtF,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"}
1
+ {"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAmB,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEvF,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;AAmBjB,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,SAAS,WAAW,CAAC,QAA2B;IAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC3C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IACnF,MAAM,MAAM,GAAG,IAAI,MAAM,EAAQ,CAAA;IACjC,0FAA0F;IAC1F,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IACxD,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAA;AAClD,CAAC;AAED,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,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IAChD,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,IAAI,MAAM,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI;gBAAE,OAAO,SAAS,CAAA;YAC5D,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,oFAAoF;YACpF,+FAA+F;YAC/F,IAAI,MAAM,KAAK,MAAM,IAAI,UAAU,KAAK,SAAS;gBAC/C,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YACtF,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/auth",
3
- "version": "5.6.0",
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.6.0"
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. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
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 { isSameOriginRequest, type Middleware } from "@nifrajs/core/server"
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 =>