use std::fmt;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use bytes::Bytes;
use secrecy::{ExposeSecret as _, SecretString};
use tokio::sync::Mutex;
use tokio::time::Instant;
use tonic::transport::Channel;
use crate::device_transport::connect_device_channel;
use crate::proto::localdevice::permission_manager_client::PermissionManagerClient;
use crate::proto::localdevice::{RequestAuthTokenRequest, RequestAuthTokenResponse};
use crate::{AuthenticatedService, ClientCredentials, Error};
const TOKEN_REFRESH_MARGIN: Duration = Duration::from_secs(30);
pub struct AuthToken {
token: SecretString,
deadline: SystemTime,
refresh_at: Instant,
}
impl AuthToken {
#[must_use]
pub fn expose_secret(&self) -> &str {
self.token.expose_secret()
}
#[must_use]
pub fn deadline(&self) -> SystemTime {
self.deadline
}
fn should_refresh(&self, now: Instant) -> bool {
now >= self.refresh_at
}
fn from_response(response: RequestAuthTokenResponse) -> Result<Self, Error> {
let deadline = response.deadline.ok_or(Error::MissingTokenDeadline)?;
let deadline = SystemTime::try_from(deadline).map_err(|_| Error::InvalidTokenDeadline)?;
let now_system = SystemTime::now();
let now_instant = Instant::now();
let lifetime = deadline.duration_since(now_system).unwrap_or_default();
let refresh_at = now_instant + lifetime.saturating_sub(TOKEN_REFRESH_MARGIN);
Ok(Self {
token: response.token.into(),
deadline,
refresh_at,
})
}
}
impl fmt::Debug for AuthToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AuthToken")
.field("token", &"<redacted>")
.field("deadline", &self.deadline)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct TokenProvider {
inner: Arc<TokenProviderInner>,
}
struct TokenProviderInner {
channel: Channel,
credentials: ClientCredentials,
cached: Mutex<Option<Arc<AuthToken>>>,
}
impl TokenProvider {
pub async fn connect(api_url: &str, credentials: ClientCredentials) -> Result<Self, Error> {
let channel = connect_device_channel(api_url, &credentials).await?;
Ok(Self::new(channel, credentials))
}
#[must_use]
pub fn new(channel: Channel, credentials: ClientCredentials) -> Self {
Self {
inner: Arc::new(TokenProviderInner {
channel,
credentials,
cached: Mutex::new(None),
}),
}
}
pub async fn token(&self) -> Result<Arc<AuthToken>, Error> {
let mut cached = self.inner.cached.lock().await;
if let Some(token) = cached.as_ref() {
if !token.should_refresh(Instant::now()) {
return Ok(Arc::clone(token));
}
}
let token = Arc::new(
request_auth_token(self.inner.channel.clone(), &self.inner.credentials).await?,
);
*cached = Some(Arc::clone(&token));
Ok(token)
}
#[must_use]
pub fn authenticated_service(&self) -> AuthenticatedService<Channel> {
AuthenticatedService::new(self.inner.channel.clone(), self.clone())
}
}
impl fmt::Debug for TokenProvider {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TokenProvider")
.field("credentials", &self.inner.credentials)
.field("cached_token", &"<redacted>")
.finish_non_exhaustive()
}
}
pub async fn request_auth_token(
channel: Channel,
credentials: &ClientCredentials,
) -> Result<AuthToken, Error> {
let material = credentials.auth_request_material()?;
let request = RequestAuthTokenRequest {
box_cert: Bytes::copy_from_slice(material.box_certificate),
app_cert: Bytes::copy_from_slice(material.application_certificate),
signature: Bytes::from(material.signature),
};
let response = PermissionManagerClient::new(channel)
.request_auth_token(request)
.await?
.into_inner();
AuthToken::from_response(response)
}