use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use secrecy::ExposeSecret;
use std::time::{SystemTime, UNIX_EPOCH};
use turbomcp_auth::AuthContext;
use crate::error::{ProxyError, ProxyResult};
#[derive(Clone)]
pub struct JwtSigner {
secret: secrecy::SecretString,
algorithm: Algorithm,
issuer: String,
audience: Option<String>,
ttl: u64,
}
impl std::fmt::Debug for JwtSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JwtSigner")
.field("secret", &"<redacted>")
.field("algorithm", &self.algorithm)
.field("issuer", &self.issuer)
.field("audience", &self.audience)
.field("ttl", &self.ttl)
.finish()
}
}
impl JwtSigner {
#[must_use]
pub fn new(secret: String, issuer: String) -> Self {
Self {
secret: secrecy::SecretString::from(secret),
algorithm: Algorithm::HS256,
issuer,
audience: None,
ttl: 3600, }
}
#[must_use]
pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
self.algorithm = algorithm;
self
}
#[must_use]
pub fn with_audience(mut self, audience: String) -> Self {
self.audience = Some(audience);
self
}
#[must_use]
pub fn with_ttl(mut self, ttl: u64) -> Self {
self.ttl = ttl;
self
}
pub fn sign(&self, auth_context: &AuthContext) -> ProxyResult<String> {
let now = Self::current_timestamp()?;
let mut backend_context = auth_context.clone();
backend_context.iss = Some(self.issuer.clone());
backend_context.aud.clone_from(&self.audience);
backend_context.iat = Some(now);
backend_context.exp = Some(now + self.ttl);
let claims = backend_context.to_jwt_claims();
self.encode_jwt(&claims)
}
pub fn sign_minimal(&self, sub: &str, roles: &[String]) -> ProxyResult<String> {
let now = Self::current_timestamp()?;
let claims = serde_json::json!({
"sub": sub,
"roles": roles,
"iss": self.issuer,
"aud": self.audience,
"iat": now,
"exp": now + self.ttl,
});
self.encode_jwt(&claims)
}
fn current_timestamp() -> ProxyResult<u64> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| ProxyError::Auth(format!("System time error: {e}")))
.map(|d| d.as_secs())
}
fn encode_jwt(&self, claims: &serde_json::Value) -> ProxyResult<String> {
let header = Header::new(self.algorithm);
let encoding_key = EncodingKey::from_secret(self.secret.expose_secret().as_bytes());
encode(&header, claims, &encoding_key)
.map_err(|e| ProxyError::Auth(format!("JWT signing failed: {e}")))
}
}
#[derive(Debug, Clone, Default)]
pub struct ProxyAuthConfig {
pub jwt_signer: Option<JwtSigner>,
pub require_auth: bool,
}
impl ProxyAuthConfig {
#[must_use]
pub fn with_jwt_signing(jwt_signer: JwtSigner) -> Self {
Self {
jwt_signer: Some(jwt_signer),
require_auth: false,
}
}
#[must_use]
pub fn require_auth(mut self) -> Self {
self.require_auth = true;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use jsonwebtoken::Algorithm;
use serde_json;
use std::collections::HashMap;
use turbomcp_auth::UserInfo;
fn create_test_auth_context() -> AuthContext {
AuthContext::builder()
.subject("test_user")
.user(UserInfo {
id: "test_user".to_string(),
username: "testuser".to_string(),
email: Some("test@example.com".to_string()),
display_name: Some("Test User".to_string()),
avatar_url: None,
metadata: HashMap::new(),
})
.provider("test")
.roles(vec!["admin".to_string(), "user".to_string()])
.permissions(vec!["read:data".to_string(), "write:data".to_string()])
.build()
.unwrap()
}
#[test]
fn test_jwt_signer_creation() {
let signer = JwtSigner::new("test-secret".to_string(), "test-proxy".to_string());
assert_eq!(signer.issuer, "test-proxy");
assert_eq!(signer.algorithm, Algorithm::HS256);
assert_eq!(signer.ttl, 3600);
}
#[test]
fn test_jwt_signer_with_options() {
let signer = JwtSigner::new("test-secret".to_string(), "test-proxy".to_string())
.with_algorithm(Algorithm::HS512)
.with_audience("backend-server".to_string())
.with_ttl(7200);
assert_eq!(signer.algorithm, Algorithm::HS512);
assert_eq!(signer.audience, Some("backend-server".to_string()));
assert_eq!(signer.ttl, 7200);
}
#[test]
fn test_sign_auth_context() {
let signer = JwtSigner::new("test-secret".to_string(), "test-proxy".to_string())
.with_audience("backend-server".to_string());
let auth_context = create_test_auth_context();
let jwt = signer.sign(&auth_context);
assert!(jwt.is_ok());
let jwt_str = jwt.unwrap();
assert!(!jwt_str.is_empty());
assert!(jwt_str.contains('.')); }
#[test]
fn test_sign_minimal() {
let signer = JwtSigner::new("test-secret".to_string(), "test-proxy".to_string());
let jwt = signer.sign_minimal("test_user", &["admin".to_string()]);
assert!(jwt.is_ok());
let jwt_str = jwt.unwrap();
assert!(!jwt_str.is_empty());
}
#[test]
fn test_proxy_auth_config_default() {
let config = ProxyAuthConfig::default();
assert!(config.jwt_signer.is_none());
assert!(!config.require_auth);
}
#[test]
fn test_proxy_auth_config_with_jwt_signing() {
let signer = JwtSigner::new("test-secret".to_string(), "test-proxy".to_string());
let config = ProxyAuthConfig::with_jwt_signing(signer).require_auth();
assert!(config.jwt_signer.is_some());
assert!(config.require_auth);
}
#[test]
fn test_mcp_security_compliance() {
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
let signer = JwtSigner::new("secret".to_string(), "proxy".to_string())
.with_audience("backend".to_string());
let auth_context = create_test_auth_context();
let backend_jwt = signer.sign(&auth_context).unwrap();
let key = DecodingKey::from_secret("secret".as_bytes());
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience(&["backend"]);
validation.set_issuer(&["proxy"]);
let decoded = decode::<serde_json::Value>(&backend_jwt, &key, &validation).unwrap();
assert_eq!(decoded.claims["aud"], "backend");
assert_eq!(decoded.claims["iss"], "proxy");
assert!(decoded.claims["iat"].is_number());
assert!(decoded.claims["exp"].is_number());
}
#[test]
fn test_jwt_roundtrip() {
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
use serde_json;
let secret = "test-secret";
let signer = JwtSigner::new(secret.to_string(), "test-proxy".to_string())
.with_audience("backend-server".to_string());
let auth_context = create_test_auth_context();
let jwt = signer.sign(&auth_context).unwrap();
let decoding_key = DecodingKey::from_secret(secret.as_bytes());
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience(&["backend-server"]);
validation.set_issuer(&["test-proxy"]);
let decoded = decode::<serde_json::Value>(&jwt, &decoding_key, &validation);
assert!(decoded.is_ok());
let claims = decoded.unwrap().claims;
assert_eq!(claims["sub"], "test_user");
assert_eq!(claims["iss"], "test-proxy");
assert_eq!(claims["aud"], "backend-server");
}
}