diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 6e0fd695..5656319c 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "react"; import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js"; import { validateRedirectUrl } from "@/utils/urlValidation"; import { useToast } from "@/lib/hooks/useToast"; +import { decodeJWT } from "@/utils/jwtUtils"; interface OAuthStepProps { label: string; @@ -51,6 +52,39 @@ const OAuthStepDetails = ({ ); }; +interface DecodedJWTDisplayProps { + token: string; + label: string; +} + +const DecodedJWTDisplay = ({ token, label }: DecodedJWTDisplayProps) => { + const decoded = useMemo(() => decodeJWT(token), [token]); + + if (!decoded) { + return null; + } + + return ( +
+

Decoded {label} (JWT)

+
+
+

Header:

+
+            {JSON.stringify(decoded.header, null, 2)}
+          
+
+
+

Payload:

+
+            {JSON.stringify(decoded.payload, null, 2)}
+          
+
+
+
+ ); +}; + interface OAuthFlowProgressProps { serverUrl: string; authState: AuthDebuggerState; @@ -352,6 +386,10 @@ export const OAuthFlowProgress = ({
                 {JSON.stringify(authState.oauthTokens, null, 2)}
               
+ )} diff --git a/client/src/utils/jwtUtils.ts b/client/src/utils/jwtUtils.ts new file mode 100644 index 00000000..b2d2e1e9 --- /dev/null +++ b/client/src/utils/jwtUtils.ts @@ -0,0 +1,65 @@ +/** + * Utilities for decoding JWT tokens (JWS format) + */ + +export interface DecodedJWT { + header: Record; + payload: Record; +} + +/** + * Checks if a string looks like a JWT (JWS format: header.payload.signature) + */ +export function isJWT(token: string): boolean { + if (!token || typeof token !== "string") { + return false; + } + + const parts = token.split("."); + if (parts.length !== 3) { + return false; + } + + // Check if each part is valid base64url + const base64urlRegex = /^[A-Za-z0-9_-]*$/; + return parts.every((part) => base64urlRegex.test(part)); +} + +/** + * Decodes a base64url string to a regular string + */ +function base64urlDecode(str: string): string { + // Replace base64url characters with base64 characters + let base64 = str.replace(/-/g, "+").replace(/_/g, "/"); + + // Add padding if needed + const padding = base64.length % 4; + if (padding) { + base64 += "=".repeat(4 - padding); + } + + return atob(base64); +} + +/** + * Decodes a JWT token and returns the header and payload as objects. + * Does NOT verify the signature - this is for display purposes only. + * + * @param token - The JWT token string + * @returns The decoded header and payload, or null if decoding fails + */ +export function decodeJWT(token: string): DecodedJWT | null { + if (!isJWT(token)) { + return null; + } + + try { + const parts = token.split("."); + const header = JSON.parse(base64urlDecode(parts[0])); + const payload = JSON.parse(base64urlDecode(parts[1])); + + return { header, payload }; + } catch { + return null; + } +}