use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
use tonic::transport::Channel;
use tonic::Status;
use crate::metadata::Metadata;
use crate::proto::udb::core::authn::services::v1 as authn;
use crate::proto::udb::core::authn::services::v1::authn_service_client::AuthnServiceClient;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Token {
pub access_token: String,
pub refresh_token: String,
pub session_id: String,
pub expires_at_unix: u64,
}
impl Token {
pub fn is_valid(&self, now_unix: u64, skew: Duration) -> bool {
if self.access_token.is_empty() {
return false;
}
if self.expires_at_unix == 0 {
return true;
}
now_unix.saturating_add(skew.as_secs()) < self.expires_at_unix
}
}
fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[derive(Clone)]
pub struct TokenManager {
inner: Arc<Inner>,
}
struct Inner {
client: Mutex<AuthnServiceClient<Channel>>,
token: Mutex<Token>,
skew: Duration,
meta: Metadata,
}
impl std::fmt::Debug for TokenManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenManager")
.field("tenant_id", &self.inner.meta.tenant_id)
.field("project_id", &self.inner.meta.project_id)
.field("skew", &self.inner.skew)
.field("token", &"<redacted>")
.finish()
}
}
impl TokenManager {
pub const DEFAULT_SKEW: Duration = Duration::from_secs(30);
pub fn new(channel: Channel, meta: Metadata) -> Self {
Self::with_skew(channel, meta, Self::DEFAULT_SKEW)
}
pub fn with_skew(channel: Channel, meta: Metadata, skew: Duration) -> Self {
Self {
inner: Arc::new(Inner {
client: Mutex::new(AuthnServiceClient::new(channel)),
token: Mutex::new(Token::default()),
skew,
meta,
}),
}
}
pub async fn peek(&self) -> Token {
self.inner.token.lock().await.clone()
}
pub async fn login(
&self,
username: impl Into<String>,
password: impl Into<String>,
) -> Result<Token, Status> {
let mut req = tonic::Request::new(authn::LoginRequest {
username: username.into(),
password: password.into(),
..Default::default()
});
self.inner.meta.apply(&mut req)?;
let resp = {
let mut client = self.inner.client.lock().await;
client.login(req).await?.into_inner()
};
if resp.mfa_required {
return Err(Status::unauthenticated(
"login requires a second factor; re-call Login with the MFA credential",
));
}
let token = Token {
access_token: resp.access_token,
refresh_token: resp.refresh_token,
session_id: resp.session_id,
expires_at_unix: expiry_from(resp.access_token_expires_in),
};
*self.inner.token.lock().await = token.clone();
Ok(token)
}
pub async fn access_token(&self) -> Result<String, Status> {
{
let token = self.inner.token.lock().await;
if token.is_valid(now_unix(), self.inner.skew) {
return Ok(token.access_token.clone());
}
}
self.refresh().await.map(|t| t.access_token)
}
pub async fn refresh(&self) -> Result<Token, Status> {
let mut stored = self.inner.token.lock().await;
if stored.is_valid(now_unix(), self.inner.skew) {
return Ok(stored.clone());
}
if stored.refresh_token.is_empty() && stored.session_id.is_empty() {
return Err(Status::unauthenticated(
"no refresh token or session id stored; call login() first",
));
}
let mut req = tonic::Request::new(authn::RefreshTokenRequest {
refresh_token: stored.refresh_token.clone(),
session_id: stored.session_id.clone(),
});
self.inner.meta.apply(&mut req)?;
let resp = {
let mut client = self.inner.client.lock().await;
client.refresh_token(req).await?.into_inner()
};
stored.access_token = resp.access_token;
stored.expires_at_unix = expiry_from(resp.access_token_expires_in);
if !resp.refresh_token.is_empty() {
stored.refresh_token = resp.refresh_token;
}
Ok(stored.clone())
}
pub async fn authenticated_metadata(&self) -> Result<Metadata, Status> {
let token = self.access_token().await?;
Ok(self.inner.meta.clone().with_bearer_token(token))
}
}
fn expiry_from(expires_in_secs: i32) -> u64 {
if expires_in_secs <= 0 {
return 0;
}
now_unix().saturating_add(expires_in_secs as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_access_token_is_never_valid() {
assert!(!Token::default().is_valid(1_000, Duration::from_secs(30)));
}
#[test]
fn unknown_expiry_is_treated_as_valid() {
let t = Token {
access_token: "a".into(),
expires_at_unix: 0,
..Default::default()
};
assert!(t.is_valid(u64::MAX, Duration::from_secs(30)));
}
#[test]
fn skew_expires_the_token_early() {
let t = Token {
access_token: "a".into(),
expires_at_unix: 1_000,
..Default::default()
};
assert!(t.is_valid(900, Duration::from_secs(30)), "930 < 1000");
assert!(
!t.is_valid(980, Duration::from_secs(30)),
"1010 >= 1000: inside the skew window, must refresh"
);
}
#[test]
fn expiry_from_ignores_non_positive() {
assert_eq!(expiry_from(0), 0);
assert_eq!(expiry_from(-5), 0);
assert!(expiry_from(60) >= now_unix());
}
}