Create beautiful black and white MCP-branded auth pages

**MCP Server Auth Page (src/auth/provider.ts):**
- Black background with white card design
- Official MCP logo and branding
- Clear client information display
- Elegant "Continue to Authentication" flow
- Professional MCP server identification

**Upstream Fake Auth Page (src/handlers/fakeauth.ts):**
- Updated branding to distinguish from MCP server
- Lock icon and "Upstream Authentication" title
- Clear user ID management with localStorage
- Generate/edit user ID functionality for testing
- Multi-user testing instructions

**Technical Updates:**
- Permissive CSP headers for both auth pages
- Logo serving route at /mcp-logo.png
- Responsive design with modern CSS
- Clear visual distinction between MCP server auth vs upstream auth
- Enhanced userId integration in OAuth authorization flow

Both pages now provide professional user experience while maintaining
clear distinction between the MCP server's auth and the fake upstream
provider auth for testing purposes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jerome 2025-07-08 12:33:30 +01:00
commit d8e706ff9d
No known key found for this signature in database
7 changed files with 447 additions and 22 deletions

BIN
mcp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -53,6 +53,7 @@ function getMockAuthValues() {
},
clientId: client.client_id,
issuedAt: Date.now() / 1000,
userId: "test-user-id",
};
return {
@ -279,6 +280,7 @@ describe("EverythingAuthProvider", () => {
},
clientId: "different-client-id",
issuedAt: Date.now() / 1000,
userId: "test-user-id",
};
await authService.saveRefreshToken(refreshToken, accessToken);
@ -314,6 +316,7 @@ describe("EverythingAuthProvider", () => {
},
clientId: "client-id",
issuedAt: Date.now() / 1000,
userId: "test-user-id",
};
await authService.saveMcpInstallation(accessToken, mcpInstallation);
@ -351,6 +354,7 @@ describe("EverythingAuthProvider", () => {
},
clientId: "client-id",
issuedAt: twoDaysAgoInSeconds, // 2 days ago, with 1-day expiry
userId: "test-user-id",
};
await authService.saveMcpInstallation(accessToken, mcpInstallation);
@ -377,6 +381,7 @@ describe("EverythingAuthProvider", () => {
},
clientId: client.client_id,
issuedAt: Date.now() / 1000,
userId: "test-user-id",
};
// Save the installation

View file

@ -72,22 +72,196 @@ export class EverythingAuthProvider implements OAuthServerProvider {
// You can redirect to another page, or you can send an html response directly
// res.redirect(new URL(`fakeupstreamauth/authorize?metadata=${authorizationCode}`, BASE_URI).href);
// Set permissive CSP for styling
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"style-src 'self' 'unsafe-inline'",
"script-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"object-src 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'"
].join('; '));
res.send(`
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>MCP Auth Page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Server Authorization</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #000000;
color: #ffffff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.auth-container {
background: #ffffff;
color: #000000;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(255, 255, 255, 0.1);
padding: 40px;
max-width: 500px;
width: 100%;
text-align: center;
border: 1px solid #e2e8f0;
}
.logo-container {
margin-bottom: 32px;
}
.logo {
width: 80px;
height: 80px;
margin: 0 auto 16px;
filter: invert(1);
}
.mcp-title {
font-size: 24px;
font-weight: 700;
color: #000000;
margin-bottom: 8px;
letter-spacing: 2px;
}
h1 {
color: #000000;
font-size: 32px;
font-weight: 800;
margin-bottom: 12px;
line-height: 1.2;
}
.subtitle {
color: #4a5568;
font-size: 18px;
margin-bottom: 32px;
line-height: 1.5;
}
.client-info {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
border: 2px solid #e2e8f0;
}
.client-info h3 {
color: #2d3748;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.client-id {
background: white;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a5568;
word-break: break-all;
}
.auth-flow-info {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
border-left: 4px solid #000000;
}
.auth-flow-info h3 {
color: #2d3748;
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
.auth-flow-info p {
color: #4a5568;
font-size: 14px;
line-height: 1.5;
}
.btn-primary {
background: #000000;
color: #ffffff;
font-size: 18px;
font-weight: 700;
padding: 18px 36px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
text-decoration: none;
display: inline-block;
text-align: center;
letter-spacing: 1px;
}
.btn-primary:hover {
background: #333333;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.branding {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #e2e8f0;
color: #718096;
font-size: 12px;
}
</style>
</head>
<body>
<h1>MCP Server Auth Page</h1>
<p>
This page is the authorization page presented by the MCP server, routing the user upstream. This is only
needed on 2025-03-26 Auth spec, where the MCP server acts as it's own authoriztion server. This page should
be present to avoid confused deputy attacks.
</p>
<p>
Click <a href="/fakeupstreamauth/authorize?redirect_uri=/fakeupstreamauth/callback&state=${authorizationCode}">here</a>
to continue to the upstream auth
</p>
<div class="auth-container">
<div class="logo-container">
<img src="/mcp-logo.png" alt="MCP Logo" class="logo">
<div class="mcp-title">MCP</div>
</div>
<h1>Authorization Required</h1>
<p class="subtitle">This client wants to connect to your MCP server</p>
<div class="client-info">
<h3>Client Application</h3>
<div class="client-id">${client.client_id}</div>
</div>
<div class="auth-flow-info">
<h3>What happens next?</h3>
<p>You'll be redirected to authenticate with the upstream provider. Once verified, you'll be granted access to this MCP server's resources.</p>
</div>
<a href="/fakeupstreamauth/authorize?redirect_uri=/fakeupstreamauth/callback&state=${authorizationCode}" class="btn-primary">
Continue to Authentication
</a>
<div class="branding">
Model Context Protocol (MCP) Server
</div>
</div>
</body>
</html>
`);
@ -156,6 +330,7 @@ export class EverythingAuthProvider implements OAuthServerProvider {
...mcpInstallation,
mcpTokens: newTokens,
issuedAt: Date.now() / 1000,
userId: mcpInstallation.userId, // Preserve the user ID
});
return newTokens;
@ -183,7 +358,10 @@ export class EverythingAuthProvider implements OAuthServerProvider {
token,
clientId: installation.clientId,
scopes: ['mcp'],
expiresAt
expiresAt,
extra: {
userId: installation.userId
}
};
}

