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-compatible crypto for PKCE #783

Merged
merged 7 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 15 additions & 14 deletions packages/auth-core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import jwtDecode from "jwt-decode";
import * as edgedb from "edgedb";
import { ResolvedConnectConfig } from "edgedb/dist/conUtils";
import { type ResolvedConnectConfig } from "edgedb/dist/conUtils";

import * as pkce from "./pkce";
import { BuiltinOAuthProviderNames, emailPasswordProviderName } from "./consts";
import {
type BuiltinOAuthProviderNames,
emailPasswordProviderName,
} from "./consts";

export interface TokenData {
auth_token: string;
Expand All @@ -25,7 +28,7 @@

static async create(client: edgedb.Client) {
const connectConfig: ResolvedConnectConfig = (
await (client as any).pool._getNormalizedConnectConfig()

Check warning on line 31 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 31 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 31 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Unexpected any. Specify a different type

Check warning on line 31 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Unexpected any. Specify a different type

Check warning on line 31 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Unexpected any. Specify a different type

Check warning on line 31 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Unexpected any. Specify a different type
).connectionParams;

const [host, port] = connectConfig.address;
Expand All @@ -37,7 +40,7 @@
}

/** @internal */
public async _get<T extends any = unknown>(path: string): Promise<T> {

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Unexpected any. Specify a different type

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Unexpected any. Specify a different type

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Unexpected any. Specify a different type

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 43 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Unexpected any. Specify a different type
const res = await fetch(new URL(path, this.baseUrl), {
method: "get",
});
Expand All @@ -47,13 +50,13 @@
if (res.headers.get("content-type")?.startsWith("application/json")) {
return res.json();
}
return null as any;

Check warning on line 53 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 53 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 53 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Unexpected any. Specify a different type

Check warning on line 53 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Unexpected any. Specify a different type

Check warning on line 53 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Unexpected any. Specify a different type

Check warning on line 53 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Unexpected any. Specify a different type
}

/** @internal */
public async _post<T extends any = unknown>(

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Unexpected any. Specify a different type

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Unexpected any. Specify a different type

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Unexpected any. Specify a different type

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Constraining the generic type `T` to `any` does nothing and is unnecessary

Check warning on line 57 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Unexpected any. Specify a different type
path: string,
body?: any

Check warning on line 59 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 59 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 59 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Unexpected any. Specify a different type

Check warning on line 59 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Unexpected any. Specify a different type

Check warning on line 59 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Unexpected any. Specify a different type

Check warning on line 59 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Unexpected any. Specify a different type
): Promise<T> {
const res = await fetch(new URL(path, this.baseUrl), {
method: "post",
Expand All @@ -70,11 +73,12 @@
if (res.headers.get("content-type")?.startsWith("application/json")) {
return res.json();
}
return null as any;

Check warning on line 76 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (18, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 76 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (20, ubuntu-latest, stable)

Unexpected any. Specify a different type

Check warning on line 76 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, nightly)

Unexpected any. Specify a different type

Check warning on line 76 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 3)

Unexpected any. Specify a different type

Check warning on line 76 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18, 2)

Unexpected any. Specify a different type

Check warning on line 76 in packages/auth-core/src/core.ts

View workflow job for this annotation

GitHub Actions / test (macos-latest, 18, stable)

Unexpected any. Specify a different type
}

createPKCESession() {
return new AuthPCKESession(this);
async createPKCESession() {
const { challenge, verifier } = await pkce.createVerifierChallengePair();
return new AuthPCKESession(this, challenge, verifier);
}

getToken(code: string, verifier: string): Promise<TokenData> {
Expand All @@ -87,7 +91,7 @@
}

async signinWithEmailPassword(email: string, password: string) {
const { challenge, verifier } = pkce.createVerifierChallengePair();
const { challenge, verifier } = await pkce.createVerifierChallengePair();
const { code } = await this._post<{ code: string }>("authenticate", {
provider: emailPasswordProviderName,
challenge,
Expand All @@ -105,7 +109,7 @@
| { status: "complete"; tokenData: TokenData }
| { status: "verificationRequired"; verifier: string }
> {
const { challenge, verifier } = pkce.createVerifierChallengePair();
const { challenge, verifier } = await pkce.createVerifierChallengePair();
const result = await this._post<
{ code: string } | { verification_email_sent_at: string }
>("register", {
Expand Down Expand Up @@ -200,14 +204,11 @@
}

export class AuthPCKESession {
public readonly challenge: string;
public readonly verifier: string;

constructor(private auth: Auth) {
const { challenge, verifier } = pkce.createVerifierChallengePair();
this.challenge = challenge;
this.verifier = verifier;
}
constructor(
private auth: Auth,
public readonly challenge: string,
public readonly verifier: string
) {}

getOAuthUrl(
providerName: BuiltinOAuthProviderNames,
Expand Down
48 changes: 48 additions & 0 deletions packages/auth-core/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint @typescript-eslint/no-var-requires: ["off"] */

// TODO: Drop when Node 18 is EOL: 2025-04-30
if (!globalThis.crypto) {
// tslint:disable-next-line: no-var-requires
globalThis.crypto = require("node:crypto").webcrypto;
}

const BASE64_URL_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

export function bytesToBase64Url(bytes: Uint8Array): string {
const len = bytes.length;
let base64url = "";

for (let i = 0; i < len; i += 3) {
const b1 = bytes[i] & 0xff;
const b2 = i + 1 < len ? bytes[i + 1] & 0xff : 0;
const b3 = i + 2 < len ? bytes[i + 2] & 0xff : 0;

const enc1 = b1 >> 2;
const enc2 = ((b1 & 0x03) << 4) | (b2 >> 4);
const enc3 = ((b2 & 0x0f) << 2) | (b3 >> 6);
const enc4 = b3 & 0x3f;

base64url += BASE64_URL_CHARS.charAt(enc1) + BASE64_URL_CHARS.charAt(enc2);
if (i + 1 < len) {
base64url += BASE64_URL_CHARS.charAt(enc3);
}
if (i + 2 < len) {
base64url += BASE64_URL_CHARS.charAt(enc4);
}
}

return base64url;
}

export async function sha256(
source: BufferSource | string
): Promise<Uint8Array> {
const bytes =
typeof source === "string" ? new TextEncoder().encode(source) : source;
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
}

export function randomBytes(length: number): Uint8Array {
return crypto.getRandomValues(new Uint8Array(length));
}
13 changes: 5 additions & 8 deletions packages/auth-core/src/pkce.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import crypto from "node:crypto";
import { bytesToBase64Url, sha256, randomBytes } from "./crypto";

export function createVerifierChallengePair(): {
export async function createVerifierChallengePair(): Promise<{
verifier: string;
challenge: string;
} {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
.createHash("sha256")
.update(verifier)
.digest("base64url");
}> {
const verifier = bytesToBase64Url(randomBytes(32));
const challenge = await sha256(verifier).then(bytesToBase64Url);

return { verifier, challenge };
}
33 changes: 33 additions & 0 deletions packages/auth-core/test/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import crypto from "node:crypto";
import { bytesToBase64Url, sha256, randomBytes } from "../src/crypto";

describe("crypto", () => {
describe("bytesToBase64Url", () => {
test("Equivalent to Buffer implementation", () => {
for (let i = 0; i < 100; i++) {
const buffer = crypto.randomBytes(32);
expect(buffer.toString("base64url")).toEqual(bytesToBase64Url(buffer));
}
});
});

describe("sha256", () => {
test("Equivalent to Node crypto SHA256 implementation", async () => {
for (let i = 0; i < 100; i++) {
const buffer = crypto.randomBytes(32);
expect(crypto.createHash("sha256").update(buffer).digest()).toEqual(
Buffer.from(await sha256(buffer))
);
}
});
});

describe("randomBytes", () => {
test("Generates Uint8Array of correct length", () => {
const length = 32;
const bytes = randomBytes(length);
expect(bytes).toBeInstanceOf(Uint8Array);
expect(bytes.length).toEqual(length);
});
});
});
8 changes: 6 additions & 2 deletions packages/auth-nextjs/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export class NextAppAuth extends NextAuth {
throw new Error(`invalid provider_name: ${provider}`);
}
const redirectUrl = `${this._authRoute}/oauth/callback`;
const pkceSession = (await this.core).createPKCESession();
const pkceSession = await this.core.then((core) =>
core.createPKCESession()
);
cookies().set({
name: this.options.pkceVerifierCookieName,
value: pkceSession.verifier,
Expand Down Expand Up @@ -238,7 +240,9 @@ export class NextAppAuth extends NextAuth {
}
case "builtin/signin":
case "builtin/signup": {
const pkceSession = (await this.core).createPKCESession();
const pkceSession = await this.core.then((core) =>
core.createPKCESession()
);
cookies().set({
name: this.options.pkceVerifierCookieName,
value: pkceSession.verifier,
Expand Down
Loading