use serde_json::Value;
use trust_tasks_rs::specs::auth::{
authenticate::v0_2 as authenticate_v0_2, authenticate::v0_3 as authenticate_v0_3,
challenge::v0_1 as challenge, refresh::v0_2 as refresh,
};
use trust_tasks_rs::{Payload, RejectReason, TrustTask};
use vti_common::auth::{AudienceBinding, AuthenticateInput, ChallengeInput, RefreshInput};
use super::helpers::{
TrustTaskOutcome, app_error_to_reject, extended_code, reject_with, reject_with_code,
success_response,
};
use super::{JoinAuthCtx, parse_spec_payload};
use crate::server::AppState;
pub(crate) const CHALLENGE_TYPE: &str = <challenge::Payload as Payload>::TYPE_URI;
pub(crate) const AUTHENTICATE_V0_2_TYPE: &str = <authenticate_v0_2::Payload as Payload>::TYPE_URI;
pub(crate) const AUTHENTICATE_V0_3_TYPE: &str = <authenticate_v0_3::Payload as Payload>::TYPE_URI;
pub(crate) const REFRESH_V0_2_TYPE: &str = <refresh::Payload as Payload>::TYPE_URI;
pub(crate) const URIS: &[&str] = &[
CHALLENGE_TYPE,
AUTHENTICATE_V0_2_TYPE,
AUTHENTICATE_V0_3_TYPE,
REFRESH_V0_2_TYPE,
];
pub(crate) const AUTHENTICATE_ERR_SESSION_KEY_UNSUPPORTED: &str =
authenticate_v0_2::error_codes::SESSION_KEY_UNSUPPORTED.code;
pub(crate) const AUTHENTICATE_V0_3_ERR_DELEGATION_NOT_RECOGNIZED: &str =
authenticate_v0_3::error_codes::DELEGATION_NOT_RECOGNIZED.code;
pub(super) async fn dispatch(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
type_uri: &str,
) -> Option<TrustTaskOutcome> {
Some(match type_uri {
CHALLENGE_TYPE => handle_challenge(state, doc).await,
AUTHENTICATE_V0_2_TYPE => handle_authenticate_v0_2(state, ctx, doc).await,
AUTHENTICATE_V0_3_TYPE => handle_authenticate_v0_3(state, ctx, doc).await,
REFRESH_V0_2_TYPE => handle_refresh_v0_2(state, ctx, doc).await,
_ => return None,
})
}
fn ed25519_session_key(session_key: &str) -> Option<&str> {
session_key
.strip_prefix("did:key:")
.filter(|mb| mb.starts_with("z6Mk"))
}
async fn handle_challenge(state: &AppState, doc: TrustTask<Value>) -> TrustTaskOutcome {
let payload: challenge::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let subject = payload
.subject
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default();
let backend = match crate::auth::VtcAuthBackend::from_state(state).await {
Ok(b) => b,
Err(e) => return app_error_to_reject(&doc, &e),
};
let resp = match vti_common::auth::handlers::handle_challenge(
&backend,
ChallengeInput {
did: subject,
session_pubkey_b58btc: None,
},
)
.await
{
Ok(r) => r,
Err(e) => return app_error_to_reject(&doc, &e),
};
success_response(&doc, resp)
}
struct SessionKeyOutcome {
session_pubkey_b58btc: Option<String>,
}
fn resolve_session_key<P>(
doc: &TrustTask<P>,
requested: Option<&str>,
unsupported_code: &'static str,
) -> Result<SessionKeyOutcome, TrustTaskOutcome> {
match requested {
None => Ok(SessionKeyOutcome {
session_pubkey_b58btc: None,
}),
Some(key) => match ed25519_session_key(key) {
Some(mb) => Ok(SessionKeyOutcome {
session_pubkey_b58btc: Some(mb.to_string()),
}),
None => {
tracing::warn!(session_key = %key, "authenticate refused: unsupported session key type");
Err(reject_with_code(
doc,
extended_code(unsupported_code),
"this service binds Ed25519 did:key session keys only",
Some(serde_json::json!({ "requested": key })),
))
}
},
}
}
async fn handle_authenticate_v0_2(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let Some(signer_did) = ctx.verified_signer.clone() else {
return reject_with(&doc, RejectReason::ProofRequired);
};
let payload: authenticate_v0_2::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let session_key = match resolve_session_key(
&doc,
payload.session_key.as_ref().map(|k| k.as_str()),
AUTHENTICATE_ERR_SESSION_KEY_UNSUPPORTED,
) {
Ok(k) => k,
Err(reject) => return reject,
};
let backend = match crate::auth::VtcAuthBackend::from_state(state).await {
Ok(b) => b,
Err(e) => return app_error_to_reject(&doc, &e),
};
let resp = match vti_common::auth::handlers::handle_authenticate(
&backend,
AuthenticateInput {
session_id: payload.session_id.to_string(),
challenge: payload.challenge.to_string(),
signer_did,
created_time: None,
session_pubkey_b58btc: session_key.session_pubkey_b58btc,
audience: AudienceBinding::Transport,
},
)
.await
{
Ok(r) => r,
Err(e) => return app_error_to_reject(&doc, &e),
};
success_response(&doc, resp)
}
async fn handle_authenticate_v0_3(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let Some(signer_did) = ctx.verified_signer.clone() else {
return reject_with(&doc, RejectReason::ProofRequired);
};
let payload: authenticate_v0_3::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
if let Some(principal) = payload.principal.as_ref()
&& principal.as_str() != signer_did
{
tracing::warn!(
principal = %principal.as_str(),
delegate = %signer_did,
"proxied authenticate refused: this build verifies no delegation evidence"
);
return reject_with_code(
&doc,
extended_code(AUTHENTICATE_V0_3_ERR_DELEGATION_NOT_RECOGNIZED),
"this service does not verify delegation evidence; authenticate with the \
principal's own key",
None,
);
}
let session_key = match resolve_session_key(
&doc,
payload.session_key.as_ref().map(|k| k.as_str()),
AUTHENTICATE_ERR_SESSION_KEY_UNSUPPORTED,
) {
Ok(k) => k,
Err(reject) => return reject,
};
let backend = match crate::auth::VtcAuthBackend::from_state(state).await {
Ok(b) => b,
Err(e) => return app_error_to_reject(&doc, &e),
};
let resp = match vti_common::auth::handlers::handle_authenticate(
&backend,
AuthenticateInput {
session_id: payload.session_id.to_string(),
challenge: payload.challenge.to_string(),
signer_did,
created_time: None,
session_pubkey_b58btc: session_key.session_pubkey_b58btc,
audience: AudienceBinding::Transport,
},
)
.await
{
Ok(r) => r,
Err(e) => return app_error_to_reject(&doc, &e),
};
success_response(&doc, resp)
}
async fn handle_refresh_v0_2(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let payload: refresh::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let backend = match crate::auth::VtcAuthBackend::from_state(state).await {
Ok(b) => b,
Err(e) => return app_error_to_reject(&doc, &e),
};
let resp = match vti_common::auth::handlers::handle_refresh(
&backend,
RefreshInput {
refresh_token: payload.refresh_token.to_string(),
signer_did: ctx.verified_signer.clone(),
},
)
.await
{
Ok(r) => r,
Err(e) => return app_error_to_reject(&doc, &e),
};
success_response(&doc, resp)
}