Skip to content

Commit 1e3a68d

Browse files
RahulHereRahulHere
authored andcommitted
Implement OAuth discovery and fix authentication issues (#130)
- Add automatic OAuth metadata discovery from .well-known endpoints - Fix JWKS fetching by allowing HTTP protocol for local development - Update TypeScript SDK to discover JWKS URI and Issuer automatically - Filter scopes properly during OAuth client registration - Simplify configuration to only require GOPHER_AUTH_SERVER_URL - Remove hardcoded JWKS_URI and TOKEN_ISSUER requirements The SDK now follows OAuth 2.0 and OpenID Connect standards for endpoint discovery, making configuration simpler and more maintainable.
1 parent b8faafe commit 1e3a68d

File tree

3 files changed

+147
-48
lines changed

3 files changed

+147
-48
lines changed

sdk/typescript/src/authenticated-mcp-server.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,22 @@ export class AuthenticatedMcpServer {
8484
constructor(config?: AuthenticatedMcpServerConfig) {
8585
// Merge provided config with environment variables
8686
const env = process.env;
87+
88+
// Get the OAuth server URL from environment or config
89+
const oauthServerUrl = config?.authServerUrl || env['OAUTH_SERVER_URL'] || env['GOPHER_AUTH_SERVER_URL'];
90+
8791
this.config = {
8892
// Server identification
8993
serverName: config?.serverName || env['SERVER_NAME'] || "mcp-server",
9094
serverVersion: config?.serverVersion || env['SERVER_VERSION'] || "1.0.0",
9195
serverUrl: config?.serverUrl || env['SERVER_URL'] || `http://localhost:${env['SERVER_PORT'] || '3001'}`,
9296
serverPort: config?.serverPort || parseInt(env['SERVER_PORT'] || env['HTTP_PORT'] || "3001"),
9397

94-
// Authentication - support both new and legacy env vars
98+
// Authentication - these will be discovered if not provided
9599
jwksUri: config?.jwksUri || env['JWKS_URI'],
96100
tokenIssuer: config?.tokenIssuer || env['TOKEN_ISSUER'],
97101
tokenAudience: config?.tokenAudience || env['TOKEN_AUDIENCE'],
98-
authServerUrl: config?.authServerUrl || env['GOPHER_AUTH_SERVER_URL'],
102+
authServerUrl: oauthServerUrl,
99103
clientId: config?.clientId || env['GOPHER_CLIENT_ID'],
100104
clientSecret: config?.clientSecret || env['GOPHER_CLIENT_SECRET'],
101105

@@ -195,18 +199,49 @@ export class AuthenticatedMcpServer {
195199
* Initialize authentication if configured
196200
*/
197201
private async initializeAuth(): Promise<void> {
202+
// Check if we have an OAuth server URL to discover from
203+
if (this.config.authServerUrl && !this.config.jwksUri && !this.config.tokenIssuer) {
204+
// Discover OAuth metadata
205+
try {
206+
const discoveryUrl = this.config.authServerUrl.includes('/realms/')
207+
? `${this.config.authServerUrl}/.well-known/openid-configuration`
208+
: `${this.config.authServerUrl}/.well-known/oauth-authorization-server`;
209+
210+
console.error(`🔍 Discovering OAuth metadata from: ${discoveryUrl}`);
211+
212+
const response = await fetch(discoveryUrl);
213+
if (response.ok) {
214+
const metadata = await response.json() as any;
215+
216+
// Update config with discovered values
217+
if (!this.config.jwksUri && metadata.jwks_uri) {
218+
this.config.jwksUri = metadata.jwks_uri;
219+
console.error(` ✅ Discovered JWKS URI: ${metadata.jwks_uri}`);
220+
}
221+
if (!this.config.tokenIssuer && metadata.issuer) {
222+
this.config.tokenIssuer = metadata.issuer;
223+
console.error(` ✅ Discovered Issuer: ${metadata.issuer}`);
224+
}
225+
} else {
226+
console.error(` ⚠️ OAuth discovery failed: ${response.status}`);
227+
}
228+
} catch (error) {
229+
console.error(` ⚠️ OAuth discovery error: ${error}`);
230+
}
231+
}
232+
198233
// Enable auth if JWKS URI or auth server URL is configured, regardless of REQUIRE_AUTH setting
199234
const jwksUri = this.config.jwksUri ||
200235
(this.config.authServerUrl ? `${this.config.authServerUrl}/protocol/openid-connect/certs` : null);
201236

202237
// Only skip auth if no JWKS URI is configured
203238
if (!jwksUri) {
204239
console.error("⚠️ Authentication disabled");
205-
console.error(" Reason: No JWKS_URI or GOPHER_AUTH_SERVER_URL configured");
240+
console.error(" Reason: No JWKS_URI or OAUTH_SERVER_URL configured");
206241
return;
207242
}
208243

209-
// Show that authentication WOULD be enabled if the C library was available
244+
// Show that authentication is being enabled
210245
console.error("🔐 Authentication configuration detected");
211246
console.error(` JWKS URI: ${jwksUri}`);
212247
console.error(` Issuer: ${this.config.tokenIssuer || this.config.authServerUrl || "https://auth.example.com"}`);
@@ -790,10 +825,38 @@ export class AuthenticatedMcpServer {
790825
console.error(` Request body:`, JSON.stringify(registrationRequest, null, 2));
791826
console.error(` Headers:`, req.headers);
792827

793-
// Don't filter scopes - let Keycloak handle what scopes are allowed
794-
// Just log what was requested
828+
// Extract allowed scopes for filtering
829+
const mcpScopes = this.extractScopesFromTools();
830+
const allowedScopes = [
831+
"openid",
832+
"offline_access",
833+
"profile",
834+
"email",
835+
"address",
836+
"phone",
837+
"roles",
838+
...mcpScopes,
839+
...(this.config.additionalAllowedScopes || []),
840+
];
841+
const uniqueAllowedScopes = [...new Set(allowedScopes)];
842+
843+
// Filter scopes to only allow what's in our allowed list
795844
if (registrationRequest.scope) {
796-
console.error(` Requested scope: ${registrationRequest.scope}`);
845+
console.error(` Original scope: ${registrationRequest.scope}`);
846+
const requestedScopes = registrationRequest.scope.split(' ');
847+
const filteredScopes = requestedScopes.filter((scope: string) =>
848+
uniqueAllowedScopes.includes(scope)
849+
);
850+
const removedScopes = requestedScopes.filter((scope: string) =>
851+
!uniqueAllowedScopes.includes(scope)
852+
);
853+
854+
if (removedScopes.length > 0) {
855+
console.error(` 🗑️ Filtered out invalid scopes: ${removedScopes.join(', ')}`);
856+
}
857+
858+
registrationRequest.scope = filteredScopes.join(' ');
859+
console.error(` ✅ Filtered scope: ${registrationRequest.scope}`);
797860
}
798861

799862
// Forward to Keycloak

src/c_api/mcp_c_auth_api.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,9 +469,9 @@ static bool http_get(const std::string& url,
469469
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, config.verify_ssl ? 1L : 0L);
470470
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, config.verify_ssl ? 2L : 0L);
471471

472-
// Set protocol to HTTPS only for security
473-
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
474-
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
472+
// Set protocol to HTTP and HTTPS (allow both for local development)
473+
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
474+
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
475475

476476
// Set user agent
477477
curl_easy_setopt(curl, CURLOPT_USERAGENT, config.user_agent.c_str());

src/c_api/mcp_c_auth_api_network_optimized.cc

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
#include <atomic>
1515
#include <vector>
1616
#include <queue>
17-
#include <rapidjson/document.h>
18-
#include <rapidjson/writer.h>
19-
#include <rapidjson/stringbuffer.h>
17+
// RapidJSON would be used here for optimized parsing
18+
// For now, using simple JSON parsing
2019
#include <thread>
2120

2221
namespace network_optimized {
@@ -297,62 +296,99 @@ class FastJSONParser {
297296
return instance;
298297
}
299298

300-
// Parse JWKS response efficiently
299+
// Parse JWKS response efficiently (simplified JSON parsing)
301300
bool parseJWKS(const std::string& json,
302301
std::vector<std::pair<std::string, std::string>>& keys) {
303-
rapidjson::Document doc;
302+
// Simple JSON parsing without external library
303+
// In production, would use RapidJSON for better performance
304304

305-
// Use in-situ parsing for better performance (modifies input)
306-
if (doc.ParseInsitu(const_cast<char*>(json.c_str())).HasParseError()) {
307-
return false;
308-
}
305+
// Find "keys" array
306+
size_t keys_pos = json.find("\"keys\"");
307+
if (keys_pos == std::string::npos) return false;
309308

310-
if (!doc.HasMember("keys") || !doc["keys"].IsArray()) {
311-
return false;
312-
}
309+
size_t array_start = json.find('[', keys_pos);
310+
if (array_start == std::string::npos) return false;
313311

314-
const rapidjson::Value& keys_array = doc["keys"];
315-
keys.reserve(keys_array.Size());
312+
size_t array_end = json.find(']', array_start);
313+
if (array_end == std::string::npos) return false;
316314

317-
for (rapidjson::SizeType i = 0; i < keys_array.Size(); i++) {
318-
const rapidjson::Value& key = keys_array[i];
315+
// Extract each key object
316+
size_t pos = array_start + 1;
317+
while (pos < array_end) {
318+
size_t obj_start = json.find('{', pos);
319+
if (obj_start == std::string::npos || obj_start >= array_end) break;
319320

320-
if (key.HasMember("kid") && key["kid"].IsString() &&
321-
key.HasMember("x5c") && key["x5c"].IsArray() &&
322-
key["x5c"].Size() > 0) {
323-
324-
std::string kid = key["kid"].GetString();
325-
std::string cert = key["x5c"][0].GetString();
326-
327-
// Convert to PEM format
321+
size_t obj_end = json.find('}', obj_start);
322+
if (obj_end == std::string::npos || obj_end >= array_end) break;
323+
324+
std::string obj = json.substr(obj_start, obj_end - obj_start + 1);
325+
326+
// Extract kid
327+
std::string kid;
328+
size_t kid_pos = obj.find("\"kid\"");
329+
if (kid_pos != std::string::npos) {
330+
size_t value_start = obj.find('\"', kid_pos + 5);
331+
if (value_start != std::string::npos) {
332+
size_t value_end = obj.find('\"', value_start + 1);
333+
if (value_end != std::string::npos) {
334+
kid = obj.substr(value_start + 1, value_end - value_start - 1);
335+
}
336+
}
337+
}
338+
339+
// Extract x5c certificate
340+
std::string cert;
341+
size_t x5c_pos = obj.find("\"x5c\"");
342+
if (x5c_pos != std::string::npos) {
343+
size_t cert_start = obj.find('\"', obj.find('[', x5c_pos));
344+
if (cert_start != std::string::npos) {
345+
size_t cert_end = obj.find('\"', cert_start + 1);
346+
if (cert_end != std::string::npos) {
347+
cert = obj.substr(cert_start + 1, cert_end - cert_start - 1);
348+
}
349+
}
350+
}
351+
352+
if (!kid.empty() && !cert.empty()) {
328353
std::string pem = "-----BEGIN CERTIFICATE-----\n" + cert +
329354
"\n-----END CERTIFICATE-----";
330-
331355
keys.emplace_back(std::move(kid), std::move(pem));
332356
}
357+
358+
pos = obj_end + 1;
333359
}
334360

335-
return true;
361+
return !keys.empty();
336362
}
337363

338-
// Parse JWT header efficiently
364+
// Parse JWT header efficiently
339365
bool parseJWTHeader(const std::string& json,
340366
std::string& alg, std::string& kid) {
341-
rapidjson::Document doc;
342-
343-
if (doc.Parse(json.c_str()).HasParseError()) {
344-
return false;
345-
}
346-
347-
if (doc.HasMember("alg") && doc["alg"].IsString()) {
348-
alg = doc["alg"].GetString();
367+
// Extract alg
368+
size_t alg_pos = json.find("\"alg\"");
369+
if (alg_pos != std::string::npos) {
370+
size_t value_start = json.find('\"', alg_pos + 5);
371+
if (value_start != std::string::npos) {
372+
size_t value_end = json.find('\"', value_start + 1);
373+
if (value_end != std::string::npos) {
374+
alg = json.substr(value_start + 1, value_end - value_start - 1);
375+
}
376+
}
349377
}
350378

351-
if (doc.HasMember("kid") && doc["kid"].IsString()) {
352-
kid = doc["kid"].GetString();
379+
// Extract kid
380+
size_t kid_pos = json.find("\"kid\"");
381+
if (kid_pos != std::string::npos) {
382+
size_t value_start = json.find('\"', kid_pos + 5);
383+
if (value_start != std::string::npos) {
384+
size_t value_end = json.find('\"', value_start + 1);
385+
if (value_end != std::string::npos) {
386+
kid = json.substr(value_start + 1, value_end - value_start - 1);
387+
}
388+
}
353389
}
354390

355-
return true;
391+
return !alg.empty();
356392
}
357393
};
358394

0 commit comments

Comments
 (0)