use crate::credentials::CredentialBundle;
use crate::didcomm_light;
use crate::protocols::auth::{AuthenticateResponse, ChallengeRequest, ChallengeResponse};
use reqwest::Client;
#[derive(Debug, Clone)]
pub struct AuthResult {
pub access_token: String,
pub access_expires_at: u64,
pub refresh_token: Option<String>,
pub refresh_expires_at: Option<u64>,
}
pub async fn challenge_response_light(
http: &Client,
base_url: &str,
client_did: &str,
private_key_multibase: &str,
vta_did: &str,
) -> Result<AuthResult, crate::error::VtaError> {
let _ = private_key_multibase;
let challenge_url = format!("{base_url}/auth/challenge");
let challenge_resp = http
.post(&challenge_url)
.json(&ChallengeRequest {
did: client_did.to_string(),
})
.send()
.await
.map_err(|e| format!("could not connect to VTA at {challenge_url}: {e}"))?;
if !challenge_resp.status().is_success() {
let status = challenge_resp.status();
let body = challenge_resp.text().await.unwrap_or_default();
return Err(format!("challenge request failed ({status}): {body}").into());
}
let challenge: ChallengeResponse = challenge_resp
.json()
.await
.map_err(|e| format!("unexpected response from VTA: {e}"))?;
let packed = didcomm_light::pack_auth_message(
"https://affinidi.com/atm/1.0/authenticate",
serde_json::json!({
"challenge": challenge.data.challenge,
"session_id": challenge.session_id,
}),
client_did,
vta_did,
)?;
let auth_url = format!("{base_url}/auth/");
let auth_resp = http
.post(&auth_url)
.header("content-type", "text/plain")
.body(packed)
.send()
.await
.map_err(|e| format!("could not connect to VTA at {auth_url}: {e}"))?;
if !auth_resp.status().is_success() {
let status = auth_resp.status();
let body = auth_resp.text().await.unwrap_or_default();
return Err(format!("authentication failed ({status}): {body}").into());
}
let auth_data: AuthenticateResponse = auth_resp
.json()
.await
.map_err(|e| format!("unexpected auth response: {e}"))?;
Ok(AuthResult {
access_token: auth_data.data.access_token,
access_expires_at: auth_data.data.access_expires_at,
refresh_token: auth_data.data.refresh_token,
refresh_expires_at: auth_data.data.refresh_expires_at,
})
}
pub async fn refresh_token_light(
http: &Client,
base_url: &str,
client_did: &str,
vta_did: &str,
refresh_token: &str,
) -> Result<AuthResult, crate::error::VtaError> {
let packed = didcomm_light::pack_auth_message(
"https://affinidi.com/atm/1.0/authenticate/refresh",
serde_json::json!({
"refresh_token": refresh_token,
}),
client_did,
vta_did,
)?;
let refresh_url = format!("{base_url}/auth/refresh");
let resp = http
.post(&refresh_url)
.header("content-type", "text/plain")
.body(packed)
.send()
.await
.map_err(|e| format!("refresh request failed: {e}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("token refresh failed ({status}): {body}").into());
}
let auth_data: AuthenticateResponse = resp
.json()
.await
.map_err(|e| format!("unexpected refresh response: {e}"))?;
Ok(AuthResult {
access_token: auth_data.data.access_token,
access_expires_at: auth_data.data.access_expires_at,
refresh_token: auth_data.data.refresh_token,
refresh_expires_at: auth_data.data.refresh_expires_at,
})
}
pub async fn authenticate_with_credential(
credential_b64: &str,
url_override: Option<&str>,
) -> Result<(AuthResult, CredentialBundle, Client), crate::error::VtaError> {
let cred = CredentialBundle::decode(credential_b64)?;
let url = url_override
.or(cred.vta_url.as_deref())
.ok_or("no VTA URL in credential and no override provided")?;
let http = Client::new();
let result = challenge_response_light(
&http,
url,
&cred.did,
&cred.private_key_multibase,
&cred.vta_did,
)
.await?;
Ok((result, cred, http))
}