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;
+ }
+}