use vta_sdk::protocols::auth::{
AuthenticateResponse, Session as WireSession, TokenBundle, epoch_to_rfc3339,
};
use crate::auth::AuthError;
use crate::auth::backend::{AuthBackend, AuthenticateInput, SessionStore};
use crate::auth::session::{SessionState, now_epoch};
const DEFAULT_AMR: &[&str] = &["did"];
const DEFAULT_ACR: &str = "aal1";
pub async fn handle_authenticate<B: AuthBackend>(
backend: &B,
input: AuthenticateInput,
) -> Result<AuthenticateResponse, B::Error> {
let amr = DEFAULT_AMR.iter().map(|s| s.to_string()).collect();
handle_authenticate_with_aal(backend, input, amr, DEFAULT_ACR.into()).await
}
pub async fn handle_authenticate_with_aal<B: AuthBackend>(
backend: &B,
input: AuthenticateInput,
amr: Vec<String>,
acr: String,
) -> Result<AuthenticateResponse, B::Error> {
let mut session = backend
.sessions()
.get_session(&input.session_id)
.await
.map_err(|e| AuthError::Internal(format!("get_session failed: {e:?}")))?
.ok_or(AuthError::SessionNotFound)?;
if session.state != SessionState::ChallengeSent {
tracing::warn!(
session_id = %input.session_id,
did = %session.did,
"authenticate rejected: session not in ChallengeSent state (replay)",
);
return Err(AuthError::SessionStateMismatch.into());
}
if !super::constant_time_challenge_eq(&session.challenge, &input.challenge) {
tracing::warn!(
session_id = %input.session_id,
did = %session.did,
"authenticate rejected: challenge mismatch",
);
return Err(AuthError::ChallengeMismatch.into());
}
if session.did != input.signer_did {
tracing::warn!(
session_id = %input.session_id,
session_did = %session.did,
signer = %input.signer_did,
"authenticate rejected: signer DID mismatch",
);
return Err(AuthError::SignerMismatch.into());
}
let now = now_epoch();
if now.saturating_sub(session.created_at) > backend.challenge_ttl() {
tracing::warn!(
session_id = %input.session_id,
did = %session.did,
"authenticate rejected: challenge expired",
);
return Err(AuthError::ChallengeExpired.into());
}
super::check_freshness(
input.created_time,
session.created_at,
now,
backend.didcomm_freshness_window(),
)?;
let role_resolution = backend.check_acl(&session.did).await?;
let minted = super::mint::mint_session_tokens(
backend,
&session.did,
&session.session_id,
&role_resolution.role,
&role_resolution.contexts,
&amr,
&acr,
session.tee_attested,
)
.await?;
session.state = SessionState::Authenticated;
session.refresh_token = Some(minted.refresh_token.clone());
session.refresh_expires_at = Some(minted.refresh_expires_at);
session.amr = amr.clone();
session.acr = acr.clone();
if let Some(pk) = input.session_pubkey_b58btc {
session.session_pubkey_b58btc = Some(pk);
}
backend
.sessions()
.store_session(&session)
.await
.map_err(|e| AuthError::Internal(format!("store_session failed: {e:?}")))?;
backend
.sessions()
.store_refresh_index(&minted.refresh_token, &session.session_id)
.await
.map_err(|e| AuthError::Internal(format!("store_refresh_index failed: {e:?}")))?;
Ok(AuthenticateResponse {
session: WireSession {
id: session.session_id.clone(),
subject: session.did,
issued_at: epoch_to_rfc3339(minted.issued_at),
expires_at: epoch_to_rfc3339(minted.access_expires_at),
amr,
acr,
},
tokens: TokenBundle {
access_token: minted.access_token,
refresh_token: Some(minted.refresh_token),
token_type: "Bearer".to_string(),
expires_in: minted.access_ttl,
refresh_expires_in: Some(backend.refresh_token_ttl()),
scope: role_resolution
.contexts
.into_iter()
.map(|c| format!("ctx:{c}"))
.collect(),
},
})
}