Skip to main content

ppoppo_sdk_core/token_cache/
credentials.rs

1//! OAuth2 `client_credentials` grant source for [`TokenCache`].
2
3use std::time::Duration;
4
5use super::{TokenCacheError, TokenSource};
6
7/// Acquires tokens via the OAuth2 `client_credentials` grant (RFC 6749 §4.4).
8///
9/// POSTs `grant_type=client_credentials` + `client_id` + `client_secret`
10/// to `token_url` and extracts `access_token` + `expires_in` from the
11/// JSON response.
12pub struct ClientCredentialsSource {
13    http: reqwest::Client,
14    token_url: String,
15    client_id: String,
16    client_secret: String,
17}
18
19impl ClientCredentialsSource {
20    pub fn new(
21        token_url: impl Into<String>,
22        client_id: impl Into<String>,
23        client_secret: impl Into<String>,
24    ) -> Self {
25        Self {
26            http: {
27                crate::install_ring_provider();
28                reqwest::Client::new()
29            },
30            token_url: token_url.into(),
31            client_id: client_id.into(),
32            client_secret: client_secret.into(),
33        }
34    }
35}
36
37#[derive(serde::Deserialize)]
38struct TokenResponse {
39    access_token: String,
40    expires_in: u64,
41}
42
43#[async_trait::async_trait]
44impl TokenSource for ClientCredentialsSource {
45    async fn fetch_token(&self) -> Result<(String, Duration), TokenCacheError> {
46        let resp = self
47            .http
48            .post(&self.token_url)
49            .form(&[
50                ("grant_type", "client_credentials"),
51                ("client_id", self.client_id.as_str()),
52                ("client_secret", self.client_secret.as_str()),
53            ])
54            .send()
55            .await
56            .map_err(|e: reqwest::Error| TokenCacheError::Fetch(e.to_string()))?;
57
58        if !resp.status().is_success() {
59            let status = resp.status();
60            let body: String = resp.text().await.unwrap_or_default();
61            return Err(TokenCacheError::Fetch(format!("{status}: {body}")));
62        }
63
64        let body: TokenResponse = resp
65            .json::<TokenResponse>()
66            .await
67            .map_err(|e: reqwest::Error| TokenCacheError::Malformed(e.to_string()))?;
68
69        Ok((body.access_token, Duration::from_secs(body.expires_in)))
70    }
71}