Extract shared PKCE helpers into src/utils/pkce.ts

Deduplicates base64UrlEncode, generateCodeVerifier, and
generateCodeChallenge which were copy-pasted across useMcpOAuthFlow
and useUserMcpOAuthFlow hooks.
This commit is contained in:
Ryan Crabbe
2026-04-16 10:35:52 -07:00
parent 72a461ba4a
commit 5aba1841cc
3 changed files with 22 additions and 40 deletions
@@ -11,6 +11,7 @@ import {
serverRootPath,
} from "@/components/networking";
import { extractErrorMessage } from "@/utils/errorUtils";
import { generateCodeChallenge, generateCodeVerifier } from "@/utils/pkce";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
export type McpOAuthStatus = "idle" | "authorizing" | "exchanging" | "success" | "error";
@@ -34,25 +35,6 @@ interface UseMcpOAuthFlowResult {
tokenResponse: Record<string, any> | null;
}
const base64UrlEncode = (buffer: ArrayBuffer) => {
const bytes = new Uint8Array(buffer);
let binary = "";
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
};
const generateCodeVerifier = () => {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return base64UrlEncode(array.buffer);
};
const generateCodeChallenge = async (verifier: string) => {
const data = new TextEncoder().encode(verifier);
const digest = await window.crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(digest);
};
export const useMcpOAuthFlow = ({
accessToken,
getCredentials,
@@ -23,6 +23,7 @@ import {
} from "@/components/networking";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { extractErrorMessage } from "@/utils/errorUtils";
import { generateCodeChallenge, generateCodeVerifier } from "@/utils/pkce";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
export type UserMcpOAuthStatus = "idle" | "authorizing" | "exchanging" | "success" | "error";
@@ -60,25 +61,6 @@ type StoredFlowState = {
scopes?: string[];
};
const b64url = (buf: ArrayBuffer) => {
const bytes = new Uint8Array(buf);
let s = "";
bytes.forEach((b) => (s += String.fromCharCode(b)));
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
};
const genVerifier = () => {
const arr = new Uint8Array(32);
window.crypto.getRandomValues(arr);
return b64url(arr.buffer);
};
const genChallenge = async (verifier: string) => {
const data = new TextEncoder().encode(verifier);
const digest = await window.crypto.subtle.digest("SHA-256", data);
return b64url(digest);
};
const setStorage = (key: string, value: string) => {
setSecureItem(key, value);
};
@@ -144,8 +126,8 @@ export const useUserMcpOAuthFlow = ({
}
}
const verifier = genVerifier();
const challenge = await genChallenge(verifier);
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
const state = crypto.randomUUID();
const redirectUri = buildCallbackUrl();
const scopeString = scopes?.filter((s) => s.trim()).join(" ");
+18
View File
@@ -0,0 +1,18 @@
const base64UrlEncode = (buffer: ArrayBuffer) => {
const bytes = new Uint8Array(buffer);
let binary = "";
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
};
export const generateCodeVerifier = () => {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return base64UrlEncode(array.buffer);
};
export const generateCodeChallenge = async (verifier: string) => {
const data = new TextEncoder().encode(verifier);
const digest = await window.crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(digest);
};