Skip to content
Open
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
38 changes: 38 additions & 0 deletions client/src/components/OAuthFlowProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<div className="mt-3 p-3 border rounded-md bg-muted/50">
<p className="font-medium text-sm mb-2">Decoded {label} (JWT)</p>
<div className="space-y-2">
<div>
<p className="text-xs font-medium text-muted-foreground">Header:</p>
<pre className="mt-1 p-2 bg-muted rounded-md overflow-auto max-h-[150px] text-xs">
{JSON.stringify(decoded.header, null, 2)}
</pre>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">Payload:</p>
<pre className="mt-1 p-2 bg-muted rounded-md overflow-auto max-h-[200px] text-xs">
{JSON.stringify(decoded.payload, null, 2)}
</pre>
</div>
</div>
</div>
);
};

interface OAuthFlowProgressProps {
serverUrl: string;
authState: AuthDebuggerState;
Expand Down Expand Up @@ -352,6 +386,10 @@ export const OAuthFlowProgress = ({
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(authState.oauthTokens, null, 2)}
</pre>
<DecodedJWTDisplay
token={authState.oauthTokens.access_token}
label="Access Token"
/>
</details>
)}
</OAuthStepDetails>
Expand Down
65 changes: 65 additions & 0 deletions client/src/utils/jwtUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Utilities for decoding JWT tokens (JWS format)
*/

export interface DecodedJWT {
header: Record<string, unknown>;
payload: Record<string, unknown>;
}

/**
* 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;
}
}