View file

@ -12,18 +12,245 @@ export async function handleFakeAuthorize(req: Request, res: Response) {
// get the redirect_uri and state from the query params
const { redirect_uri, state } = req.query;
// TODO, mint actual codes?
// Set a more permissive CSP for auth pages to allow inline styles and scripts
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"style-src 'self' 'unsafe-inline'", // Allow inline styles for auth page styling
"script-src 'self' 'unsafe-inline'", // Allow inline scripts for auth page functionality
"object-src 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'"
].join('; '));
res.send(`
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Fake Upstream Auth Provider!</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upstream Provider Authentication</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.auth-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 480px;
width: 100%;
text-align: center;
}
.logo {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 16px;
margin: 0 auto 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: white;
}
h1 {
color: #1a202c;
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
}
.subtitle {
color: #718096;
font-size: 16px;
margin-bottom: 32px;
}
.user-section {
background: #f7fafc;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
border: 2px solid #e2e8f0;
}
.user-section h3 {
color: #2d3748;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.user-id-display {
background: white;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a5568;
margin-bottom: 16px;
word-break: break-all;
}
.user-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn {
flex: 1;
min-width: 120px;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.btn-primary {
background: linear-gradient(135deg, #4299e1, #3182ce);
color: white;
font-size: 16px;
padding: 16px 32px;
margin-top: 16px;
width: 100%;
}
.btn-primary:hover {
background: linear-gradient(135deg, #3182ce, #2c5282);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
}
.help-text {
color: #718096;
font-size: 14px;
margin-top: 24px;
line-height: 1.5;
}
.help-text strong {
color: #4a5568;
}
</style>
</head>
<body>
<h1>Fake Auth</h1>
<p>Fake auth page</p>
<p>Click <a href="${redirect_uri}?state=${state}&code=fakecode">here</a> to authorize</p>
<p>Click <a href="${redirect_uri}?state=${state}&code=fakecode">here</a> to fail authorization</p>
<div class="auth-container">
<div class="logo">🔒</div>
<h1>Upstream Authentication</h1>
<p class="subtitle">Please verify your identity with the upstream provider</p>
<div class="user-section">
<h3>Your User Identity</h3>
<div class="user-id-display" id="userIdDisplay">Loading...</div>
<div class="user-actions">
<button class="btn btn-secondary" onclick="generateNewUserId()">Generate New ID</button>
<button class="btn btn-secondary" onclick="editUserId()">Edit ID</button>
</div>
</div>
<button class="btn btn-primary" onclick="authorize()">
Complete Authentication
</button>
<div class="help-text">
<strong>Testing Multiple Users:</strong> Open this page in different browser windows or incognito tabs to simulate different users. Each will have their own unique User ID and separate MCP sessions.
</div>
</div>
<script>
// Generate UUID v4
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// Get or create user ID
function getUserId() {
let userId = localStorage.getItem('mcpUserId');
if (!userId) {
userId = generateUUID();
localStorage.setItem('mcpUserId', userId);
}
return userId;
}
// Update the display
function updateDisplay() {
const userId = getUserId();
document.getElementById('userIdDisplay').textContent = userId;
}
// Generate new user ID
function generateNewUserId() {
const newId = generateUUID();
localStorage.setItem('mcpUserId', newId);
updateDisplay();
}
// Edit user ID
function editUserId() {
const currentId = getUserId();
const newId = prompt('Enter new User ID:', currentId);
if (newId && newId.trim()) {
localStorage.setItem('mcpUserId', newId.trim());
updateDisplay();
}
}
// Authorize with current user ID
function authorize() {
const userId = getUserId();
// Handle relative URLs by making them absolute
const redirectUri = '${redirect_uri}';
const baseUrl = redirectUri.startsWith('http') ? redirectUri : window.location.origin + redirectUri;
const url = new URL(baseUrl);
url.searchParams.set('state', '${state}');
url.searchParams.set('code', 'fakecode');
url.searchParams.set('userId', userId);
window.location.href = url.toString();
}
// Initialize on page load
updateDisplay();
</script>
</body>
</html>
`);
@ -36,6 +263,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) {
// The state returned from the upstream auth server is actually the authorization code
state: mcpAuthorizationCode,
code: upstreamAuthorizationCode,
userId, // User ID from the authorization flow
} = req.query;
// This is where you'd exchange the upstreamAuthorizationCode for access/refresh tokens
@ -63,6 +291,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) {
mcpTokens,
clientId: pendingAuth.clientId,
issuedAt: Date.now() / 1000,
userId: (userId as string) || 'anonymous-user', // Include user ID from auth flow
}
// Store the upstream authorization data

