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::{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 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 did = session.did.clone();
let minted = super::mint::mint_session_tokens(
backend,
&did,
&did,
&role_resolution.role,
&role_resolution.contexts,
&amr,
&acr,
session.tee_attested,
)
.await?;
let auth_session = Session {
session_id: did.clone(),
did: did.clone(),
challenge: String::new(),
state: SessionState::Authenticated,
created_at: now,
last_seen: now,
refresh_token: Some(minted.refresh_token.clone()),
refresh_expires_at: Some(minted.refresh_expires_at),
tee_attested: session.tee_attested,
amr: amr.clone(),
acr: acr.clone(),
acr_expires_at: None,
token_id: Some(minted.token_id.clone()),
session_pubkey_b58btc: input
.session_pubkey_b58btc
.or(session.session_pubkey_b58btc.clone()),
};
backend
.sessions()
.store_session(&auth_session)
.await
.map_err(|e| AuthError::Internal(format!("store_session failed: {e:?}")))?;
if input.session_id != did {
backend
.sessions()
.delete_session(&input.session_id)
.await
.map_err(|e| AuthError::Internal(format!("delete_session failed: {e:?}")))?;
}
backend
.sessions()
.store_refresh_index(&minted.refresh_token, &did)
.await
.map_err(|e| AuthError::Internal(format!("store_refresh_index failed: {e:?}")))?;
Ok(AuthenticateResponse {
session: WireSession {
id: did.clone(),
subject: 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(),
},
})
}