diff --git a/README.md b/README.md index 377ea92..b774ba9 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ const client = new Client(authClient); ### Generating an Authentication URL ```typescript -const authUrl = authClient.generateAuthURL({ +const authUrl = await authClient.generateAuthURL({ code_challenge_method: "s256", }); ``` diff --git a/examples/oauth2-callback.ts b/examples/oauth2-callback.ts index 10c95b5..6066c04 100644 --- a/examples/oauth2-callback.ts +++ b/examples/oauth2-callback.ts @@ -32,7 +32,7 @@ app.get("/callback", async function (req, res) { }); app.get("/login", async function (req, res) { - const authUrl = authClient.generateAuthURL({ + const authUrl = await authClient.generateAuthURL({ state: STATE, code_challenge_method: "s256", }); diff --git a/examples/oauth2-callback_pkce_plain.ts b/examples/oauth2-callback_pkce_plain.ts index 647e7cf..26e0dfb 100644 --- a/examples/oauth2-callback_pkce_plain.ts +++ b/examples/oauth2-callback_pkce_plain.ts @@ -32,7 +32,7 @@ app.get("/callback", async function (req, res) { }); app.get("/login", async function (req, res) { - const authUrl = authClient.generateAuthURL({ + const authUrl = await authClient.generateAuthURL({ state: STATE, code_challenge_method: "plain", code_challenge: "test", diff --git a/examples/oauth2-callback_pkce_s256.ts b/examples/oauth2-callback_pkce_s256.ts index 4f86cad..6eaa864 100644 --- a/examples/oauth2-callback_pkce_s256.ts +++ b/examples/oauth2-callback_pkce_s256.ts @@ -32,7 +32,7 @@ app.get("/callback", async function (req, res) { }); app.get("/login", async function (req, res) { - const authUrl = authClient.generateAuthURL({ + const authUrl = await authClient.generateAuthURL({ state: STATE, code_challenge_method: "s256", }); diff --git a/examples/oauth2-public-callback_pkce_s256.ts b/examples/oauth2-public-callback_pkce_s256.ts index 39c08d1..ee0202e 100644 --- a/examples/oauth2-public-callback_pkce_s256.ts +++ b/examples/oauth2-public-callback_pkce_s256.ts @@ -31,7 +31,7 @@ app.get("/callback", async function (req, res) { }); app.get("/login", async function (req, res) { - const authUrl = authClient.generateAuthURL({ + const authUrl = await authClient.generateAuthURL({ state: STATE, code_challenge_method: "s256", }); diff --git a/src/OAuth2User.ts b/src/OAuth2User.ts index 64d2eb6..1e6c429 100644 --- a/src/OAuth2User.ts +++ b/src/OAuth2User.ts @@ -1,8 +1,7 @@ // Copyright 2021 Twitter, Inc. // SPDX-License-Identifier: Apache-2.0 -import crypto from "crypto"; -import { buildQueryString, basicAuthHeader } from "./utils"; +import { buildQueryString, basicAuthHeader, base64Encode } from "./utils"; import { AuthClient, AuthHeader } from "./types"; import { RequestOptions, rest } from "./request"; @@ -63,13 +62,12 @@ export interface RevokeAccessTokenParams { client_id: string; } -function sha256(buffer: string) { - return crypto.createHash("sha256").update(buffer).digest(); +async function sha256(buffer: string): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(buffer))); } -function base64URLEncode(str: Buffer) { - return str - .toString("base64") +function base64URLEncode(str: Uint8Array) { + return base64Encode(str) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); @@ -242,14 +240,14 @@ export class OAuth2User implements AuthClient { }); } - generateAuthURL(options: GenerateAuthUrlOptions): string { + async generateAuthURL(options: GenerateAuthUrlOptions): Promise { const { client_id, callback, scopes } = this.#options; if (!callback) throw new Error("callback required"); if (!scopes) throw new Error("scopes required"); if (options.code_challenge_method === "s256") { - const code_verifier = base64URLEncode(crypto.randomBytes(32)); + const code_verifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32))); this.#code_verifier = code_verifier; - this.#code_challenge = base64URLEncode(sha256(code_verifier)); + this.#code_challenge = base64URLEncode(await sha256(code_verifier)); } else { this.#code_challenge = options.code_challenge; this.#code_verifier = options.code_challenge; diff --git a/src/utils.ts b/src/utils.ts index aad4eea..16d9ace 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,8 +12,10 @@ export function buildQueryString(query: Record): string { .join("&"); } +export function base64Encode(byteValues: Uint8Array): string { + return btoa([...byteValues].map(byteValue => String.fromCharCode(byteValue)).join('')) +} + export function basicAuthHeader(client_id: string, client_secret: string) { - return `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString( - "base64" - )}`; + return `Basic ${base64Encode(new TextEncoder().encode(`${client_id}:${client_secret}`))}` } diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 0000000..faa58ef --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,15 @@ +// Copyright 2021 Twitter, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { base64Encode } from "../src/utils"; + +describe("test utils", () => { + + test("base64Encode is equivalent to Buffer.toString('base64')", () => { + for (let i = 0; i < 64; i++) { // go through all 64 symbols + const byteValues = new Uint8Array([i << 2]); + expect(base64Encode(byteValues)).toEqual(Buffer.from(Buffer.from(byteValues)).toString("base64")); + } + }); + +});