View file

@ -36,6 +36,11 @@ const baseSecurityHeaders = (req: express.Request, res: express.Response, next:
// simple logging middleware
const logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
if (req.url.includes('/mcp')) {
console.log(' Headers:', JSON.stringify(req.headers, null, 2));
console.log(' Body:', JSON.stringify(req.body, null, 2));
}
next();
};
@ -106,7 +111,12 @@ app.get("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP
app.post("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP);
app.delete("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP);
// Upstream auth routes
// Static assets
app.get("/mcp-logo.png", (req, res) => {
res.sendFile("mcp.png", { root: "." });
});
// Upstream auth routes
app.get("/fakeupstreamauth/authorize", cors(corsOptions), handleFakeAuthorize);
app.get("/fakeupstreamauth/callback", cors(corsOptions), handleFakeAuthorizeRedirect);

View file

@ -142,7 +142,7 @@ export class MockRedisClient implements RedisClient {
options = { url: "redis://localhost:6379" };
private store = new Map<string, string>();
private lists = new Map<string, string[]>();
private subscribers = new Map<string, ((message: string) => void)[]>();
public subscribers = new Map<string, ((message: string) => void)[]>(); // Public for testing access
private errorCallbacks = new Map<string, ((error: Error) => void)[]>();
async get(key: string): Promise<string | null> {

View file

@ -193,6 +193,7 @@ describe("auth utils", () => {
},
clientId: "client-id",
issuedAt: Date.now() / 1000,
userId: "test-user-id",
}
await saveMcpInstallation(accessToken, mcpInstallation);
@ -239,6 +240,7 @@ describe("auth utils", () => {
},
clientId: "client-id",
issuedAt: Date.now() / 1000,
userId: "test-user-id",
});
const getDel = jest.spyOn(mockRedis, 'getDel').mockImplementationOnce(() => {
@ -252,6 +254,7 @@ describe("auth utils", () => {
},
clientId: "client-id",
issuedAt: Date.now() / 1000,
userId: "test-user-id",
};
const value = JSON.stringify(mcpInstallation);
const iv = crypto.randomBytes(16);