Skip to content

Commit fb2de61

Browse files
committed
feat: add JWT decoding utility and display component
- Introduced a new utility for decoding JWT tokens in `jwtUtils.ts`. - Added `DecodedJWTDisplay` component in `OAuthFlowProgress.tsx` to visualize the decoded access token's header and payload. - Integrated the new component into the OAuth flow progress display.
1 parent e9e38b0 commit fb2de61

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

client/src/components/OAuthFlowProgress.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "react";
66
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
77
import { validateRedirectUrl } from "@/utils/urlValidation";
88
import { useToast } from "@/lib/hooks/useToast";
9+
import { decodeJWT } from "@/utils/jwtUtils";
910

1011
interface OAuthStepProps {
1112
label: string;
@@ -51,6 +52,39 @@ const OAuthStepDetails = ({
5152
);
5253
};
5354

55+
interface DecodedJWTDisplayProps {
56+
token: string;
57+
label: string;
58+
}
59+
60+
const DecodedJWTDisplay = ({ token, label }: DecodedJWTDisplayProps) => {
61+
const decoded = useMemo(() => decodeJWT(token), [token]);
62+
63+
if (!decoded) {
64+
return null;
65+
}
66+
67+
return (
68+
<div className="mt-3 p-3 border rounded-md bg-muted/50">
69+
<p className="font-medium text-sm mb-2">Decoded {label} (JWT)</p>
70+
<div className="space-y-2">
71+
<div>
72+
<p className="text-xs font-medium text-muted-foreground">Header:</p>
73+
<pre className="mt-1 p-2 bg-muted rounded-md overflow-auto max-h-[150px] text-xs">
74+
{JSON.stringify(decoded.header, null, 2)}
75+
</pre>
76+
</div>
77+
<div>
78+
<p className="text-xs font-medium text-muted-foreground">Payload:</p>
79+
<pre className="mt-1 p-2 bg-muted rounded-md overflow-auto max-h-[200px] text-xs">
80+
{JSON.stringify(decoded.payload, null, 2)}
81+
</pre>
82+
</div>
83+
</div>
84+
</div>
85+
);
86+
};
87+
5488
interface OAuthFlowProgressProps {
5589
serverUrl: string;
5690
authState: AuthDebuggerState;
@@ -352,6 +386,10 @@ export const OAuthFlowProgress = ({
352386
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
353387
{JSON.stringify(authState.oauthTokens, null, 2)}
354388
</pre>
389+
<DecodedJWTDisplay
390+
token={authState.oauthTokens.access_token}
391+
label="Access Token"
392+
/>
355393
</details>
356394
)}
357395
</OAuthStepDetails>

client/src/utils/jwtUtils.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Utilities for decoding JWT tokens (JWS format)
3+
*/
4+
5+
export interface DecodedJWT {
6+
header: Record<string, unknown>;
7+
payload: Record<string, unknown>;
8+
}
9+
10+
/**
11+
* Checks if a string looks like a JWT (JWS format: header.payload.signature)
12+
*/
13+
export function isJWT(token: string): boolean {
14+
if (!token || typeof token !== "string") {
15+
return false;
16+
}
17+
18+
const parts = token.split(".");
19+
if (parts.length !== 3) {
20+
return false;
21+
}
22+
23+
// Check if each part is valid base64url
24+
const base64urlRegex = /^[A-Za-z0-9_-]*$/;
25+
return parts.every((part) => base64urlRegex.test(part));
26+
}
27+
28+
/**
29+
* Decodes a base64url string to a regular string
30+
*/
31+
function base64urlDecode(str: string): string {
32+
// Replace base64url characters with base64 characters
33+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
34+
35+
// Add padding if needed
36+
const padding = base64.length % 4;
37+
if (padding) {
38+
base64 += "=".repeat(4 - padding);
39+
}
40+
41+
return atob(base64);
42+
}
43+
44+
/**
45+
* Decodes a JWT token and returns the header and payload as objects.
46+
* Does NOT verify the signature - this is for display purposes only.
47+
*
48+
* @param token - The JWT token string
49+
* @returns The decoded header and payload, or null if decoding fails
50+
*/
51+
export function decodeJWT(token: string): DecodedJWT | null {
52+
if (!isJWT(token)) {
53+
return null;
54+
}
55+
56+
try {
57+
const parts = token.split(".");
58+
const header = JSON.parse(base64urlDecode(parts[0]));
59+
const payload = JSON.parse(base64urlDecode(parts[1]));
60+
61+
return { header, payload };
62+
} catch {
63+
return null;
64+
}
65+
}

0 commit comments

Comments
 (0)