Skip to content

Instantly share code, notes, and snippets.

@erebe
Created June 19, 2026 08:58
Show Gist options
  • Select an option

  • Save erebe/a5de36d42214721b2466fb0e66f61c5e to your computer and use it in GitHub Desktop.

Select an option

Save erebe/a5de36d42214721b2466fb0e66f61c5e to your computer and use it in GitHub Desktop.
oauth_proxy.rs
/// Oauth2Service is a service that handles the proxying of oauth2 requests in the case of MCP server
/// We use/forward to Auth0 behind the scene but we need to intercept because:
/// * We don't want to support dynamic client registration in auth0, as we should enable it for the whole tenant and it causes security implications
/// *but* we wants users to use it without having to give them our oauth2 client and secret id before hands
/// *thus* we need to fake it in this server and returns our static creds. So it provides a smooth experience. Other implementations seems to do it too
/// There is new rfc/spec in progress to paliate to this https://auth0.com/blog/cimd-vs-dcr-mcp-registration/
///
/// * Auth0 is not Oauth2 MCP compliant by default. It uses `audience` as a parameter in the token request, but MCP/oauth2 expects `resource`
/// * We want users to generate a token for the audience/api of the CORE, *but* it is not possible by default as client check the audience/resource is the same
/// as the hostname of this server. So we intercept the request and replace the audience/resource with the audience of the CORE.
pub struct Oauth2Service {
original_oauth_server: Uri,
current_oauth_server: Uri,
oauth_client_id: String,
oauth_client_secret: String,
oauth_audience_override: String,
http_client: reqwest::Client,
response_auth_required: (StatusCode, HeaderMap),
response_oauth2_protected_resources: Arc<Value>,
}
impl Oauth2Service {
pub fn new(
original_oauth_server: Uri,
current_oauth_server: Uri,
oauth_client_id: String,
oauth_client_secret: String,
oauth_audience: String,
) -> Arc<Self> {
let response_auth_required = {
let headers = HeaderMap::from_iter([
(CONTENT_TYPE, HeaderValue::from_static("application/json")),
(WWW_AUTHENTICATE, HeaderValue::from_str(&format!("Bearer error=\"authentification_required\" resource_metadata=\"{}.well-known/oauth-protected-resource\"", current_oauth_server)).expect("Failed to format WWW-Authenticate header"))
]);
(StatusCode::UNAUTHORIZED, headers)
};
let response_oauth2_protected_resources = Arc::new(json!({
"resource": current_oauth_server.to_string(),
"authorization_servers": [
current_oauth_server.to_string()
],
"scopes_supported": [
"email", "offline_access"
],
"bearer_methods_supported": [
"header"
]
}));
let this = Self {
original_oauth_server,
current_oauth_server,
oauth_client_id,
oauth_client_secret,
oauth_audience_override: oauth_audience,
http_client: reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.redirect(reqwest::redirect::Policy::none())
.gzip(true)
.user_agent("Qovery MCP Server")
.build()
.expect("Cannot build reqwest client"),
response_auth_required,
response_oauth2_protected_resources,
};
Arc::new(this)
}
pub fn axum_router(self: Arc<Self>) -> Router {
Router::new()
.route("/oauth/register", post(Self::oidc_register))
.route("/oauth/authorize", get(Self::oauth2_authorize))
.route("/oauth/token", post(Self::oauth2_token_proxy))
.route(
"/.well-known/oauth-protected-resource",
get(Self::oauth2_protected_resources),
)
.route("/.well-known/oauth-authorization-server", get(Self::oauth2_auth_server))
.with_state(self)
}
pub fn response_auth_required(&self) -> axum::response::Response {
self.response_auth_required.clone().into_response()
}
async fn oidc_register(
State(srv): State<Arc<Oauth2Service>>,
Json(auth_request): Json<RegisterPayload>,
) -> Json<Value> {
// const CALLBACK_URL: [&str; 2] = [
// "http://localhost:4242/callback",
// "https://claude.ai/api/mcp/auth_callback",
// ];
//
// if !auth_request
// .redirect_uris
// .iter()
// .any(|uri| CALLBACK_URL.iter().any(|callback| callback == uri))
// {
// return Json(json!({
// "error": "invalid_redirect_uri",
// "description": format!("The only redirect_uri/callback allowed is `{CALLBACK_URL:?}`. It is a limitation of our IDP provider (Auth0). Please pin the port with --callback-port in claude or use a token")
// }));
// }
let json = json!({
"client_id": srv.oauth_client_id,
"client_secret": srv.oauth_client_secret,
"client_id_issued_at": 1710000000,
"client_name": auth_request.client_name,
"redirect_uris": auth_request.redirect_uris,
"grant_types": auth_request.grant_types,
"response_types": auth_request.response_types,
"token_endpoint_auth_method": auth_request.token_endpoint_auth_method,
});
Json(json)
}
// We intercept to inject correct audience/resource and ask to contact Auth0 to get the token
async fn oauth2_authorize(State(srv): State<Arc<Oauth2Service>>, req: Parts) -> Redirect {
info!("Oauth2 authorize request: {:?}", req);
let query = req.uri.path_and_query().and_then(|p| p.query()).unwrap_or("");
let mut params: HashMap<String, String> = serde_urlencoded::from_str(query).unwrap_or_default();
params.insert("resource".to_string(), srv.oauth_audience_override.clone());
params.insert("audience".to_string(), srv.oauth_audience_override.clone());
Redirect::to(
format!(
"{}authorize?{}",
srv.original_oauth_server,
serde_urlencoded::to_string(&params).unwrap_or_default()
)
.as_str(),
)
}
/// OAuth2 flow is done/user authentificated, we fetch the token from Auth0 with the correct audience/resource
/// We proxy and not redirect as the info is inside the body of the request.
async fn oauth2_token_proxy(
State(srv): State<Arc<Oauth2Service>>,
mut req: Parts,
body: Bytes,
) -> Result<(StatusCode, http::HeaderMap, Bytes), (StatusCode, String)> {
trace!("Token proxy request: {:?} {:?}", req, body);
const EMPTY_HEADER: fn() -> HeaderValue = || HeaderValue::from_static("");
let mut params: HashMap<String, String> =
serde_urlencoded::from_bytes(&body).map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
params.insert("resource".to_string(), srv.oauth_audience_override.clone());
params.insert("audience".to_string(), srv.oauth_audience_override.clone());
let (content_type, auth) = {
let mut headers = std::mem::take(&mut req.headers);
(
headers.remove(CONTENT_TYPE).unwrap_or(EMPTY_HEADER()),
headers.remove(AUTHORIZATION).unwrap_or(EMPTY_HEADER()),
)
};
let mut resp = srv
.http_client
.post(format!("{}oauth/token", srv.original_oauth_server))
.header(CONTENT_TYPE, content_type)
.header(AUTHORIZATION, auth)
.body(serde_urlencoded::to_string(&params).unwrap_or_default())
.send()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let status = resp.status();
let headers = std::mem::take(resp.headers_mut());
let body = resp
.bytes()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok((status, headers, body))
}
/// Advertise the oauth2 config of this server to the client.
/// The client uses this to know where to authenticate with the oauth2 server
async fn oauth2_protected_resources(State(srv): State<Arc<Oauth2Service>>, req: Request) -> Json<Arc<Value>> {
trace!("Oauth2 protected resource request: {:?}", req);
Json(srv.response_oauth2_protected_resources.clone())
}
/// We fetch the information about the auth server from Auth0.
/// And modify only the part we need for the client to contact us on specific endpoints
/// ie.:curl https://auth.qovery.com/.well-known/oauth-authorization-server | jq .
pub async fn oauth2_auth_server(
State(srv): State<Arc<Oauth2Service>>,
) -> Result<Json<Value>, (StatusCode, String)> {
let response = srv
.http_client
.get(format!(
"{}.well-known/oauth-authorization-server",
srv.original_oauth_server
))
.send()
.await
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
trace!("Oauth2 auth server response: {:?}", response);
let mut json: Value = response
.json()
.await
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
*json.get_mut("token_endpoint").unwrap_or(&mut Value::Null) =
Value::String(format!("{}oauth/token", srv.current_oauth_server));
*json.get_mut("authorization_endpoint").unwrap_or(&mut Value::Null) =
Value::String(format!("{}oauth/authorize", srv.current_oauth_server));
*json.get_mut("registration_endpoint").unwrap_or(&mut Value::Null) =
Value::String(format!("{}oauth/register", srv.current_oauth_server));
Ok(Json(json))
}
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
struct RegisterPayload {
pub client_name: String,
pub grant_types: Vec<String>,
pub redirect_uris: Vec<String>,
pub response_types: Vec<String>,
pub token_endpoint_auth_method: String,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment