Skip to main content

lzc_sdk/
auth.rs

1use std::fmt;
2use std::sync::Arc;
3use std::time::{Duration, SystemTime};
4
5use bytes::Bytes;
6use secrecy::{ExposeSecret as _, SecretString};
7use tokio::sync::Mutex;
8use tokio::time::Instant;
9use tonic::transport::Channel;
10
11use crate::device_transport::connect_device_channel;
12use crate::proto::localdevice::permission_manager_client::PermissionManagerClient;
13use crate::proto::localdevice::{RequestAuthTokenRequest, RequestAuthTokenResponse};
14use crate::{AuthenticatedService, ClientCredentials, Error};
15
16const TOKEN_REFRESH_MARGIN: Duration = Duration::from_secs(30);
17
18/// Secret device API token and its server-provided deadline.
19pub struct AuthToken {
20    token: SecretString,
21    deadline: SystemTime,
22    refresh_at: Instant,
23}
24
25impl AuthToken {
26    /// Expose the token for explicit integration at an authentication boundary.
27    #[must_use]
28    pub fn expose_secret(&self) -> &str {
29        self.token.expose_secret()
30    }
31
32    /// Return the token expiry deadline supplied by the device.
33    #[must_use]
34    pub fn deadline(&self) -> SystemTime {
35        self.deadline
36    }
37
38    fn should_refresh(&self, now: Instant) -> bool {
39        now >= self.refresh_at
40    }
41
42    fn from_response(response: RequestAuthTokenResponse) -> Result<Self, Error> {
43        let deadline = response.deadline.ok_or(Error::MissingTokenDeadline)?;
44        let deadline = SystemTime::try_from(deadline).map_err(|_| Error::InvalidTokenDeadline)?;
45        let now_system = SystemTime::now();
46        let now_instant = Instant::now();
47        let lifetime = deadline.duration_since(now_system).unwrap_or_default();
48        let refresh_at = now_instant + lifetime.saturating_sub(TOKEN_REFRESH_MARGIN);
49        Ok(Self {
50            token: response.token.into(),
51            deadline,
52            refresh_at,
53        })
54    }
55}
56
57impl fmt::Debug for AuthToken {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter
60            .debug_struct("AuthToken")
61            .field("token", &"<redacted>")
62            .field("deadline", &self.deadline)
63            .finish_non_exhaustive()
64    }
65}
66
67/// Single-flight provider for cached `LazyCat` device API tokens.
68#[derive(Clone)]
69pub struct TokenProvider {
70    inner: Arc<TokenProviderInner>,
71}
72
73struct TokenProviderInner {
74    channel: Channel,
75    credentials: ClientCredentials,
76    cached: Mutex<Option<Arc<AuthToken>>>,
77}
78
79impl TokenProvider {
80    /// Connect an mTLS device channel and create its token provider.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the URL is invalid or the device channel cannot be
85    /// established.
86    pub async fn connect(api_url: &str, credentials: ClientCredentials) -> Result<Self, Error> {
87        let channel = connect_device_channel(api_url, &credentials).await?;
88        Ok(Self::new(channel, credentials))
89    }
90
91    /// Create a provider around an already connected raw device channel.
92    #[must_use]
93    pub fn new(channel: Channel, credentials: ClientCredentials) -> Self {
94        Self {
95            inner: Arc::new(TokenProviderInner {
96                channel,
97                credentials,
98                cached: Mutex::new(None),
99            }),
100        }
101    }
102
103    /// Return a cached token or refresh it once for all concurrent callers.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error when request signing or the `PermissionManager` RPC fails.
108    pub async fn token(&self) -> Result<Arc<AuthToken>, Error> {
109        let mut cached = self.inner.cached.lock().await;
110        if let Some(token) = cached.as_ref() {
111            if !token.should_refresh(Instant::now()) {
112                return Ok(Arc::clone(token));
113            }
114        }
115
116        let token = Arc::new(
117            request_auth_token(self.inner.channel.clone(), &self.inner.credentials).await?,
118        );
119        *cached = Some(Arc::clone(&token));
120        Ok(token)
121    }
122
123    /// Wrap this provider's channel with automatic auth-token metadata.
124    #[must_use]
125    pub fn authenticated_service(&self) -> AuthenticatedService<Channel> {
126        AuthenticatedService::new(self.inner.channel.clone(), self.clone())
127    }
128}
129
130impl fmt::Debug for TokenProvider {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        formatter
133            .debug_struct("TokenProvider")
134            .field("credentials", &self.inner.credentials)
135            .field("cached_token", &"<redacted>")
136            .finish_non_exhaustive()
137    }
138}
139
140/// Request a fresh auth token over an unauthenticated mTLS channel.
141///
142/// # Errors
143///
144/// Returns an error when the identity cannot be signed, the RPC returns a
145/// status, or the response deadline is missing or invalid.
146pub async fn request_auth_token(
147    channel: Channel,
148    credentials: &ClientCredentials,
149) -> Result<AuthToken, Error> {
150    let material = credentials.auth_request_material()?;
151    let request = RequestAuthTokenRequest {
152        box_cert: Bytes::copy_from_slice(material.box_certificate),
153        app_cert: Bytes::copy_from_slice(material.application_certificate),
154        signature: Bytes::from(material.signature),
155    };
156    let response = PermissionManagerClient::new(channel)
157        .request_auth_token(request)
158        .await?
159        .into_inner();
160    AuthToken::from_response(response)
161}