1use std::sync::Arc;
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14use tokio::sync::Mutex;
15use tonic::transport::Channel;
16use tonic::Status;
17
18use crate::metadata::Metadata;
19use crate::proto::udb::core::authn::services::v1 as authn;
20use crate::proto::udb::core::authn::services::v1::authn_service_client::AuthnServiceClient;
21
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
25pub struct Token {
26 pub access_token: String,
27 pub refresh_token: String,
28 pub session_id: String,
29 pub expires_at_unix: u64,
30}
31
32impl Token {
33 pub fn is_valid(&self, now_unix: u64, skew: Duration) -> bool {
35 if self.access_token.is_empty() {
36 return false;
37 }
38 if self.expires_at_unix == 0 {
39 return true;
40 }
41 now_unix.saturating_add(skew.as_secs()) < self.expires_at_unix
42 }
43}
44
45fn now_unix() -> u64 {
46 SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .map(|d| d.as_secs())
49 .unwrap_or(0)
50}
51
52#[derive(Clone)]
59pub struct TokenManager {
60 inner: Arc<Inner>,
61}
62
63struct Inner {
64 client: Mutex<AuthnServiceClient<Channel>>,
65 token: Mutex<Token>,
66 skew: Duration,
67 meta: Metadata,
68}
69
70impl std::fmt::Debug for TokenManager {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("TokenManager")
76 .field("tenant_id", &self.inner.meta.tenant_id)
77 .field("project_id", &self.inner.meta.project_id)
78 .field("skew", &self.inner.skew)
79 .field("token", &"<redacted>")
80 .finish()
81 }
82}
83
84impl TokenManager {
85 pub const DEFAULT_SKEW: Duration = Duration::from_secs(30);
87
88 pub fn new(channel: Channel, meta: Metadata) -> Self {
89 Self::with_skew(channel, meta, Self::DEFAULT_SKEW)
90 }
91
92 pub fn with_skew(channel: Channel, meta: Metadata, skew: Duration) -> Self {
93 Self {
94 inner: Arc::new(Inner {
95 client: Mutex::new(AuthnServiceClient::new(channel)),
96 token: Mutex::new(Token::default()),
97 skew,
98 meta,
99 }),
100 }
101 }
102
103 pub async fn peek(&self) -> Token {
105 self.inner.token.lock().await.clone()
106 }
107
108 pub async fn login(
110 &self,
111 username: impl Into<String>,
112 password: impl Into<String>,
113 ) -> Result<Token, Status> {
114 let mut req = tonic::Request::new(authn::LoginRequest {
115 username: username.into(),
116 password: password.into(),
117 ..Default::default()
118 });
119 self.inner.meta.apply(&mut req)?;
120
121 let resp = {
122 let mut client = self.inner.client.lock().await;
123 client.login(req).await?.into_inner()
124 };
125
126 if resp.mfa_required {
127 return Err(Status::unauthenticated(
128 "login requires a second factor; re-call Login with the MFA credential",
129 ));
130 }
131
132 let token = Token {
133 access_token: resp.access_token,
134 refresh_token: resp.refresh_token,
135 session_id: resp.session_id,
136 expires_at_unix: expiry_from(resp.access_token_expires_in),
137 };
138 *self.inner.token.lock().await = token.clone();
139 Ok(token)
140 }
141
142 pub async fn access_token(&self) -> Result<String, Status> {
144 {
145 let token = self.inner.token.lock().await;
146 if token.is_valid(now_unix(), self.inner.skew) {
147 return Ok(token.access_token.clone());
148 }
149 }
150 self.refresh().await.map(|t| t.access_token)
151 }
152
153 pub async fn refresh(&self) -> Result<Token, Status> {
157 let mut stored = self.inner.token.lock().await;
158
159 if stored.is_valid(now_unix(), self.inner.skew) {
161 return Ok(stored.clone());
162 }
163 if stored.refresh_token.is_empty() && stored.session_id.is_empty() {
164 return Err(Status::unauthenticated(
165 "no refresh token or session id stored; call login() first",
166 ));
167 }
168
169 let mut req = tonic::Request::new(authn::RefreshTokenRequest {
170 refresh_token: stored.refresh_token.clone(),
171 session_id: stored.session_id.clone(),
172 });
173 self.inner.meta.apply(&mut req)?;
174
175 let resp = {
176 let mut client = self.inner.client.lock().await;
177 client.refresh_token(req).await?.into_inner()
178 };
179
180 stored.access_token = resp.access_token;
181 stored.expires_at_unix = expiry_from(resp.access_token_expires_in);
182 if !resp.refresh_token.is_empty() {
187 stored.refresh_token = resp.refresh_token;
188 }
189 Ok(stored.clone())
190 }
191
192 pub async fn authenticated_metadata(&self) -> Result<Metadata, Status> {
194 let token = self.access_token().await?;
195 Ok(self.inner.meta.clone().with_bearer_token(token))
196 }
197}
198
199fn expiry_from(expires_in_secs: i32) -> u64 {
200 if expires_in_secs <= 0 {
201 return 0;
202 }
203 now_unix().saturating_add(expires_in_secs as u64)
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn empty_access_token_is_never_valid() {
212 assert!(!Token::default().is_valid(1_000, Duration::from_secs(30)));
213 }
214
215 #[test]
216 fn unknown_expiry_is_treated_as_valid() {
217 let t = Token {
218 access_token: "a".into(),
219 expires_at_unix: 0,
220 ..Default::default()
221 };
222 assert!(t.is_valid(u64::MAX, Duration::from_secs(30)));
223 }
224
225 #[test]
226 fn skew_expires_the_token_early() {
227 let t = Token {
228 access_token: "a".into(),
229 expires_at_unix: 1_000,
230 ..Default::default()
231 };
232 assert!(t.is_valid(900, Duration::from_secs(30)), "930 < 1000");
233 assert!(
234 !t.is_valid(980, Duration::from_secs(30)),
235 "1010 >= 1000: inside the skew window, must refresh"
236 );
237 }
238
239 #[test]
240 fn expiry_from_ignores_non_positive() {
241 assert_eq!(expiry_from(0), 0);
242 assert_eq!(expiry_from(-5), 0);
243 assert!(expiry_from(60) >= now_unix());
244 }
245}