use std::{
collections::HashMap,
time::{Duration, SystemTime},
};
use lexe_api_core::error::{BackendApiError, BackendErrorKind};
use lexe_common::api::auth::{
BearerAuthRequest, BearerAuthRequestWire, BearerAuthToken, LexeScope,
TokenWithExpiration,
};
use lexe_crypto::ed25519;
use crate::def::BearerAuthBackendApi;
pub const DEFAULT_USER_TOKEN_LIFETIME_SECS: u32 = 10 * 60; pub const LONG_LIVED_GATEWAY_PROXY_TOKEN_LIFETIME_SECS: u32 =
10 * 365 * 24 * 60 * 60; const EXPIRATION_BUFFER: Duration = Duration::from_secs(30);
#[allow(clippy::large_enum_variant)]
pub enum BearerAuthenticator {
Ephemeral {
user_key_pair: ed25519::KeyPair,
cache: tokio::sync::Mutex<HashMap<LexeScope, TokenWithExpiration>>,
},
Static {
token: BearerAuthToken,
scope: LexeScope,
},
}
impl BearerAuthenticator {
pub fn new(user_key_pair: ed25519::KeyPair) -> Self {
Self::Ephemeral {
user_key_pair,
cache: tokio::sync::Mutex::new(HashMap::new()),
}
}
pub fn new_static_token(token: BearerAuthToken, scope: LexeScope) -> Self {
Self::Static { token, scope }
}
pub fn user_key_pair(&self) -> Option<&ed25519::KeyPair> {
match self {
Self::Ephemeral { user_key_pair, .. } => Some(user_key_pair),
Self::Static { .. } => None,
}
}
pub async fn get_token<T: BearerAuthBackendApi + ?Sized>(
&self,
api: &T,
now: SystemTime,
scope: LexeScope,
) -> Result<BearerAuthToken, BackendApiError> {
self.get_token_with_exp(api, now, scope)
.await
.map(|token_with_exp| token_with_exp.token)
}
pub async fn get_token_with_exp<T: BearerAuthBackendApi + ?Sized>(
&self,
api: &T,
now: SystemTime,
scope: LexeScope,
) -> Result<TokenWithExpiration, BackendApiError> {
match self {
Self::Ephemeral {
user_key_pair,
cache,
} => {
let mut cache = cache.lock().await;
if let Some(token) = cache.get(&scope)
&& !helpers::token_needs_refresh(now, token.expiration)
{
return Ok(token.clone());
}
let token_with_exp = helpers::do_bearer_auth(
api,
now,
user_key_pair,
DEFAULT_USER_TOKEN_LIFETIME_SECS,
scope,
)
.await?;
cache.insert(scope, token_with_exp.clone());
Ok(token_with_exp)
}
Self::Static {
token,
scope: granted,
} => {
if !granted.has_permission_for(&scope) {
return Err(BackendApiError {
kind: BackendErrorKind::Unauthorized,
msg: format!(
"Static auth token's scope ({granted:?}) does not \
cover the requested scope ({scope:?})"
),
..Default::default()
});
}
Ok(TokenWithExpiration {
expiration: None,
token: token.clone(),
})
}
}
}
pub async fn mint_long_lived_gateway_proxy_token<T>(
&self,
api: &T,
now: SystemTime,
) -> Result<BearerAuthToken, BackendApiError>
where
T: BearerAuthBackendApi + ?Sized,
{
let user_key_pair =
self.user_key_pair().ok_or_else(|| BackendApiError {
kind: BackendErrorKind::Unauthorized,
msg: "Can't mint new tokens with a static auth token. \
Authenticate with root seed credentials instead."
.to_owned(),
..Default::default()
})?;
let token_with_exp = helpers::do_bearer_auth(
api,
now,
user_key_pair,
LONG_LIVED_GATEWAY_PROXY_TOKEN_LIFETIME_SECS,
LexeScope::GatewayProxy,
)
.await?;
Ok(token_with_exp.token)
}
}
pub mod helpers {
use super::*;
pub async fn do_bearer_auth<T: BearerAuthBackendApi + ?Sized>(
api: &T,
now: SystemTime,
user_key_pair: &ed25519::KeyPair,
lifetime_secs: u32,
scope: LexeScope,
) -> Result<TokenWithExpiration, BackendApiError> {
let expiration = now + Duration::from_secs(lifetime_secs as u64);
let auth_req = BearerAuthRequest::new(now, lifetime_secs, scope);
let auth_req_wire = BearerAuthRequestWire::from(auth_req);
let (_, signed_req) = user_key_pair
.sign_struct(&auth_req_wire)
.map_err(|err| BackendApiError {
kind: BackendErrorKind::Building,
msg: format!("Error signing auth request: {err:#}"),
..Default::default()
})?;
let resp = api.bearer_auth(&signed_req).await?;
Ok(TokenWithExpiration {
expiration: Some(expiration),
token: resp.bearer_auth_token,
})
}
#[inline]
pub fn token_needs_refresh(
now: SystemTime,
expiration: Option<SystemTime>,
) -> bool {
match expiration {
Some(expiration) => now + EXPIRATION_BUFFER >= expiration,
None => false,
}
}
}