Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Web Crypto API for cookies and sessions #11837

Merged
merged 6 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
mjackson marked this conversation as resolved.
Show resolved Hide resolved
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) {
mjackson marked this conversation as resolved.
Show resolved Hide resolved
// @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