use uuid::Uuid;
use crate::auth::backend::{AuthAuditEvent, AuthBackend};
use crate::auth::session::now_epoch;
pub struct MintedTokens {
pub access_token: String,
pub refresh_token: String,
pub access_ttl: u64,
pub access_expires_at: u64,
pub refresh_expires_at: u64,
pub issued_at: u64,
}
#[allow(clippy::too_many_arguments)]
pub async fn mint_session_tokens<B: AuthBackend>(
backend: &B,
did: &str,
session_id: &str,
role: &B::Role,
contexts: &[String],
amr: &[String],
acr: &str,
tee_attested: bool,
) -> Result<MintedTokens, B::Error> {
let now = now_epoch();
let access_ttl = if acr == "aal2" {
backend.access_token_ttl_for_aal2()
} else {
backend.access_token_ttl()
};
let refresh_token = Uuid::new_v4().to_string();
let refresh_expires_at = now.saturating_add(backend.refresh_token_ttl());
let access_expires_at = now.saturating_add(access_ttl);
let access_token = backend
.mint_access_token(
did,
session_id,
role,
contexts,
amr,
acr,
tee_attested,
access_ttl,
)
.await?;
backend.audit(AuthAuditEvent::Authenticated {
did,
session_id,
amr,
acr,
});
Ok(MintedTokens {
access_token,
refresh_token,
access_ttl,
access_expires_at,
refresh_expires_at,
issued_at: now,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::backend::{RoleResolution, SessionStore};
use crate::auth::session::Session;
use crate::error::AppError;
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct DummyStore;
#[async_trait]
impl SessionStore for DummyStore {
type Error = AppError;
async fn store_session(&self, _: &Session) -> Result<(), AppError> {
unreachable!()
}
async fn get_session(&self, _: &str) -> Result<Option<Session>, AppError> {
unreachable!()
}
async fn delete_session(&self, _: &str) -> Result<(), AppError> {
unreachable!()
}
async fn store_refresh_index(&self, _: &str, _: &str) -> Result<(), AppError> {
unreachable!()
}
async fn take_session_id_by_refresh(&self, _: &str) -> Result<Option<String>, AppError> {
unreachable!()
}
async fn count_pending_challenges(&self, _: &str) -> Result<usize, AppError> {
unreachable!()
}
}
struct MockBackend {
store: DummyStore,
authenticated_fired: Arc<AtomicBool>,
}
#[async_trait]
impl AuthBackend for MockBackend {
type Store = DummyStore;
type Error = AppError;
type Role = String;
fn sessions(&self) -> &DummyStore {
&self.store
}
#[allow(clippy::too_many_arguments)]
async fn mint_access_token(
&self,
_subject: &str,
_session_id: &str,
_role: &String,
_contexts: &[String],
_amr: &[String],
_acr: &str,
_tee_attested: bool,
ttl_secs: u64,
) -> Result<String, AppError> {
Ok(format!("token-ttl-{ttl_secs}"))
}
async fn check_acl(&self, _did: &str) -> Result<RoleResolution<String>, AppError> {
unreachable!()
}
fn challenge_ttl(&self) -> u64 {
60
}
fn access_token_ttl(&self) -> u64 {
900
}
fn refresh_token_ttl(&self) -> u64 {
86_400
}
fn audit(&self, event: AuthAuditEvent<'_>) {
if matches!(event, AuthAuditEvent::Authenticated { .. }) {
self.authenticated_fired.store(true, Ordering::SeqCst);
}
}
}
fn backend() -> (MockBackend, Arc<AtomicBool>) {
let fired = Arc::new(AtomicBool::new(false));
(
MockBackend {
store: DummyStore,
authenticated_fired: fired.clone(),
},
fired,
)
}
#[tokio::test]
async fn aal2_mint_uses_short_ttl_and_fires_authenticated_audit() {
let (backend, fired) = backend();
let minted = mint_session_tokens(
&backend,
"did:key:zA",
"sess-1",
&"admin".to_string(),
&[],
&["passkey".to_string()],
"aal2",
false,
)
.await
.unwrap();
assert_eq!(minted.access_ttl, backend.access_token_ttl_for_aal2());
assert_eq!(minted.access_ttl, 300);
assert_eq!(minted.access_token, "token-ttl-300");
assert_eq!(minted.access_expires_at, minted.issued_at + 300);
assert_eq!(minted.refresh_expires_at, minted.issued_at + 86_400);
assert!(
fired.load(Ordering::SeqCst),
"Authenticated audit hook must fire"
);
}
#[tokio::test]
async fn aal1_mint_uses_full_ttl() {
let (backend, fired) = backend();
let minted = mint_session_tokens(
&backend,
"did:key:zA",
"sess-1",
&"member".to_string(),
&[],
&["did".to_string()],
"aal1",
false,
)
.await
.unwrap();
assert_eq!(minted.access_ttl, backend.access_token_ttl());
assert_eq!(minted.access_ttl, 900);
assert_eq!(minted.access_token, "token-ttl-900");
assert!(fired.load(Ordering::SeqCst));
}
}