Skip to content

Commit a5f191b

Browse files
Use Web Crypto API for cookies and sessions (#11837)
1 parent 23ff35b commit a5f191b

File tree

24 files changed

+277
-565
lines changed

24 files changed

+277
-565
lines changed

.changeset/fast-plums-peel.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@react-router/architect": major
3+
"@react-router/cloudflare": major
4+
"@react-router/node": major
5+
"react-router": major
6+
---
7+
8+
For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages:
9+
10+
- `createCookie`
11+
- `createCookieSessionStorage`
12+
- `createMemorySessionStorage`
13+
- `createSessionStorage`
14+
15+
For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html)
16+
17+
Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:
18+
19+
- `createCookieFactory`
20+
- `createSessionStorageFactory`
21+
- `createCookieSessionStorageFactory`
22+
- `createMemorySessionStorageFactory`

integration/set-cookie-revalidation-test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test.describe("set-cookie revalidation", () => {
1818
fixture = await createFixture({
1919
files: {
2020
"app/session.server.ts": js`
21-
import { createCookieSessionStorage } from "@react-router/node";
21+
import { createCookieSessionStorage } from "react-router";
2222
2323
export let MESSAGE_KEY = "message";
2424
@@ -33,8 +33,8 @@ test.describe("set-cookie revalidation", () => {
3333
`,
3434

3535
"app/root.tsx": js`
36-
import { json } from "react-router";
3736
import {
37+
json,
3838
Links,
3939
Meta,
4040
Outlet,

packages/react-router-architect/sessions/arcTableSessionStorage.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import * as crypto from "node:crypto";
21
import type {
32
SessionData,
43
SessionStorage,
54
SessionIdStorageStrategy,
65
} from "react-router";
7-
import { createSessionStorage } from "@react-router/node";
6+
import { createSessionStorage } from "react-router";
87
import arc from "@architect/functions";
98
import type { ArcTable } from "@architect/functions/types/tables";
109

@@ -64,7 +63,7 @@ export function createArcTableSessionStorage<
6463
async createData(data, expires) {
6564
let table = await getTable();
6665
while (true) {
67-
let randomBytes = crypto.randomBytes(8);
66+
let randomBytes = crypto.getRandomValues(new Uint8Array(8));
6867
// This storage manages an id space of 2^64 ids, which is far greater
6968
// than the maximum number of files allowed on an NTFS or ext4 volume
7069
// (2^32). However, the larger id space should help to avoid collisions

packages/react-router-cloudflare/crypto.ts

-53
This file was deleted.

packages/react-router-cloudflare/implementations.ts

-15
This file was deleted.

packages/react-router-cloudflare/index.ts

-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
export { createWorkersKVSessionStorage } from "./sessions/workersKVStorage";
22

3-
export {
4-
createCookie,
5-
createCookieSessionStorage,
6-
createMemorySessionStorage,
7-
createSessionStorage,
8-
} from "./implementations";
9-
103
export type {
114
createPagesFunctionHandlerParams,
125
GetLoadContextFunction,

packages/react-router-cloudflare/sessions/workersKVStorage.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import type {
33
SessionIdStorageStrategy,
44
SessionData,
55
} from "react-router";
6-
7-
import { createSessionStorage } from "../implementations";
6+
import { createSessionStorage } from "react-router";
87

98
interface WorkersKVSessionStorageOptions {
109
/**
@@ -36,8 +35,7 @@ export function createWorkersKVSessionStorage<
3635
cookie,
3736
async createData(data, expires) {
3837
while (true) {
39-
let randomBytes = new Uint8Array(8);
40-
crypto.getRandomValues(randomBytes);
38+
let randomBytes = crypto.getRandomValues(new Uint8Array(8));
4139
// This storage manages an id space of 2^64 ids, which is far greater
4240
// than the maximum number of files allowed on an NTFS or ext4 volume
4341
// (2^32). However, the larger id space should help to avoid collisions

packages/react-router-node/__tests__/sessions-test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
15
import path from "node:path";
26
import { promises as fsp } from "node:fs";
37
import os from "node:os";

packages/react-router-node/crypto.ts

-13
This file was deleted.

packages/react-router-node/globals.ts

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Request as NodeRequest,
77
Response as NodeResponse,
88
} from "undici";
9+
import { webcrypto as nodeWebCrypto } from "node:crypto";
910

1011
declare global {
1112
namespace NodeJS {
@@ -24,6 +25,8 @@ declare global {
2425

2526
ReadableStream: typeof ReadableStream;
2627
WritableStream: typeof WritableStream;
28+
29+
crypto: typeof nodeWebCrypto;
2730
}
2831
}
2932

@@ -44,4 +47,9 @@ export function installGlobals() {
4447
global.fetch = nodeFetch;
4548
// @ts-expect-error - overriding globals
4649
global.FormData = NodeFormData;
50+
51+
if (!global.crypto) {
52+
// @ts-expect-error - overriding globals
53+
global.crypto = nodeWebCrypto;
54+
}
4755
}

packages/react-router-node/implementations.ts

-15
This file was deleted.

packages/react-router-node/index.ts

-7
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ export {
77
NodeOnDiskFile,
88
} from "./upload/fileUploadHandler";
99

10-
export {
11-
createCookie,
12-
createCookieSessionStorage,
13-
createMemorySessionStorage,
14-
createSessionStorage,
15-
} from "./implementations";
16-
1710
export {
1811
createReadableStreamFromReadable,
1912
readableStreamToString,

packages/react-router-node/package.json

-2
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,11 @@
3535
},
3636
"dependencies": {
3737
"@web3-storage/multipart-parser": "^1.0.0",
38-
"cookie-signature": "^1.1.0",
3938
"source-map-support": "^0.5.21",
4039
"stream-slice": "^0.1.2",
4140
"undici": "^6.19.2"
4241
},
4342
"devDependencies": {
44-
"@types/cookie-signature": "^1.0.3",
4543
"@types/source-map-support": "^0.5.4",
4644
"react-router": "workspace:*",
4745
"typescript": "^5.1.6"

packages/react-router-node/sessions/fileStorage.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import * as crypto from "node:crypto";
21
import { promises as fsp } from "node:fs";
32
import * as path from "node:path";
43
import type {
54
SessionStorage,
65
SessionIdStorageStrategy,
76
SessionData,
87
} from "react-router";
9-
10-
import { createSessionStorage } from "../implementations";
8+
import { createSessionStorage } from "react-router";
119

1210
interface FileSessionStorageOptions {
1311
/**
@@ -40,9 +38,7 @@ export function createFileSessionStorage<Data = SessionData, FlashData = Data>({
4038
let content = JSON.stringify({ data, expires });
4139

4240
while (true) {
43-
// TODO: Once Node v19 is supported we should use the globally provided
44-
// Web Crypto API's crypto.getRandomValues() function here instead.
45-
let randomBytes = crypto.webcrypto.getRandomValues(new Uint8Array(8));
41+
let randomBytes = crypto.getRandomValues(new Uint8Array(8));
4642
// This storage manages an id space of 2^64 ids, which is far greater
4743
// than the maximum number of files allowed on an NTFS or ext4 volume
4844
// (2^32). However, the larger id space should help to avoid collisions

packages/react-router/__tests__/server-runtime/cookies-test.ts

+5-22
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
1-
import {
2-
createCookieFactory,
3-
isCookie,
4-
} from "../../lib/server-runtime/cookies";
5-
import type {
6-
SignFunction,
7-
UnsignFunction,
8-
} from "../../lib/server-runtime/crypto";
9-
10-
const sign: SignFunction = async (value, secret) => {
11-
return JSON.stringify({ value, secret });
12-
};
13-
const unsign: UnsignFunction = async (signed, secret) => {
14-
try {
15-
let unsigned = JSON.parse(signed);
16-
if (unsigned.secret !== secret) return false;
17-
return unsigned.value;
18-
} catch (e: unknown) {
19-
return false;
20-
}
21-
};
22-
const createCookie = createCookieFactory({ sign, unsign });
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { createCookie, isCookie } from "../../lib/server-runtime/cookies";
236

247
function getCookieFromSetCookie(setCookie: string): string {
258
return setCookie.split(/;\s*/)[0];

0 commit comments

Comments
 (0)