use crate::error::FastMCPError;
use crate::mcp::types::JsonRpcRequest;
use crate::server::auth::{AuthContext, AuthProvider};
use async_trait::async_trait;
use jsonwebtoken::{DecodingKey, Validation, decode, decode_header};
use reqwest;
use serde::{Deserialize, Serialize};
use std::env;
pub struct SupabaseProvider {
project_url: String,
jwks_url: String,
issuer: String,
}
#[derive(Debug, Deserialize)]
struct Jwks {
keys: Vec<JwkKey>,
}
#[derive(Debug, Deserialize)]
struct JwkKey {
kid: String,
#[allow(dead_code)]
kty: String,
n: String,
e: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
iss: String,
aud: Option<String>,
exp: usize,
email: Option<String>,
role: Option<String>,
}
impl SupabaseProvider {
pub fn new(project_url: &str) -> Self {
let base = project_url.trim_end_matches('/');
Self {
project_url: base.to_string(),
jwks_url: format!("{}/auth/v1/.well-known/jwks.json", base),
issuer: format!("{}/auth/v1", base),
}
}
pub fn from_env() -> Result<Self, FastMCPError> {
let project_url = env::var("OXFASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL").map_err(|_| {
FastMCPError::new("Missing OXFASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL".to_string())
})?;
Ok(Self::new(&project_url))
}
async fn fetch_jwk(&self, kid: &str) -> Result<JwkKey, FastMCPError> {
let client = reqwest::Client::new();
let resp = client
.get(&self.jwks_url)
.send()
.await
.map_err(|e| FastMCPError::new(format!("Supabase JWKS fetch failed: {}", e)))?;
let jwks: Jwks = resp
.json()
.await
.map_err(|e| FastMCPError::new(format!("Invalid Supabase JWKS JSON: {}", e)))?;
jwks.keys
.into_iter()
.find(|k| k.kid == kid)
.ok_or_else(|| FastMCPError::new(format!("Key ID {} not found in Supabase JWKS", kid)))
}
}
#[async_trait]
impl AuthProvider for SupabaseProvider {
async fn verify(&self, request: &JsonRpcRequest) -> Result<AuthContext, FastMCPError> {
let token_str = request
.transport_metadata
.as_ref()
.and_then(|m| m.get("Authorization").or_else(|| m.get("authorization")))
.and_then(|h| h.strip_prefix("Bearer "))
.or_else(|| {
request
.params
.as_ref()
.and_then(|p| p.get("token"))
.and_then(|v| v.as_str())
})
.ok_or_else(|| FastMCPError::InvalidRequest("Missing token".to_string()))?;
let header = decode_header(token_str)
.map_err(|e| FastMCPError::InvalidRequest(format!("Invalid Token Header: {}", e)))?;
let kid = header.kid.ok_or_else(|| {
FastMCPError::InvalidRequest("Missing kid in token header".to_string())
})?;
let jwk = self.fetch_jwk(&kid).await?;
let mut validation = Validation::new(header.alg);
validation.set_issuer(&[&self.issuer]);
validation.validate_aud = false;
let key = DecodingKey::from_rsa_components(&jwk.n, &jwk.e)
.map_err(|e| FastMCPError::new(format!("Invalid key components: {}", e)))?;
let token_data = decode::<Claims>(token_str, &key, &validation).map_err(|e| {
FastMCPError::InvalidRequest(format!("Supabase Token Verification Failed: {}", e))
})?;
Ok(AuthContext {
client_id: Some(self.project_url.clone()),
user_id: Some(token_data.claims.sub),
scopes: token_data.claims.role.map(|r| vec![r]).unwrap_or_default(),
})
}
}