feat: add OAuth 2.0 authentication and README

- Implement RFC 8414 OAuth authorization server metadata
- Add dynamic client registration (RFC 7591)
- Add /authorize and /token endpoints
- Create comprehensive README with setup instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude (Lupul Augmentat)
2026-01-27 07:32:42 +01:00
parent 250420e9e2
commit 874e871ec2
2 changed files with 214 additions and 11 deletions

View File

@@ -67,26 +67,94 @@ async function startSSEServer() {
res.send('healthy\n');
});
// OAuth discovery endpoints - return empty/minimal responses
// Claude Code checks these before connecting
// OAuth 2.0 Authorization Server Metadata (RFC 8414)
// This tells Claude Code how to authenticate
const serverUrl = 'https://ultra.runningwolf.com';
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
res.json({
issuer: serverUrl,
authorization_endpoint: `${serverUrl}/authorize`,
token_endpoint: `${serverUrl}/token`,
registration_endpoint: `${serverUrl}/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: ['none'],
});
});
// Protected Resource Metadata (RFC 9728)
app.get('/.well-known/oauth-protected-resource', (_req, res) => {
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
res.json({
resource: serverUrl,
authorization_servers: [serverUrl],
bearer_methods_supported: ['header'],
});
});
app.get('/.well-known/oauth-protected-resource/*', (_req, res) => {
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
});
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
res.json({
resource: serverUrl,
authorization_servers: [serverUrl],
bearer_methods_supported: ['header'],
});
});
// OpenID Connect discovery (optional, return 404)
app.get('/.well-known/openid-configuration', (_req, res) => {
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
res.status(404).json({ error: 'not_supported' });
});
app.post('/register', (_req, res) => {
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
// Dynamic Client Registration (RFC 7591)
// Auto-register any client that asks
app.post('/register', (req, res) => {
const clientId = `client_${Date.now()}`;
res.status(201).json({
client_id: clientId,
client_secret: '', // Public client, no secret needed
client_id_issued_at: Math.floor(Date.now() / 1000),
grant_types: ['authorization_code'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
});
});
// Authorization endpoint - return the API key as code
app.get('/authorize', (req, res) => {
const redirectUri = req.query.redirect_uri as string;
const state = req.query.state as string;
if (!redirectUri) {
res.status(400).json({ error: 'invalid_request', error_description: 'Missing redirect_uri' });
return;
}
// Return the API key as the authorization code
const code = API_KEY;
const redirectUrl = new URL(redirectUri);
redirectUrl.searchParams.set('code', code || 'no-api-key');
if (state) redirectUrl.searchParams.set('state', state);
res.redirect(redirectUrl.toString());
});
// Token endpoint - exchange code for access token
app.post('/token', express.urlencoded({ extended: true }), (req, res) => {
const code = req.body.code;
// Verify the code is our API key
if (code !== API_KEY) {
res.status(400).json({ error: 'invalid_grant', error_description: 'Invalid authorization code' });
return;
}
// Return the API key as access token
res.json({
access_token: API_KEY,
token_type: 'Bearer',
expires_in: 86400 * 365, // 1 year
});
});
// SSE endpoint - establishes the SSE stream (protected)