pub mod error;
use crate::core::jwk::JwksSource;
use crate::core::jwt::BoxedJtiUniquenessChecker;
use crate::core::server_metadata::AuthorizationServerMetadata;
use crate::validator::dpop_nonce::NoNonceCheck;
use crate::validator::introspection::introspection_validator_builder::SetDpopNonceChecker;
pub use error::IntrospectionValidateError;
use std::marker::PhantomData;
use std::sync::Arc;
use http::HeaderName;
use serde::Deserialize;
use snafu::ResultExt as _;
use crate::core::BoxedError;
use crate::core::EndpointUrl;
use crate::core::client_auth::ClientAuthentication;
use crate::core::crypto::verifier::{JwsVerifierFactory, JwsVerifierPlatform};
use crate::core::http::HttpClient;
use crate::core::platform::{Duration, MaybeSendSync};
use crate::introspection::TokenIntrospection;
use crate::validator::binding::DPoPBindingChecker;
use crate::validator::binding::check_token_binding;
use crate::validator::dpop_nonce::DpopNonceChecker;
use crate::validator::extract::extract_token;
use crate::validator::introspection::error::{BindingSnafu, CallSnafu, ExtractSnafu};
use crate::validator::introspection::introspection_validator_builder::SetIntrospectionEndpoint;
use crate::validator::introspection::introspection_validator_builder::SetIssuer;
use crate::validator::introspection::introspection_validator_builder::SetJwksUri;
use crate::validator::introspection::introspection_validator_builder::State;
use crate::validator::{
AccessTokenValidator, ValidatedRequest, ValidationResult,
metadata::{ProvideValidatorMetadata, ValidatorMetadata},
observe::{OnValidate, ValidationOutcome},
};
pub struct IntrospectionValidator<
Auth: ClientAuthentication,
C: HttpClient,
N: DpopNonceChecker,
Claims = (),
> {
token_introspection: TokenIntrospection<Auth>,
http_client: C,
dpop_binding_checker: DPoPBindingChecker<N>,
token_header: HeaderName,
on_validate: Option<Arc<dyn OnValidate>>,
issuer: Option<String>,
require_mtls: bool,
_phantom: PhantomData<Claims>,
}
#[bon::bon]
impl<
Auth: ClientAuthentication,
C: HttpClient + Clone + 'static,
N: DpopNonceChecker,
Claims: for<'de> Deserialize<'de> + Clone + 'static,
> IntrospectionValidator<Auth, C, N, Claims>
{
#[builder(
start_fn(vis = "", name = "builder_internal"),
generics(setters(vis = "", name = "with_{}_internal")),
on(String, into)
)]
pub async fn new(
client_id: String,
issuer: Option<String>,
introspection_endpoint: EndpointUrl,
client_auth: Auth,
#[builder(default)]
request_jwt_response: bool,
http_client: C,
#[builder(into)]
allowed_dpop_signing_algorithms: Option<Vec<String>>,
#[builder(default = Duration::from_secs(60))]
max_dpop_proof_age: Duration,
#[builder(default)]
require_dpop: bool,
#[builder(default)]
require_mtls: bool,
jwks_uri: Option<EndpointUrl>,
#[cfg_attr(feature = "default-jws-verifier-platform", builder(default = crate::DefaultJwsVerifierPlatform::default().into()))]
jws_verifier_platform: Arc<dyn JwsVerifierPlatform>,
#[builder(setters(vis = "", name = "dpop_nonce_checker_internal"))]
dpop_nonce_checker: Option<N>,
dpop_jti_checker: Option<BoxedJtiUniquenessChecker>,
#[builder(default = Arc::new(JwksSource::builder().http_client(http_client.clone()).build()))]
jws_verifier_factory: Arc<dyn JwsVerifierFactory>,
#[builder(default = http::header::AUTHORIZATION)]
token_header: HeaderName,
on_validate: Option<Arc<dyn OnValidate>>,
) -> Result<Self, BoxedError> {
let token_introspection = TokenIntrospection::builder()
.client_id(client_id.clone())
.maybe_issuer(issuer.clone())
.introspection_endpoint(introspection_endpoint)
.client_auth(client_auth)
.request_jwt_response(request_jwt_response)
.maybe_jwks_uri(jwks_uri)
.jws_verifier_factory(jws_verifier_factory)
.jws_verifier_platform(jws_verifier_platform.clone())
.build()
.await?;
Ok(Self {
token_introspection,
http_client,
dpop_binding_checker: DPoPBindingChecker {
dpop_nonce_checker,
dpop_jti_checker,
max_proof_age: max_dpop_proof_age,
jws_verifier_platform,
allowed_signing_algorithms: allowed_dpop_signing_algorithms,
required: require_dpop,
},
token_header,
on_validate,
issuer,
require_mtls,
_phantom: PhantomData,
})
}
}
impl<Auth: ClientAuthentication, C: HttpClient + Clone + 'static>
IntrospectionValidator<Auth, C, NoNonceCheck, ()>
{
pub fn builder() -> IntrospectionValidatorBuilder<Auth, C, NoNonceCheck, ()> {
IntrospectionValidator::builder_internal()
}
#[allow(clippy::type_complexity)]
pub fn builder_from_metadata(
metadata: &AuthorizationServerMetadata,
) -> Option<
IntrospectionValidatorBuilder<
Auth,
C,
NoNonceCheck,
(),
SetJwksUri<SetIntrospectionEndpoint<SetIssuer>>,
>,
> {
metadata
.introspection_endpoint
.as_ref()
.map(|introspection_endpoint| {
Self::builder()
.issuer(metadata.issuer.clone())
.introspection_endpoint(introspection_endpoint.clone())
.maybe_jwks_uri(metadata.jwks_uri.clone())
})
}
}
impl<
Auth: ClientAuthentication,
C: HttpClient + Clone + 'static,
N: DpopNonceChecker,
Claims: for<'de> Deserialize<'de> + Clone + 'static,
S: State,
> IntrospectionValidatorBuilder<Auth, C, N, Claims, S>
{
pub fn with_claims<Claims1: for<'de> Deserialize<'de> + Clone + 'static>(
self,
) -> IntrospectionValidatorBuilder<Auth, C, N, Claims1, S> {
self.with_claims_internal()
}
pub fn dpop_nonce_checker<N1: DpopNonceChecker>(
self,
dpop_nonce_checker: N1,
) -> IntrospectionValidatorBuilder<Auth, C, N1, Claims, SetDpopNonceChecker<S>>
where
S::DpopNonceChecker: introspection_validator_builder::IsUnset,
{
self.with_n_internal()
.dpop_nonce_checker_internal(dpop_nonce_checker)
}
}
impl<
Auth: ClientAuthentication,
C: HttpClient,
N: DpopNonceChecker,
Claims: for<'de> Deserialize<'de> + Clone + 'static,
> IntrospectionValidator<Auth, C, N, Claims>
{
pub fn validator_metadata(&self, resource: Option<&str>) -> ValidatorMetadata {
ValidatorMetadata {
realm: None,
authorization_servers: self.issuer.as_ref().map(|s| vec![s.clone()]),
dpop_signing_alg_values_supported: self
.dpop_binding_checker
.allowed_signing_algorithms
.clone(),
dpop_bound_access_tokens_required: Some(self.dpop_binding_checker.required),
resource: resource.map(|r| r.to_owned()),
bearer_methods_supported: Some(vec!["header"]),
}
}
pub async fn validate_request(
&self,
headers: &http::HeaderMap,
http_method: &http::Method,
http_uri: &http::Uri,
client_cert_der: Option<&[u8]>,
) -> ValidationResult<
Claims,
IntrospectionValidateError<
<Auth as ClientAuthentication>::Error,
C::Error,
C::ResponseError,
>,
> {
let (dpop_nonce, outcome) = self
.validate_inner(headers, http_method, http_uri, client_cert_der)
.await;
if let Some(cb) = &self.on_validate {
use crate::introspection::IntrospectionCallError;
let validation_outcome = match &outcome {
Ok(Some(_)) => ValidationOutcome::Success,
Ok(None) => ValidationOutcome::NoToken,
Err(IntrospectionValidateError::Extract { .. }) => ValidationOutcome::ExtractError,
Err(IntrospectionValidateError::Binding { .. }) => ValidationOutcome::BindingError,
Err(IntrospectionValidateError::Call { source, .. }) => {
if matches!(source, IntrospectionCallError::TokenInactive) {
ValidationOutcome::InvalidToken
} else {
ValidationOutcome::CallError
}
}
};
cb.on_validate(validation_outcome);
}
ValidationResult {
outcome,
dpop_nonce,
}
}
async fn validate_inner(
&self,
headers: &http::HeaderMap,
http_method: &http::Method,
http_uri: &http::Uri,
client_cert_der: Option<&[u8]>,
) -> (
Option<String>,
Result<
Option<ValidatedRequest<Claims>>,
IntrospectionValidateError<
<Auth as ClientAuthentication>::Error,
C::Error,
C::ResponseError,
>,
>,
) {
let (token_type, access_token) =
match extract_token(headers, &self.token_header).context(ExtractSnafu) {
Err(e) => return (None, Err(e)),
Ok(None) => return (None, Ok(None)),
Ok(Some(v)) => v,
};
let validated = match self
.token_introspection
.introspect::<_, Claims>(&self.http_client, &access_token)
.await
.context(CallSnafu { token_type })
{
Err(e) => return (None, Err(e)),
Ok(v) => v,
};
let (dpop_nonce, binding_result) = check_token_binding(
token_type,
validated.cnf.as_ref(),
&access_token,
&self.dpop_binding_checker,
self.require_mtls,
headers,
http_method,
http_uri,
client_cert_der,
)
.await;
let outcome = binding_result
.context(BindingSnafu { token_type })
.map(|()| Some(validated));
(dpop_nonce, outcome)
}
}
impl<
Auth: ClientAuthentication,
C: HttpClient,
N: DpopNonceChecker,
Claims: for<'de> Deserialize<'de> + Clone + 'static,
> ProvideValidatorMetadata for IntrospectionValidator<Auth, C, N, Claims>
{
fn validator_metadata(&self, resource: Option<&str>) -> ValidatorMetadata {
self.validator_metadata(resource)
}
}
impl<
Auth: ClientAuthentication,
C: HttpClient,
N: DpopNonceChecker,
Claims: for<'de> Deserialize<'de> + Clone + MaybeSendSync + 'static,
> AccessTokenValidator for IntrospectionValidator<Auth, C, N, Claims>
{
type Claims = Claims;
type Error = IntrospectionValidateError<
<Auth as ClientAuthentication>::Error,
C::Error,
C::ResponseError,
>;
async fn validate_request(
&self,
headers: &http::HeaderMap,
method: &http::Method,
uri: &http::Uri,
client_cert_der: Option<&[u8]>,
) -> ValidationResult<Self::Claims, Self::Error> {
self.validate_request(headers, method, uri, client_cert_der)
.await
}
}