Skip to content

Commit

Permalink
Use Web Crypto API for cookies and sessions (#11837)
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish authored Aug 20, 2024
1 parent 23ff35b commit a5f191b
Show file tree
Hide file tree
Showing 24 changed files with 277 additions and 565 deletions.
22 changes: 22 additions & 0 deletions .changeset/fast-plums-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@react-router/architect": major
"@react-router/cloudflare": major
"@react-router/node": major
"react-router": major
---

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:

- `createCookie`
- `createCookieSessionStorage`
- `createMemorySessionStorage`
- `createSessionStorage`

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)

Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:

- `createCookieFactory`
- `createSessionStorageFactory`
- `createCookieSessionStorageFactory`
- `createMemorySessionStorageFactory`
4 changes: 2 additions & 2 deletions integration/set-cookie-revalidation-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test.describe("set-cookie revalidation", () => {
fixture = await createFixture({
files: {
"app/session.server.ts": js`
import { createCookieSessionStorage } from "@react-router/node";
import { createCookieSessionStorage } from "react-router";
export let MESSAGE_KEY = "message";
Expand All @@ -33,8 +33,8 @@ test.describe("set-cookie revalidation", () => {
`,

"app/root.tsx": js`
import { json } from "react-router";
import {
json,
Links,
Meta,
Outlet,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as crypto from "node:crypto";
import type {
SessionData,
SessionStorage,
SessionIdStorageStrategy,
} from "react-router";
import { createSessionStorage } from "@react-router/node";
import { createSessionStorage } from "react-router";
import arc from "@architect/functions";
import type { ArcTable } from "@architect/functions/types/tables";

Expand Down Expand Up @@ -64,7 +63,7 @@ export function createArcTableSessionStorage<
async createData(data, expires) {
let table = await getTable();
while (true) {
let randomBytes = crypto.randomBytes(8);
let randomBytes = crypto.getRandomValues(new Uint8Array(8));
// This storage manages an id space of 2^64 ids, which is far greater
// than the maximum number of files allowed on an NTFS or ext4 volume
// (2^32). However, the larger id space should help to avoid collisions
Expand Down
53 changes: 0 additions & 53 deletions packages/react-router-cloudflare/crypto.ts

This file was deleted.

15 changes: 0 additions & 15 deletions packages/react-router-cloudflare/implementations.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/react-router-cloudflare/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
export { createWorkersKVSessionStorage } from "./sessions/workersKVStorage";

export {
createCookie,
createCookieSessionStorage,
createMemorySessionStorage,
createSessionStorage,
} from "./implementations";

export type {
createPagesFunctionHandlerParams,
GetLoadContextFunction,
Expand Down
6 changes: 2 additions & 4 deletions packages/react-router-cloudflare/sessions/workersKVStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import type {
SessionIdStorageStrategy,
SessionData,
} from "react-router";

import { createSessionStorage } from "../implementations";
import { createSessionStorage } from "react-router";

interface WorkersKVSessionStorageOptions {
/**
Expand Down Expand Up @@ -36,8 +35,7 @@ export function createWorkersKVSessionStorage<
cookie,
async createData(data, expires) {
while (true) {
let randomBytes = new Uint8Array(8);
crypto.getRandomValues(randomBytes);
let randomBytes = crypto.getRandomValues(new Uint8Array(8));
// This storage manages an id space of 2^64 ids, which is far greater
// than the maximum number of files allowed on an NTFS or ext4 volume
// (2^32). However, the larger id space should help to avoid collisions
Expand Down
4 changes: 4 additions & 0 deletions packages/react-router-node/__tests__/sessions-test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @jest-environment node
*/

import path from "node:path";
import { promises as fsp } from "node:fs";
import os from "node:os";
Expand Down
13 changes: 0 additions & 13 deletions packages/react-router-node/crypto.ts

This file was deleted.

8 changes: 8 additions & 0 deletions packages/react-router-node/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Request as NodeRequest,
Response as NodeResponse,
} from "undici";
import { webcrypto as nodeWebCrypto } from "node:crypto";

declare global {
namespace NodeJS {
Expand All @@ -24,6 +25,8 @@ declare global {

ReadableStream: typeof ReadableStream;
WritableStream: typeof WritableStream;

crypto: typeof nodeWebCrypto;
}
}

Expand All @@ -44,4 +47,9 @@ export function installGlobals() {
global.fetch = nodeFetch;
// @ts-expect-error - overriding globals
global.FormData = NodeFormData;

if (!global.crypto) {
// @ts-expect-error - overriding globals
global.crypto = nodeWebCrypto;
}
}
15 changes: 0 additions & 15 deletions packages/react-router-node/implementations.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/react-router-node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ export {
NodeOnDiskFile,
} from "./upload/fileUploadHandler";

export {
createCookie,
createCookieSessionStorage,
createMemorySessionStorage,
createSessionStorage,
} from "./implementations";

export {
createReadableStreamFromReadable,
readableStreamToString,
Expand Down
2 changes: 0 additions & 2 deletions packages/react-router-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,11 @@
},
"dependencies": {
"@web3-storage/multipart-parser": "^1.0.0",
"cookie-signature": "^1.1.0",
"source-map-support": "^0.5.21",
"stream-slice": "^0.1.2",
"undici": "^6.19.2"
},
"devDependencies": {
"@types/cookie-signature": "^1.0.3",
"@types/source-map-support": "^0.5.4",
"react-router": "workspace:*",
"typescript": "^5.1.6"
Expand Down
8 changes: 2 additions & 6 deletions packages/react-router-node/sessions/fileStorage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import * as crypto from "node:crypto";
import { promises as fsp } from "node:fs";
import * as path from "node:path";
import type {
SessionStorage,
SessionIdStorageStrategy,
SessionData,
} from "react-router";

import { createSessionStorage } from "../implementations";
import { createSessionStorage } from "react-router";

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

while (true) {
// TODO: Once Node v19 is supported we should use the globally provided
// Web Crypto API's crypto.getRandomValues() function here instead.
let randomBytes = crypto.webcrypto.getRandomValues(new Uint8Array(8));
let randomBytes = crypto.getRandomValues(new Uint8Array(8));
// This storage manages an id space of 2^64 ids, which is far greater
// than the maximum number of files allowed on an NTFS or ext4 volume
// (2^32). However, the larger id space should help to avoid collisions
Expand Down
27 changes: 5 additions & 22 deletions packages/react-router/__tests__/server-runtime/cookies-test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
import {
createCookieFactory,
isCookie,
} from "../../lib/server-runtime/cookies";
import type {
SignFunction,
UnsignFunction,
} from "../../lib/server-runtime/crypto";

const sign: SignFunction = async (value, secret) => {
return JSON.stringify({ value, secret });
};
const unsign: UnsignFunction = async (signed, secret) => {
try {
let unsigned = JSON.parse(signed);
if (unsigned.secret !== secret) return false;
return unsigned.value;
} catch (e: unknown) {
return false;
}
};
const createCookie = createCookieFactory({ sign, unsign });
/**
* @jest-environment node
*/

import { createCookie, isCookie } from "../../lib/server-runtime/cookies";

function getCookieFromSetCookie(setCookie: string): string {
return setCookie.split(/;\s*/)[0];
Expand Down
Loading

0 comments on commit a5f191b

Please sign in to comment.