1use crate::error::{ConcurError, Result};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::sync::Mutex;
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct Token {
10 pub access_token: String,
11 pub token_type: String,
12 pub expires_in: u64,
13 pub refresh_token: Option<String>,
14 pub scope: Option<String>,
15 pub geolocation: Option<String>,
16 pub id_token: Option<String>,
17 #[serde(skip)]
18 pub obtained_at: Option<Instant>,
19}
20
21impl Token {
22 pub fn is_expired(&self, buffer: Duration) -> bool {
24 match self.obtained_at {
25 Some(t) => {
26 let elapsed = t.elapsed();
27 let lifetime = Duration::from_secs(self.expires_in);
28 elapsed + buffer >= lifetime
29 }
30 None => true,
31 }
32 }
33}
34
35#[async_trait]
37pub trait TokenManager {
38 async fn access_token(&self) -> Result<String>;
40}
41
42pub struct StaticToken {
44 token: String,
45}
46
47impl StaticToken {
48 pub fn new(token: impl Into<String>) -> Self {
49 Self {
50 token: token.into(),
51 }
52 }
53}
54
55#[async_trait]
56impl TokenManager for StaticToken {
57 async fn access_token(&self) -> Result<String> {
58 Ok(self.token.clone())
59 }
60}
61
62pub struct OAuth2TokenManager {
64 http: reqwest::Client,
65 token_url: String,
66 client_id: String,
67 client_secret: String,
68 grant: Grant,
69 state: Mutex<TokenState>,
70}
71
72struct TokenState {
73 token: Option<Token>,
74}
75
76#[derive(Debug, Clone)]
77pub enum Grant {
78 ClientCredentials { scope: Option<String> },
80 Password {
82 username: String,
83 password: String,
84 credtype: Option<String>,
85 scope: Option<String>,
86 },
87 RefreshToken {
89 refresh_token: String,
90 scope: Option<String>,
91 },
92}
93
94impl OAuth2TokenManager {
95 pub fn new(
96 token_url: impl Into<String>,
97 client_id: impl Into<String>,
98 client_secret: impl Into<String>,
99 grant: Grant,
100 ) -> Self {
101 Self {
102 http: reqwest::Client::new(),
103 token_url: token_url.into(),
104 client_id: client_id.into(),
105 client_secret: client_secret.into(),
106 grant,
107 state: Mutex::new(TokenState { token: None }),
108 }
109 }
110
111 async fn fetch_token(&self) -> Result<Token> {
112 let mut params = vec![
113 ("client_id", self.client_id.clone()),
114 ("client_secret", self.client_secret.clone()),
115 ];
116
117 match &self.grant {
118 Grant::ClientCredentials { scope } => {
119 params.push(("grant_type", "client_credentials".to_string()));
120 if let Some(s) = scope {
121 params.push(("scope", s.clone()));
122 }
123 }
124 Grant::Password {
125 username,
126 password,
127 credtype,
128 scope,
129 } => {
130 params.push(("grant_type", "password".to_string()));
131 params.push(("username", username.clone()));
132 params.push(("password", password.clone()));
133 if let Some(c) = credtype {
134 params.push(("credtype", c.clone()));
135 }
136 if let Some(s) = scope {
137 params.push(("scope", s.clone()));
138 }
139 }
140 Grant::RefreshToken {
141 refresh_token,
142 scope,
143 } => {
144 params.push(("grant_type", "refresh_token".to_string()));
145 params.push(("refresh_token", refresh_token.clone()));
146 if let Some(s) = scope {
147 params.push(("scope", s.clone()));
148 }
149 }
150 }
151
152 let response = self
153 .http
154 .post(&self.token_url)
155 .header("Content-Type", "application/x-www-form-urlencoded")
156 .form(¶ms)
157 .send()
158 .await?;
159
160 if !response.status().is_success() {
161 let text = response.text().await.unwrap_or_default();
162 return Err(ConcurError::Auth(format!("Token request failed: {}", text)));
163 }
164
165 let mut token: Token = response.json().await?;
166 token.obtained_at = Some(Instant::now());
167 Ok(token)
168 }
169}
170
171#[async_trait]
172impl TokenManager for OAuth2TokenManager {
173 async fn access_token(&self) -> Result<String> {
174 {
175 let state = self
176 .state
177 .lock()
178 .map_err(|e| ConcurError::Other(e.to_string()))?;
179 if let Some(token) = &state.token
180 && !token.is_expired(Duration::from_secs(60))
181 {
182 return Ok(token.access_token.clone());
183 }
184 }
185
186 let token = self.fetch_token().await?;
187 let access = token.access_token.clone();
188
189 let mut state = self
190 .state
191 .lock()
192 .map_err(|e| ConcurError::Other(e.to_string()))?;
193 state.token = Some(token);
194 Ok(access)
195 }
196}
197
198pub async fn revoke_tokens(
200 http: &reqwest::Client,
201 base_url: &str,
202 access_token: &str,
203) -> Result<()> {
204 let url = format!("{}/app-mgmt/v0/connections", base_url.trim_end_matches('/'));
205 let response = http
206 .delete(&url)
207 .header("Authorization", format!("Bearer {}", access_token))
208 .send()
209 .await?;
210
211 if response.status().is_success() {
212 Ok(())
213 } else {
214 let text = response.text().await.unwrap_or_default();
215 Err(ConcurError::Auth(format!("Revoke failed: {}", text)))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::time::Duration;
223
224 #[test]
225 fn token_expiry_with_buffer() {
226 let mut token = Token {
227 access_token: "abc".to_string(),
228 token_type: "Bearer".to_string(),
229 expires_in: 300,
230 refresh_token: None,
231 scope: None,
232 geolocation: None,
233 id_token: None,
234 obtained_at: Some(Instant::now() - Duration::from_secs(200)),
235 };
236
237 assert!(!token.is_expired(Duration::from_secs(60)));
238
239 token.obtained_at = Some(Instant::now() - Duration::from_secs(245));
240 assert!(token.is_expired(Duration::from_secs(60)));
241 }
242
243 #[test]
244 fn token_expired_when_no_obtained_at() {
245 let token = Token {
246 access_token: "abc".to_string(),
247 token_type: "Bearer".to_string(),
248 expires_in: 300,
249 refresh_token: None,
250 scope: None,
251 geolocation: None,
252 id_token: None,
253 obtained_at: None,
254 };
255
256 assert!(token.is_expired(Duration::from_secs(60)));
257 }
258
259 #[tokio::test]
260 async fn static_token_returns_value() {
261 let manager = StaticToken::new("test-token");
262 assert_eq!(manager.access_token().await.unwrap(), "test-token");
263 }
264}