use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OAuthError {
InvalidRequest,
UnauthorizedClient,
AccessDenied,
UnsupportedResponseType,
InvalidScope,
ServerError,
TemporarilyUnavailable,
InvalidGrant,
InvalidClient,
UnsupportedGrantType,
InvalidToken,
InsufficientScope,
}
impl std::fmt::Display for OAuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidRequest => write!(f, "invalid_request"),
Self::UnauthorizedClient => write!(f, "unauthorized_client"),
Self::AccessDenied => write!(f, "access_denied"),
Self::UnsupportedResponseType => write!(f, "unsupported_response_type"),
Self::InvalidScope => write!(f, "invalid_scope"),
Self::ServerError => write!(f, "server_error"),
Self::TemporarilyUnavailable => write!(f, "temporarily_unavailable"),
Self::InvalidGrant => write!(f, "invalid_grant"),
Self::InvalidClient => write!(f, "invalid_client"),
Self::UnsupportedGrantType => write!(f, "unsupported_grant_type"),
Self::InvalidToken => write!(f, "invalid_token"),
Self::InsufficientScope => write!(f, "insufficient_scope"),
}
}
}
impl std::error::Error for OAuthError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthErrorResponse {
pub error: OAuthError,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_uri: Option<String>,
}
impl OAuthErrorResponse {
#[must_use]
pub const fn new(error: OAuthError) -> Self {
Self {
error,
error_description: None,
error_uri: None,
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.error_description = Some(description.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtectedResourceMetadata {
pub resource: String,
pub authorization_servers: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bearer_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_documentation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_signing_alg_values_supported: Option<Vec<String>>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl ProtectedResourceMetadata {
#[must_use]
pub fn new(resource: impl Into<String>) -> Self {
Self {
resource: resource.into(),
authorization_servers: Vec::new(),
bearer_methods_supported: Some(vec!["header".to_string()]),
resource_documentation: None,
scopes_supported: None,
resource_signing_alg_values_supported: None,
extra: HashMap::new(),
}
}
#[must_use]
pub fn with_authorization_server(mut self, server: impl Into<String>) -> Self {
self.authorization_servers.push(server.into());
self
}
#[must_use]
pub fn with_scopes(mut self, scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.scopes_supported = Some(scopes.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn with_documentation(mut self, url: impl Into<String>) -> Self {
self.resource_documentation = Some(url.into());
self
}
pub fn validate(&self) -> Result<(), String> {
if self.resource.is_empty() {
return Err("Resource identifier is required".to_string());
}
if self.authorization_servers.is_empty() {
return Err(
"At least one authorization server is required per MCP specification".to_string(),
);
}
Ok(())
}
#[must_use]
pub fn well_known_url(resource_url: &str) -> Option<String> {
url::Url::parse(resource_url).ok().map(|url| {
format!(
"{}://{}/.well-known/oauth-protected-resource",
url.scheme(),
url.host_str().unwrap_or("localhost")
)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationServerMetadata {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_types_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grant_types_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_challenge_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub introspection_endpoint: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl AuthorizationServerMetadata {
#[must_use]
pub fn new(
issuer: impl Into<String>,
authorization_endpoint: impl Into<String>,
token_endpoint: impl Into<String>,
) -> Self {
Self {
issuer: issuer.into(),
authorization_endpoint: authorization_endpoint.into(),
token_endpoint: token_endpoint.into(),
jwks_uri: None,
registration_endpoint: None,
scopes_supported: None,
response_types_supported: Some(vec!["code".to_string()]),
grant_types_supported: Some(vec![
"authorization_code".to_string(),
"client_credentials".to_string(),
"refresh_token".to_string(),
]),
token_endpoint_auth_methods_supported: None,
code_challenge_methods_supported: Some(vec!["S256".to_string()]),
revocation_endpoint: None,
introspection_endpoint: None,
extra: HashMap::new(),
}
}
#[must_use]
pub fn from_issuer(issuer: impl Into<String>) -> Self {
let issuer = issuer.into();
Self::new(
&issuer,
format!("{issuer}/authorize"),
format!("{issuer}/token"),
)
}
#[must_use]
pub fn with_jwks_uri(mut self, uri: impl Into<String>) -> Self {
self.jwks_uri = Some(uri.into());
self
}
#[must_use]
pub fn with_registration_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.registration_endpoint = Some(endpoint.into());
self
}
#[must_use]
pub fn well_known_url(issuer: &str) -> Option<String> {
url::Url::parse(issuer).ok().map(|url| {
format!(
"{}://{}/.well-known/oauth-authorization-server",
url.scheme(),
url.host_str().unwrap_or("localhost")
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantType {
AuthorizationCode,
ClientCredentials,
RefreshToken,
}
impl std::fmt::Display for GrantType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AuthorizationCode => write!(f, "authorization_code"),
Self::ClientCredentials => write!(f, "client_credentials"),
Self::RefreshToken => write!(f, "refresh_token"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CodeChallengeMethod {
#[default]
S256,
Plain,
}
impl std::fmt::Display for CodeChallengeMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::S256 => write!(f, "S256"),
Self::Plain => write!(f, "plain"),
}
}
}
#[derive(Debug, Clone)]
pub struct PkceChallenge {
pub verifier: String,
pub challenge: String,
pub method: CodeChallengeMethod,
}
impl PkceChallenge {
#[must_use]
pub fn new() -> Self {
Self::with_method(CodeChallengeMethod::S256)
}
#[must_use]
pub fn with_method(method: CodeChallengeMethod) -> Self {
use base64::Engine;
use rand::Rng;
let mut rng = rand::thread_rng();
let verifier_bytes: [u8; 32] = rng.r#gen();
let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(verifier_bytes);
let challenge = match method {
CodeChallengeMethod::S256 => {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(verifier.as_bytes());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
}
CodeChallengeMethod::Plain => verifier.clone(),
};
Self {
verifier,
challenge,
method,
}
}
#[must_use]
pub fn verify(verifier: &str, challenge: &str, method: CodeChallengeMethod) -> bool {
use base64::Engine;
let computed = match method {
CodeChallengeMethod::S256 => {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(verifier.as_bytes());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
}
CodeChallengeMethod::Plain => verifier.to_string(),
};
constant_time_eq(computed.as_bytes(), challenge.as_bytes())
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
impl Default for PkceChallenge {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationRequest {
pub response_type: String,
pub client_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
pub code_challenge: String,
pub code_challenge_method: String,
pub resource: String,
}
impl AuthorizationRequest {
#[must_use]
pub fn new(
client_id: impl Into<String>,
pkce: &PkceChallenge,
resource: impl Into<String>,
) -> Self {
Self {
response_type: "code".to_string(),
client_id: client_id.into(),
redirect_uri: None,
scope: None,
state: None,
code_challenge: pkce.challenge.clone(),
code_challenge_method: pkce.method.to_string(),
resource: resource.into(),
}
}
#[must_use]
pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
self.redirect_uri = Some(uri.into());
self
}
#[must_use]
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
#[must_use]
pub fn with_state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
#[must_use]
pub fn build_url(&self, authorization_endpoint: &str) -> Option<String> {
let mut url = url::Url::parse(authorization_endpoint).ok()?;
{
let mut query = url.query_pairs_mut();
query.append_pair("response_type", &self.response_type);
query.append_pair("client_id", &self.client_id);
query.append_pair("code_challenge", &self.code_challenge);
query.append_pair("code_challenge_method", &self.code_challenge_method);
query.append_pair("resource", &self.resource);
if let Some(ref uri) = self.redirect_uri {
query.append_pair("redirect_uri", uri);
}
if let Some(ref scope) = self.scope {
query.append_pair("scope", scope);
}
if let Some(ref state) = self.state {
query.append_pair("state", state);
}
}
Some(url.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenRequest {
pub grant_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_uri: Option<String>,
pub client_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_verifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
}
impl TokenRequest {
#[must_use]
pub fn authorization_code(
code: impl Into<String>,
client_id: impl Into<String>,
code_verifier: impl Into<String>,
resource: impl Into<String>,
) -> Self {
Self {
grant_type: "authorization_code".to_string(),
code: Some(code.into()),
redirect_uri: None,
client_id: client_id.into(),
client_secret: None,
code_verifier: Some(code_verifier.into()),
resource: Some(resource.into()),
refresh_token: None,
scope: None,
}
}
#[must_use]
pub fn client_credentials(
client_id: impl Into<String>,
client_secret: impl Into<String>,
resource: impl Into<String>,
) -> Self {
Self {
grant_type: "client_credentials".to_string(),
code: None,
redirect_uri: None,
client_id: client_id.into(),
client_secret: Some(client_secret.into()),
code_verifier: None,
resource: Some(resource.into()),
refresh_token: None,
scope: None,
}
}
#[must_use]
pub fn refresh(refresh_token: impl Into<String>, client_id: impl Into<String>) -> Self {
Self {
grant_type: "refresh_token".to_string(),
code: None,
redirect_uri: None,
client_id: client_id.into(),
client_secret: None,
code_verifier: None,
resource: None,
refresh_token: Some(refresh_token.into()),
scope: None,
}
}
#[must_use]
pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
self.redirect_uri = Some(uri.into());
self
}
#[must_use]
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
pub token_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_in: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
}
impl TokenResponse {
#[must_use]
pub fn is_expired(&self, issued_at: SystemTime) -> bool {
if let Some(expires_in) = self.expires_in {
let expiry = issued_at + Duration::from_secs(expires_in);
SystemTime::now() >= expiry
} else {
false }
}
}
#[derive(Debug, Clone)]
pub struct StoredToken {
pub token: TokenResponse,
pub issued_at: SystemTime,
pub resource: String,
}
impl StoredToken {
#[must_use]
pub fn new(token: TokenResponse, resource: impl Into<String>) -> Self {
Self {
token,
issued_at: SystemTime::now(),
resource: resource.into(),
}
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.token.is_expired(self.issued_at)
}
#[must_use]
pub fn expires_within(&self, duration: Duration) -> bool {
if let Some(expires_in) = self.token.expires_in {
let expiry = self.issued_at + Duration::from_secs(expires_in);
SystemTime::now() + duration >= expiry
} else {
false
}
}
#[must_use]
pub fn authorization_header(&self) -> String {
format!("Bearer {}", self.token.access_token)
}
}
#[derive(Debug, Clone)]
pub struct WwwAuthenticate {
pub realm: Option<String>,
pub resource_metadata: String,
pub error: Option<OAuthError>,
pub error_description: Option<String>,
}
impl WwwAuthenticate {
#[must_use]
pub fn new(resource_metadata: impl Into<String>) -> Self {
Self {
realm: None,
resource_metadata: resource_metadata.into(),
error: None,
error_description: None,
}
}
#[must_use]
pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
self.realm = Some(realm.into());
self
}
#[must_use]
pub const fn with_error(mut self, error: OAuthError) -> Self {
self.error = Some(error);
self
}
#[must_use]
pub fn with_error_description(mut self, description: impl Into<String>) -> Self {
self.error_description = Some(description.into());
self
}
#[must_use]
pub fn to_header_value(&self) -> String {
let mut parts = vec![format!(
"Bearer resource_metadata=\"{}\"",
self.resource_metadata
)];
if let Some(ref realm) = self.realm {
parts.push(format!("realm=\"{realm}\""));
}
if let Some(ref error) = self.error {
parts.push(format!("error=\"{error}\""));
}
if let Some(ref desc) = self.error_description {
parts.push(format!("error_description=\"{desc}\""));
}
parts.join(", ")
}
#[must_use]
pub fn parse(header_value: &str) -> Option<Self> {
if !header_value.starts_with("Bearer ") {
return None;
}
let params = &header_value[7..]; let mut resource_metadata = None;
let mut realm = None;
let mut error = None;
let mut error_description = None;
for part in params.split(", ") {
if let Some((key, value)) = part.split_once('=') {
let value = value.trim_matches('"');
match key.trim() {
"resource_metadata" => resource_metadata = Some(value.to_string()),
"realm" => realm = Some(value.to_string()),
"error" => {
error = match value {
"invalid_request" => Some(OAuthError::InvalidRequest),
"invalid_token" => Some(OAuthError::InvalidToken),
"insufficient_scope" => Some(OAuthError::InsufficientScope),
_ => None,
};
}
"error_description" => error_description = Some(value.to_string()),
_ => {}
}
}
}
resource_metadata.map(|rm| Self {
realm,
resource_metadata: rm,
error,
error_description,
})
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationConfig {
pub authorization_server: String,
pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Option<String>,
pub resource: Option<String>,
pub scopes: Vec<String>,
}
impl AuthorizationConfig {
#[must_use]
pub fn new(authorization_server: impl Into<String>) -> Self {
Self {
authorization_server: authorization_server.into(),
client_id: String::new(),
client_secret: None,
redirect_uri: None,
resource: None,
scopes: Vec::new(),
}
}
#[must_use]
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = client_id.into();
self
}
#[must_use]
pub fn with_client_secret(mut self, secret: impl Into<String>) -> Self {
self.client_secret = Some(secret.into());
self
}
#[must_use]
pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
self.redirect_uri = Some(uri.into());
self
}
#[must_use]
pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
self.resource = Some(resource.into());
self
}
#[must_use]
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scopes.push(scope.into());
self
}
#[must_use]
pub const fn is_public_client(&self) -> bool {
self.client_secret.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientRegistrationRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_uris: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint_auth_method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grant_types: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_types: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub software_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub software_version: Option<String>,
}
impl ClientRegistrationRequest {
#[must_use]
pub fn new() -> Self {
Self {
redirect_uris: None,
token_endpoint_auth_method: Some("none".to_string()), grant_types: Some(vec!["authorization_code".to_string()]),
response_types: Some(vec!["code".to_string()]),
client_name: None,
client_uri: None,
software_id: None,
software_version: None,
}
}
#[must_use]
pub fn with_redirect_uris(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.redirect_uris = Some(uris.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
self
}
#[must_use]
pub fn with_software_id(mut self, id: impl Into<String>) -> Self {
self.software_id = Some(id.into());
self
}
}
impl Default for ClientRegistrationRequest {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientRegistrationResponse {
pub client_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret_expires_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id_issued_at: Option<u64>,
#[serde(flatten)]
pub metadata: HashMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_protected_resource_metadata() {
let metadata = ProtectedResourceMetadata::new("https://mcp.example.com")
.with_authorization_server("https://auth.example.com")
.with_scopes(["mcp:read", "mcp:write"]);
assert_eq!(metadata.resource, "https://mcp.example.com");
assert_eq!(metadata.authorization_servers.len(), 1);
assert!(metadata.validate().is_ok());
}
#[test]
fn test_protected_resource_metadata_validation() {
let metadata = ProtectedResourceMetadata::new("https://mcp.example.com");
assert!(metadata.validate().is_err());
let metadata = metadata.with_authorization_server("https://auth.example.com");
assert!(metadata.validate().is_ok());
}
#[test]
fn test_authorization_server_metadata() {
let metadata = AuthorizationServerMetadata::from_issuer("https://auth.example.com");
assert_eq!(metadata.issuer, "https://auth.example.com");
assert_eq!(
metadata.authorization_endpoint,
"https://auth.example.com/authorize"
);
assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
}
#[test]
fn test_pkce_challenge() {
let pkce = PkceChallenge::new();
assert_ne!(pkce.verifier, pkce.challenge);
assert!(PkceChallenge::verify(
&pkce.verifier,
&pkce.challenge,
CodeChallengeMethod::S256
));
assert!(!PkceChallenge::verify(
"wrong",
&pkce.challenge,
CodeChallengeMethod::S256
));
}
#[test]
fn test_constant_time_eq() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(constant_time_eq(b"", b""));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"abcd"));
assert!(!constant_time_eq(b"abc", b""));
}
#[test]
fn test_authorization_request() -> Result<(), Box<dyn std::error::Error>> {
let pkce = PkceChallenge::new();
let request = AuthorizationRequest::new("client123", &pkce, "https://mcp.example.com")
.with_redirect_uri("http://localhost:8080/callback")
.with_scope("mcp:read")
.with_state("random_state");
assert_eq!(request.client_id, "client123");
assert_eq!(request.resource, "https://mcp.example.com");
let url = request.build_url("https://auth.example.com/authorize");
assert!(url.is_some());
let url = url.ok_or("Expected URL")?;
assert!(url.contains("response_type=code"));
assert!(url.contains("client_id=client123"));
assert!(url.contains("resource="));
Ok(())
}
#[test]
fn test_token_request_authorization_code() {
let request = TokenRequest::authorization_code(
"auth_code_123",
"client123",
"verifier123",
"https://mcp.example.com",
);
assert_eq!(request.grant_type, "authorization_code");
assert_eq!(request.code, Some("auth_code_123".to_string()));
assert_eq!(request.code_verifier, Some("verifier123".to_string()));
}
#[test]
fn test_token_request_client_credentials() {
let request =
TokenRequest::client_credentials("client123", "secret456", "https://mcp.example.com");
assert_eq!(request.grant_type, "client_credentials");
assert_eq!(request.client_secret, Some("secret456".to_string()));
}
#[test]
fn test_www_authenticate_header() {
let header =
WwwAuthenticate::new("https://mcp.example.com/.well-known/oauth-protected-resource")
.with_realm("mcp")
.with_error(OAuthError::InvalidToken)
.with_error_description("Token expired");
let value = header.to_header_value();
assert!(value.starts_with("Bearer resource_metadata="));
assert!(value.contains("realm=\"mcp\""));
assert!(value.contains("error=\"invalid_token\""));
}
#[test]
fn test_www_authenticate_parse() -> Result<(), Box<dyn std::error::Error>> {
let header_value = "Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\", realm=\"mcp\"";
let parsed = WwwAuthenticate::parse(header_value);
assert!(parsed.is_some());
let parsed = parsed.ok_or("Expected parsed header")?;
assert_eq!(
parsed.resource_metadata,
"https://example.com/.well-known/oauth-protected-resource"
);
assert_eq!(parsed.realm, Some("mcp".to_string()));
Ok(())
}
#[test]
fn test_authorization_config() {
let config = AuthorizationConfig::new("https://auth.example.com")
.with_client_id("my-client")
.with_resource("https://mcp.example.com")
.with_scope("mcp:read");
assert!(config.is_public_client());
assert_eq!(config.client_id, "my-client");
let config = config.with_client_secret("secret");
assert!(!config.is_public_client());
}
#[test]
fn test_stored_token() {
let token = TokenResponse {
access_token: "access123".to_string(),
token_type: "Bearer".to_string(),
expires_in: Some(3600),
refresh_token: Some("refresh456".to_string()),
scope: Some("mcp:read".to_string()),
};
let stored = StoredToken::new(token, "https://mcp.example.com");
assert!(!stored.is_expired());
assert_eq!(stored.authorization_header(), "Bearer access123");
}
#[test]
fn test_client_registration_request() {
let request = ClientRegistrationRequest::new()
.with_client_name("My MCP Client")
.with_redirect_uris(["http://localhost:8080/callback"]);
assert_eq!(request.client_name, Some("My MCP Client".to_string()));
assert!(request.redirect_uris.is_some());
}
#[test]
fn test_oauth_error_display() {
assert_eq!(OAuthError::InvalidRequest.to_string(), "invalid_request");
assert_eq!(OAuthError::InvalidToken.to_string(), "invalid_token");
assert_eq!(
OAuthError::InsufficientScope.to_string(),
"insufficient_scope"
);
}
}