Skip to main content

rust_mcp_sdk/auth/client_auth/
token.rs

1use serde::{Deserialize, Serialize};
2
3/// Response from a successful token exchange (RFC 6749).
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct TokenResponse {
6    pub access_token: String,
7    pub token_type: String,
8    #[serde(default)]
9    pub expires_in: Option<u64>,
10    #[serde(default)]
11    pub refresh_token: Option<String>,
12    #[serde(default)]
13    pub scope: Option<String>,
14}
15
16impl TokenResponse {
17    fn now_secs() -> u64 {
18        std::time::SystemTime::now()
19            .duration_since(std::time::UNIX_EPOCH)
20            .unwrap_or_default()
21            .as_secs()
22    }
23
24    pub fn issued_at_secs(&self) -> u64 {
25        Self::now_secs()
26    }
27
28    pub fn expires_at_secs(&self) -> Option<u64> {
29        self.expires_in
30            .map(|secs| self.issued_at_secs().saturating_add(secs))
31    }
32
33    pub fn is_expired(&self) -> bool {
34        self.expires_at_secs()
35            .map(|exp| exp <= Self::now_secs().saturating_add(30))
36            .unwrap_or(false)
37    }
38}
39
40/// OAuth 2.0 grant types supported by the client.
41///
42/// Variants carry their own parameters so `McpAuthClient::exchange_token`
43/// takes only a `&GrantType`.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum GrantType {
46    /// Machine-to-machine authentication (RFC 6749 §4.4)
47    ClientCredentials,
48    /// User authorization code exchange (RFC 6749 §4.1)
49    AuthorizationCode { code: String, redirect_uri: String },
50    /// Authorization code with PKCE (RFC 7636)
51    AuthorizationCodePkce {
52        code: String,
53        redirect_uri: String,
54        /// The PKCE code verifier generated by `generate_pkce_params`
55        code_verifier: String,
56    },
57    /// Refresh an expired access token (RFC 6749 §6)
58    RefreshToken { refresh_token: String },
59}
60
61impl GrantType {
62    pub fn as_str(&self) -> &'static str {
63        match self {
64            GrantType::ClientCredentials => "client_credentials",
65            GrantType::AuthorizationCode { .. } => "authorization_code",
66            GrantType::AuthorizationCodePkce { .. } => "authorization_code",
67            GrantType::RefreshToken { .. } => "refresh_token",
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn token_response_deserialize_full() {
78        let json = r#"{
79            "access_token": "abc123",
80            "token_type": "bearer",
81            "expires_in": 3600,
82            "refresh_token": "ref456",
83            "scope": "mcp read"
84        }"#;
85        let token: TokenResponse = serde_json::from_str(json).unwrap();
86        assert_eq!(token.access_token, "abc123");
87        assert_eq!(token.token_type, "bearer");
88        assert_eq!(token.expires_in, Some(3600));
89        assert_eq!(token.refresh_token.as_deref(), Some("ref456"));
90        assert_eq!(token.scope.as_deref(), Some("mcp read"));
91        assert!(!token.is_expired());
92    }
93
94    #[test]
95    fn token_response_deserialize_minimal() {
96        let json = r#"{"access_token": "abc", "token_type": "bearer"}"#;
97        let token: TokenResponse = serde_json::from_str(json).unwrap();
98        assert_eq!(token.access_token, "abc");
99        assert_eq!(token.expires_in, None);
100        assert_eq!(token.refresh_token, None);
101    }
102
103    #[test]
104    fn token_with_very_short_expiry_is_expired() {
105        let json = r#"{"access_token": "x", "token_type": "bearer", "expires_in": 0}"#;
106        let token: TokenResponse = serde_json::from_str(json).unwrap();
107        assert!(token.is_expired());
108    }
109
110    #[test]
111    fn grant_type_as_str() {
112        assert_eq!(GrantType::ClientCredentials.as_str(), "client_credentials");
113        assert_eq!(
114            GrantType::AuthorizationCode {
115                code: "x".into(),
116                redirect_uri: "https://cb".into()
117            }
118            .as_str(),
119            "authorization_code"
120        );
121        assert_eq!(
122            GrantType::AuthorizationCodePkce {
123                code: "x".into(),
124                redirect_uri: "https://cb".into(),
125                code_verifier: "v".into(),
126            }
127            .as_str(),
128            "authorization_code"
129        );
130        assert_eq!(
131            GrantType::RefreshToken {
132                refresh_token: "rt".into()
133            }
134            .as_str(),
135            "refresh_token"
136        );
137    }
138}