use super::types::RequestAuthorization;
use do_memory_mcp::protocol::OAuthConfig;
use do_memory_mcp::server::rate_limiter::OperationType;
use serde_json::Value;
#[cfg(feature = "oauth")]
use {
super::types::{AuthenticatedPrincipal, AuthorizationResult},
jsonwebtoken::{DecodingKey, Validation, decode},
serde::{Deserialize, Serialize},
tracing::{debug, warn},
};
#[cfg(feature = "oauth")]
pub const SCOPE_WRITE: &str = "mcp:write";
#[cfg(feature = "oauth")]
pub const SCOPE_READ: &str = "mcp:read";
pub const UNAUTHORIZED_CODE: i32 = -32001;
#[cfg(feature = "oauth")]
pub const INSUFFICIENT_SCOPE_CODE: i32 = -32003;
pub fn load_oauth_config() -> OAuthConfig {
load_oauth_config_from(&process_env)
}
fn process_env(var: &str) -> Option<String> {
std::env::var(var).ok()
}
pub(crate) fn load_oauth_config_from(lookup: &dyn Fn(&str) -> Option<String>) -> OAuthConfig {
let enabled = lookup("MCP_OAUTH_ENABLED")
.unwrap_or_else(|| "false".to_string())
.to_lowercase();
OAuthConfig {
enabled: enabled == "true" || enabled == "1" || enabled == "yes",
audience: lookup("MCP_OAUTH_AUDIENCE"),
issuer: lookup("MCP_OAUTH_ISSUER"),
scopes: lookup("MCP_OAUTH_SCOPES")
.unwrap_or_else(|| "mcp:read,mcp:write".to_string())
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
jwks_uri: lookup("MCP_OAUTH_JWKS_URI"),
token_secret: lookup("MCP_OAUTH_TOKEN_SECRET"),
}
}
pub fn load_transport_token() -> Option<String> {
load_transport_token_from(&process_env)
}
pub(crate) fn load_transport_token_from(lookup: &dyn Fn(&str) -> Option<String>) -> Option<String> {
for var in ["MCP_OAUTH_TOKEN", "MCP_OAUTH_BEARER_TOKEN"] {
if let Some(value) = lookup(var) {
let value = value.trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
#[cfg(feature = "oauth")]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Claims {
iss: Option<String>,
aud: Option<String>,
exp: Option<u64>,
sub: String,
scope: Option<String>,
}
#[cfg(feature = "oauth")]
fn parse_scopes(scope: Option<&str>) -> Vec<String> {
scope
.map(|s| {
s.split(' ')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
#[cfg(feature = "oauth")]
pub fn validate_access_token(
token: &str,
config: &OAuthConfig,
) -> Result<AuthenticatedPrincipal, String> {
let mut validation = Validation::default();
if let Some(expected_iss) = &config.issuer {
validation.set_issuer(&[expected_iss]);
}
if let Some(expected_aud) = &config.audience {
validation.set_audience(&[expected_aud]);
}
let decoding_key = if let Some(secret) = &config.token_secret {
DecodingKey::from_secret(secret.as_bytes())
} else {
warn!("SECURITY ERROR: No OAUTH_TOKEN_SECRET configured. Rejecting token.");
return Err("Server misconfiguration: OAUTH_TOKEN_SECRET is missing".to_string());
};
match decode::<Claims>(token, &decoding_key, &validation) {
Ok(token_data) => {
debug!("Token validated for subject: {}", token_data.claims.sub);
Ok(AuthenticatedPrincipal {
subject: token_data.claims.sub,
scopes: parse_scopes(token_data.claims.scope.as_deref()),
})
}
Err(e) => {
let err_msg = format!("JWT validation failed: {e}");
warn!("{}", err_msg);
Err(err_msg)
}
}
}
#[cfg(feature = "oauth")]
pub fn validate_bearer_token(token: &str, config: &OAuthConfig) -> AuthorizationResult {
match validate_access_token(token, config) {
Ok(_) => AuthorizationResult::Authorized,
Err(msg) => AuthorizationResult::InvalidToken(msg),
}
}
#[cfg(feature = "oauth")]
pub fn check_scopes(token_scope: Option<&str>, required_scopes: &[String]) -> AuthorizationResult {
let token_scopes = parse_scopes(token_scope);
if required_scopes.is_empty() {
return AuthorizationResult::Authorized;
}
if token_scopes.is_empty() {
return AuthorizationResult::InsufficientScope(required_scopes.to_vec());
}
let missing: Vec<String> = required_scopes
.iter()
.filter(|r| !token_scopes.contains(r))
.cloned()
.collect();
if missing.is_empty() {
AuthorizationResult::Authorized
} else {
AuthorizationResult::InsufficientScope(missing)
}
}
pub fn extract_bearer_token(headers: &str) -> Option<String> {
for segment in headers.split(['\n', ',']) {
let segment = segment.trim();
let value = match segment.split_once(':') {
Some((name, rest)) if name.trim().eq_ignore_ascii_case("authorization") => rest.trim(),
_ => segment,
};
if let Some((scheme, rest)) = value.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
let token = rest.trim();
if !token.is_empty() {
return Some(token.to_string());
}
}
}
}
None
}
pub fn extract_request_bearer_token(
params: Option<&Value>,
transport_token: Option<&str>,
) -> Option<String> {
if let Some(params) = params {
let meta = params.get("_meta");
let headers = meta.and_then(|m| m.get("headers"));
let candidates = [
meta.and_then(|m| m.get("authorization")),
headers.and_then(|h| h.get("authorization")),
headers.and_then(|h| h.get("Authorization")),
params.get("authorization"),
];
for candidate in candidates.into_iter().flatten() {
if let Value::String(raw) = candidate {
if let Some(token) = extract_bearer_token(raw) {
return Some(token);
}
}
}
if let Some(Value::String(raw)) = headers {
if let Some(token) = extract_bearer_token(raw) {
return Some(token);
}
}
}
let transport_token = transport_token?;
extract_bearer_token(transport_token).or_else(|| {
let raw = transport_token.trim();
(!raw.is_empty()).then(|| raw.to_string())
})
}
#[cfg(feature = "oauth")]
pub fn required_scope(operation: OperationType) -> &'static str {
match operation {
OperationType::Read => SCOPE_READ,
OperationType::Write => SCOPE_WRITE,
}
}
pub fn is_public_method(method: &str) -> bool {
matches!(
method,
"initialize" | ".well-known/oauth-protected-resource"
)
}
#[cfg(feature = "oauth")]
fn verify(token: &str, operation: OperationType, config: &OAuthConfig) -> RequestAuthorization {
let principal = match validate_access_token(token, config) {
Ok(principal) => principal,
Err(description) => {
return RequestAuthorization::Rejected {
code: UNAUTHORIZED_CODE,
error: "invalid_token".to_string(),
description,
};
}
};
let required = required_scope(operation);
if !principal.scopes.iter().any(|scope| scope == required) {
return RequestAuthorization::Rejected {
code: INSUFFICIENT_SCOPE_CODE,
error: "insufficient_scope".to_string(),
description: format!("token is missing the required scope '{required}'"),
};
}
RequestAuthorization::Authenticated(principal)
}
#[cfg(not(feature = "oauth"))]
fn verify(_token: &str, _operation: OperationType, config: &OAuthConfig) -> RequestAuthorization {
RequestAuthorization::Rejected {
code: UNAUTHORIZED_CODE,
error: "invalid_config".to_string(),
description: do_memory_mcp::protocol::validate_oauth_config(config)
.err()
.unwrap_or("token verification is unavailable")
.to_string(),
}
}
pub fn authorize_request(
params: Option<&Value>,
operation: OperationType,
config: &OAuthConfig,
transport_token: Option<&str>,
) -> RequestAuthorization {
if !config.enabled {
return RequestAuthorization::Unauthenticated;
}
if let Some(reason) = config.enforcement_error() {
return RequestAuthorization::Rejected {
code: UNAUTHORIZED_CODE,
error: "invalid_config".to_string(),
description: format!("authorization is enabled but cannot be enforced: {reason}"),
};
}
let Some(token) = extract_request_bearer_token(params, transport_token) else {
return RequestAuthorization::Rejected {
code: UNAUTHORIZED_CODE,
error: "unauthorized".to_string(),
description: "missing bearer token for an authenticated operation".to_string(),
};
};
verify(&token, operation, config)
}
#[cfg(feature = "oauth")]
pub fn create_www_authenticate_header(
error: &str,
error_description: Option<&str>,
realm: Option<&str>,
) -> String {
let mut parts = vec![format!("error=\"{error}\"")];
if let Some(desc) = error_description {
parts.push(format!("error_description=\"{desc}\""));
}
if let Some(r) = realm {
parts.push(format!("realm=\"{r}\""));
}
format!("Bearer {}", parts.join(", "))
}
#[cfg(all(test, feature = "oauth"))]
#[path = "oauth_tests.rs"]
mod tests;