@remix-run/csrf-middleware 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -21,10 +21,10 @@ This middleware requires [`session-middleware`](https://github.com/remix-run/rem
21
21
 
22
22
  ```ts
23
23
  import { createCookie } from 'remix/cookie'
24
- import { createRouter } from 'remix/fetch-router'
25
- import { createCookieSessionStorage } from 'remix/session/cookie-storage'
26
- import { session } from 'remix/session-middleware'
27
- import { csrf, getCsrfToken } from 'remix/csrf-middleware'
24
+ import { createRouter } from 'remix/router'
25
+ import { createCookieSessionStorage } from 'remix/session-storage/cookie'
26
+ import { session } from 'remix/middleware/session'
27
+ import { csrf, getCsrfToken } from 'remix/middleware/csrf'
28
28
 
29
29
  let sessionCookie = createCookie('__session', { secrets: ['secret1'] })
30
30
  let sessionStorage = createCookieSessionStorage()
@@ -49,7 +49,7 @@ router.get('/form', (context) => {
49
49
 
50
50
  By default, `csrf()` checks token values in this order:
51
51
 
52
- 1. Request headers: `x-csrf-token`, `x-xsrf-token`, `csrf-token`
52
+ 1. Request headers: `X-Csrf-Token`, `X-Xsrf-Token`, `Csrf-Token`
53
53
  2. Form field: `_csrf` (requires `formData()` middleware to parse request bodies)
54
54
  3. Query param: `_csrf`
55
55
 
@@ -1,4 +1,5 @@
1
- import type { Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router';
1
+ import type { AnyParams, ContextEntries, Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router';
2
+ type AnyRequestContext = RequestContext<AnyParams, ContextEntries>;
2
3
  type OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>;
3
4
  /**
4
5
  * Return shape for a dynamic CSRF origin resolver.
@@ -11,7 +12,7 @@ export interface CsrfOriginResolver {
11
12
  /**
12
13
  * Resolves whether an unsafe request origin should be trusted.
13
14
  */
14
- (origin: string, context: RequestContext): CsrfOriginResolverResult | Promise<CsrfOriginResolverResult>;
15
+ (origin: string, context: AnyRequestContext): CsrfOriginResolverResult | Promise<CsrfOriginResolverResult>;
15
16
  }
16
17
  /**
17
18
  * Accepted forms for configuring allowed CSRF origins.
@@ -28,7 +29,7 @@ export interface CsrfTokenResolver {
28
29
  /**
29
30
  * Resolves the submitted CSRF token for the current request.
30
31
  */
31
- (context: RequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>;
32
+ (context: AnyRequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>;
32
33
  }
33
34
  /**
34
35
  * The reason a CSRF request was rejected.
@@ -53,7 +54,7 @@ export interface CsrfOptions {
53
54
  /**
54
55
  * Header names checked (in order) for CSRF tokens.
55
56
  *
56
- * @default ['x-csrf-token', 'x-xsrf-token', 'csrf-token']
57
+ * @default ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token']
57
58
  */
58
59
  headerNames?: readonly string[];
59
60
  /**
@@ -81,7 +82,7 @@ export interface CsrfOptions {
81
82
  /**
82
83
  * Optional custom error response for rejected requests.
83
84
  */
84
- onError?: (reason: CsrfFailureReason, context: RequestContext) => Response | Promise<Response>;
85
+ onError?: (reason: CsrfFailureReason, context: AnyRequestContext) => Response | Promise<Response>;
85
86
  }
86
87
  /**
87
88
  * Session-backed CSRF protection middleware.
@@ -99,6 +100,6 @@ export declare function csrf(options?: CsrfOptions): Middleware;
99
100
  * @param tokenKey Session key that stores the token
100
101
  * @returns The active CSRF token
101
102
  */
102
- export declare function getCsrfToken(context: RequestContext, tokenKey?: string): string;
103
+ export declare function getCsrfToken(context: AnyRequestContext, tokenKey?: string): string;
103
104
  export {};
104
105
  //# sourceMappingURL=csrf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/lib/csrf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAMxF,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;AAErE;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,CACE,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,cAAc,GACtB,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;CAChE;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAE3D;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;AAE/D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,CAAC,OAAO,EAAE,cAAc,GAAG,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CACtF;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,eAAe,CAAA;AAEpF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;IAEtC;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;IAEnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,cAAc,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC/F;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAwC1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,SAAU,GAAG,MAAM,CAehF"}
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/lib/csrf.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,UAAU,EACV,cAAc,EACd,aAAa,EACd,MAAM,yBAAyB,CAAA;AAMhC,KAAK,iBAAiB,GAAG,cAAc,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;AAClE,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;AAErE;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,CACE,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,iBAAiB,GACzB,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;CAChE;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAE3D;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;AAE/D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,CAAC,OAAO,EAAE,iBAAiB,GAAG,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CACzF;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,eAAe,CAAA;AAEpF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;IAEtC;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;IAEnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,iBAAiB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAClG;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAwC1D;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,SAAU,GAAG,MAAM,CAenF"}
package/dist/lib/csrf.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { isRequestMethod } from '@remix-run/fetch-router';
1
2
  import { Session } from '@remix-run/session';
2
3
  const defaultSafeMethods = ['GET', 'HEAD', 'OPTIONS'];
3
- const defaultTokenHeaderNames = ['x-csrf-token', 'x-xsrf-token', 'csrf-token'];
4
+ const defaultTokenHeaderNames = ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token'];
4
5
  /**
5
6
  * Session-backed CSRF protection middleware.
6
7
  *
@@ -16,11 +17,11 @@ export function csrf(options = {}) {
16
17
  let headerNames = options.headerNames ?? defaultTokenHeaderNames;
17
18
  let allowMissingOrigin = options.allowMissingOrigin ?? true;
18
19
  return async (context, next) => {
19
- if (!context.has(Session)) {
20
+ if (context.get(Session) == null) {
20
21
  throw new Error('csrf middleware requires session() middleware to run before it');
21
22
  }
22
23
  let expectedToken = getCsrfToken(context, tokenKey);
23
- if (safeMethods.includes(context.method)) {
24
+ if (isSafeMethod(context.method, safeMethods)) {
24
25
  return next();
25
26
  }
26
27
  let validOrigin = await validateRequestOrigin(context, options.origin, allowMissingOrigin, context.url.origin);
@@ -37,6 +38,9 @@ export function csrf(options = {}) {
37
38
  return next();
38
39
  };
39
40
  }
41
+ function isSafeMethod(method, safeMethods) {
42
+ return isRequestMethod(method) && safeMethods.includes(method);
43
+ }
40
44
  /**
41
45
  * Gets the CSRF token from the session. Creates one if missing.
42
46
  *
@@ -45,10 +49,10 @@ export function csrf(options = {}) {
45
49
  * @returns The active CSRF token
46
50
  */
47
51
  export function getCsrfToken(context, tokenKey = '_csrf') {
48
- if (!context.has(Session)) {
52
+ let session = context.get(Session);
53
+ if (session == null) {
49
54
  throw new Error('Session is not started. Use session() middleware before csrf().');
50
55
  }
51
- let session = context.get(Session);
52
56
  let token = session.get(tokenKey);
53
57
  if (typeof token === 'string' && token !== '') {
54
58
  return token;
@@ -97,7 +101,7 @@ async function resolveSubmittedToken(context, valueResolver, fieldName, headerNa
97
101
  return trimmedHeaderValue;
98
102
  }
99
103
  }
100
- let formValue = context.has(FormData) ? context.get(FormData).get(fieldName) : undefined;
104
+ let formValue = context.get(FormData)?.get(fieldName);
101
105
  if (typeof formValue === 'string') {
102
106
  let trimmedFormValue = formValue.trim();
103
107
  if (trimmedFormValue !== '') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/csrf-middleware",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Middleware for CSRF protection in Fetch API servers",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -28,17 +28,17 @@
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.6.0",
30
30
  "@typescript/native-preview": "7.0.0-dev.20251125.1",
31
- "@remix-run/cookie": "0.5.1",
32
- "@remix-run/assert": "0.1.0",
33
- "@remix-run/form-data-middleware": "0.2.1",
34
- "@remix-run/fetch-router": "0.18.1",
35
- "@remix-run/session-middleware": "0.2.1",
31
+ "@remix-run/assert": "0.2.0",
32
+ "@remix-run/cookie": "0.5.2",
33
+ "@remix-run/form-data-middleware": "0.3.0",
34
+ "@remix-run/fetch-router": "0.19.0",
36
35
  "@remix-run/session": "0.4.1",
37
- "@remix-run/test": "0.1.0"
36
+ "@remix-run/session-middleware": "0.3.0",
37
+ "@remix-run/test": "0.4.0"
38
38
  },
39
39
  "dependencies": {
40
- "@remix-run/fetch-router": "^0.18.1",
41
- "@remix-run/session": "^0.4.1"
40
+ "@remix-run/session": "^0.4.1",
41
+ "@remix-run/fetch-router": "^0.19.0"
42
42
  },
43
43
  "keywords": [
44
44
  "fetch",
@@ -52,6 +52,7 @@
52
52
  "build": "tsgo -p tsconfig.build.json",
53
53
  "clean": "git clean -fdX",
54
54
  "test": "remix-test",
55
+ "test:bun": "bun x --bun remix-test",
55
56
  "typecheck": "tsgo --noEmit"
56
57
  }
57
58
  }
package/src/lib/csrf.ts CHANGED
@@ -1,9 +1,17 @@
1
- import type { Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router'
1
+ import { isRequestMethod } from '@remix-run/fetch-router'
2
+ import type {
3
+ AnyParams,
4
+ ContextEntries,
5
+ Middleware,
6
+ RequestContext,
7
+ RequestMethod,
8
+ } from '@remix-run/fetch-router'
2
9
  import { Session } from '@remix-run/session'
3
10
 
4
11
  const defaultSafeMethods: RequestMethod[] = ['GET', 'HEAD', 'OPTIONS']
5
- const defaultTokenHeaderNames = ['x-csrf-token', 'x-xsrf-token', 'csrf-token']
12
+ const defaultTokenHeaderNames = ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token']
6
13
 
14
+ type AnyRequestContext = RequestContext<AnyParams, ContextEntries>
7
15
  type OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>
8
16
 
9
17
  /**
@@ -20,7 +28,7 @@ export interface CsrfOriginResolver {
20
28
  */
21
29
  (
22
30
  origin: string,
23
- context: RequestContext,
31
+ context: AnyRequestContext,
24
32
  ): CsrfOriginResolverResult | Promise<CsrfOriginResolverResult>
25
33
  }
26
34
 
@@ -41,7 +49,7 @@ export interface CsrfTokenResolver {
41
49
  /**
42
50
  * Resolves the submitted CSRF token for the current request.
43
51
  */
44
- (context: RequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>
52
+ (context: AnyRequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>
45
53
  }
46
54
 
47
55
  /**
@@ -70,7 +78,7 @@ export interface CsrfOptions {
70
78
  /**
71
79
  * Header names checked (in order) for CSRF tokens.
72
80
  *
73
- * @default ['x-csrf-token', 'x-xsrf-token', 'csrf-token']
81
+ * @default ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token']
74
82
  */
75
83
  headerNames?: readonly string[]
76
84
 
@@ -103,7 +111,7 @@ export interface CsrfOptions {
103
111
  /**
104
112
  * Optional custom error response for rejected requests.
105
113
  */
106
- onError?: (reason: CsrfFailureReason, context: RequestContext) => Response | Promise<Response>
114
+ onError?: (reason: CsrfFailureReason, context: AnyRequestContext) => Response | Promise<Response>
107
115
  }
108
116
 
109
117
  /**
@@ -122,13 +130,13 @@ export function csrf(options: CsrfOptions = {}): Middleware {
122
130
  let allowMissingOrigin = options.allowMissingOrigin ?? true
123
131
 
124
132
  return async (context, next) => {
125
- if (!context.has(Session)) {
133
+ if (context.get(Session) == null) {
126
134
  throw new Error('csrf middleware requires session() middleware to run before it')
127
135
  }
128
136
 
129
137
  let expectedToken = getCsrfToken(context, tokenKey)
130
138
 
131
- if (safeMethods.includes(context.method)) {
139
+ if (isSafeMethod(context.method, safeMethods)) {
132
140
  return next()
133
141
  }
134
142
 
@@ -156,6 +164,10 @@ export function csrf(options: CsrfOptions = {}): Middleware {
156
164
  }
157
165
  }
158
166
 
167
+ function isSafeMethod(method: string, safeMethods: readonly RequestMethod[]): boolean {
168
+ return isRequestMethod(method) && safeMethods.includes(method)
169
+ }
170
+
159
171
  /**
160
172
  * Gets the CSRF token from the session. Creates one if missing.
161
173
  *
@@ -163,12 +175,12 @@ export function csrf(options: CsrfOptions = {}): Middleware {
163
175
  * @param tokenKey Session key that stores the token
164
176
  * @returns The active CSRF token
165
177
  */
166
- export function getCsrfToken(context: RequestContext, tokenKey = '_csrf'): string {
167
- if (!context.has(Session)) {
178
+ export function getCsrfToken(context: AnyRequestContext, tokenKey = '_csrf'): string {
179
+ let session = context.get(Session)
180
+ if (session == null) {
168
181
  throw new Error('Session is not started. Use session() middleware before csrf().')
169
182
  }
170
183
 
171
- let session = context.get(Session)
172
184
  let token = session.get(tokenKey)
173
185
  if (typeof token === 'string' && token !== '') {
174
186
  return token
@@ -195,7 +207,7 @@ function createCsrfToken(): string {
195
207
  function getErrorResponse(
196
208
  options: CsrfOptions,
197
209
  reason: CsrfFailureReason,
198
- context: RequestContext,
210
+ context: AnyRequestContext,
199
211
  ): Response | Promise<Response> {
200
212
  if (options.onError) {
201
213
  return options.onError(reason, context)
@@ -213,7 +225,7 @@ function getErrorResponse(
213
225
  }
214
226
 
215
227
  async function resolveSubmittedToken(
216
- context: RequestContext,
228
+ context: AnyRequestContext,
217
229
  valueResolver: CsrfTokenResolver | undefined,
218
230
  fieldName: string,
219
231
  headerNames: readonly string[],
@@ -240,7 +252,7 @@ async function resolveSubmittedToken(
240
252
  }
241
253
  }
242
254
 
243
- let formValue = context.has(FormData) ? context.get(FormData).get(fieldName) : undefined
255
+ let formValue = context.get(FormData)?.get(fieldName)
244
256
  if (typeof formValue === 'string') {
245
257
  let trimmedFormValue = formValue.trim()
246
258
  if (trimmedFormValue !== '') {
@@ -258,7 +270,7 @@ async function resolveSubmittedToken(
258
270
  }
259
271
 
260
272
  async function validateRequestOrigin(
261
- context: RequestContext,
273
+ context: AnyRequestContext,
262
274
  configuredOrigin: CsrfOrigin | undefined,
263
275
  allowMissingOrigin: boolean,
264
276
  defaultOrigin: string,
@@ -298,7 +310,7 @@ async function validateRequestOrigin(
298
310
  return false
299
311
  }
300
312
 
301
- function getRequestOrigin(context: RequestContext): string | null {
313
+ function getRequestOrigin(context: AnyRequestContext): string | null {
302
314
  let origin = context.headers.get('Origin')
303
315
  if (origin != null && origin.trim() !== '') {
304
316
  return origin