use std::sync::Arc;
use bon::Builder;
use serde::Deserialize;
use snafu::prelude::*;
use crate::core::{
crypto::verifier::{CreateVerifierError, JwsVerifierPlatform},
jwt::{
JtiUniquenessChecker, JwsParseError, parse_compact_jws,
validator::{ClaimCheck, JwtValidationError, JwtValidator},
},
platform::{Duration, SystemTime},
};
pub struct ValidatedDpopProof {
pub htm: Option<String>,
pub htu: Option<String>,
pub ath: Option<String>,
pub nonce: Option<String>,
pub jti: Option<String>,
pub thumbprint: Option<String>,
pub alg: String,
pub iat: Option<u64>,
pub exp: Option<u64>,
}
#[derive(Debug, Builder)]
pub struct DpopProofValidator {
jws_verifier_platform: Arc<dyn JwsVerifierPlatform>,
#[builder(default = Duration::from_secs(60))]
max_proof_age: Duration,
allowed_signing_algorithms: Option<Vec<String>>,
jti_checker: Option<Arc<dyn JtiUniquenessChecker>>,
}
impl DpopProofValidator {
#[must_use]
pub fn allowed_signing_algorithms(&self) -> Option<&[String]> {
self.allowed_signing_algorithms.as_deref()
}
pub async fn validate(&self, proof: &str) -> Result<ValidatedDpopProof, DpopProofError> {
let parsed = parse_compact_jws::<(), DpopProofClaims>(proof).context(BadFormatSnafu)?;
let jwk = parsed
.header
.jwk
.clone()
.ok_or_else(|| MissingJwkHeaderSnafu.build())?;
ensure!(jwk.x5u.is_none(), JwkX5uSnafu);
let thumbprint = jwk.thumbprint();
let alg = parsed.header.alg.clone().into_owned();
let verifier = self
.jws_verifier_platform
.create_verifier_from_jwk(jwk)
.await
.context(CreateVerifierSnafu)?;
let validator = JwtValidator::builder()
.verifier(verifier)
.typ(ClaimCheck::required_value("dpop+jwt"))
.maybe_allowed_algorithms(self.allowed_signing_algorithms.clone())
.max_token_age(self.max_proof_age)
.require_jti(self.jti_checker.is_some())
.maybe_jti_checker(self.jti_checker.clone())
.build();
let validated = validator
.validate_parsed_jws(parsed)
.await
.context(InvalidProofSnafu)?;
Ok(ValidatedDpopProof {
htm: validated.claims.htm,
htu: validated.claims.htu,
ath: validated.claims.ath,
nonce: validated.claims.nonce,
jti: validated.jti,
thumbprint: Some(thumbprint),
alg,
iat: validated.issued_at.and_then(|t| {
t.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}),
exp: validated.expiration.and_then(|t| {
t.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}),
})
}
}
#[derive(Debug, Clone, Deserialize)]
struct DpopProofClaims {
htm: Option<String>,
htu: Option<String>,
ath: Option<String>,
nonce: Option<String>,
}
impl DpopProofError {
#[must_use]
pub fn error_description(&self) -> Option<String> {
match self {
Self::BadFormat { .. } => Some("The DPoP proof is malformed".to_string()),
Self::MissingJwkHeader => Some("The DPoP proof is missing the JWK header".to_string()),
Self::JwkX5u => {
Some("The DPoP proof JWK contains an unsupported x5u parameter".to_string())
}
Self::CreateVerifier { .. } => Some("The DPoP proof signature is invalid".to_string()),
Self::InvalidProof { source } => {
use JwtValidationError as E;
match source {
E::Parse { .. } => Some("The DPoP proof is malformed".to_string()),
E::Signature { .. } => Some("The DPoP proof signature is invalid".to_string()),
E::UnsignedToken => Some("The DPoP proof is unsigned".to_string()),
E::DisallowedAlgorithm { .. } => {
Some("The DPoP proof uses an unsupported signature algorithm".to_string())
}
E::UnrecognizedCriticalHeader { .. } => Some(
"The DPoP proof contains unrecognized critical header parameters"
.to_string(),
),
E::Expired { .. } => Some("The DPoP proof has expired".to_string()),
E::NotYetValid { .. } => Some("The DPoP proof is not yet valid".to_string()),
E::IssuedInFuture { .. } => {
Some("The DPoP proof was issued in the future".to_string())
}
E::TokenTooOld { .. } => Some("The DPoP proof is too old".to_string()),
E::InvalidTokenType { .. } => {
Some("The DPoP proof has an invalid typ header".to_string())
}
E::ClaimMismatch { claim, .. } => {
Some(format!("The DPoP proof '{claim}' claim is invalid"))
}
E::RequiredClaimMissing { claim } => Some(format!(
"The DPoP proof is missing the required '{claim}' claim"
)),
E::JtiNotUnique => Some("The DPoP proof jti has already been used".to_string()),
E::JtiTooLong { .. } => Some("The DPoP proof jti is too long".to_string()),
E::JtiCheck { .. } | E::ExtraClaims { .. } => None,
}
}
}
}
}
#[derive(Debug, Snafu)]
pub enum DpopProofError {
#[snafu(display("Bad DPoP proof format"))]
BadFormat {
source: JwsParseError,
},
#[snafu(display("DPoP proof is missing the JWK header"))]
MissingJwkHeader,
#[snafu(display("DPoP proof JWK contains unsupported x5u parameter"))]
JwkX5u,
#[snafu(display("Failed to create DPoP verification key"))]
CreateVerifier {
source: CreateVerifierError,
},
#[snafu(display("Invalid DPoP proof"))]
InvalidProof {
source: JwtValidationError,
},
}