use std::borrow::Cow;
use crate::core::dpop::DPoPNotConfigured;
use bon::Builder;
use serde::Serialize;
use snafu::prelude::*;
use crate::{
core::{
EndpointUrl, IntoEndpointUrl, client_auth::ClientAuthentication, dpop::NoDPoP,
http::HttpClient, server_metadata::AuthorizationServerMetadata,
},
grant::core::form::{OAuth2FormError, OAuth2FormRequest},
token::{AccessToken, RefreshToken},
};
pub trait RevocableToken {
fn token_value(&self) -> &str;
fn token_type_hint(&self) -> &'static str;
}
impl RevocableToken for AccessToken {
fn token_value(&self) -> &str {
self.token().expose_secret()
}
fn token_type_hint(&self) -> &'static str {
"access_token"
}
}
impl RevocableToken for RefreshToken {
fn token_value(&self) -> &str {
self.token().expose_secret()
}
fn token_type_hint(&self) -> &'static str {
"refresh_token"
}
}
#[derive(Debug, Clone, Builder)]
#[builder(state_mod(name = "builder"))]
pub struct TokenRevocation<Auth: ClientAuthentication + 'static> {
#[builder(into)]
client_id: Cow<'static, str>,
client_auth: Auth,
#[builder(into)]
issuer: Option<String>,
#[builder(setters(vis = "", name = "revocation_endpoint_internal"))]
revocation_endpoint: EndpointUrl,
#[builder(setters(vis = "", name = "mtls_revocation_endpoint_internal"))]
mtls_revocation_endpoint: Option<EndpointUrl>,
revocation_endpoint_auth_methods_supported: Option<Vec<String>>,
}
impl<Auth: ClientAuthentication + 'static> TokenRevocation<Auth> {
#[allow(clippy::type_complexity)]
pub fn builder_from_metadata(
metadata: &AuthorizationServerMetadata,
) -> Option<
TokenRevocationBuilder<
Auth,
builder::SetMtlsRevocationEndpoint<
builder::SetRevocationEndpointAuthMethodsSupported<
builder::SetRevocationEndpoint<builder::SetIssuer<builder::Empty>>,
>,
>,
>,
> {
let revocation_endpoint = metadata.revocation_endpoint.clone()?;
Some(
Self::builder()
.issuer(metadata.issuer.clone())
.revocation_endpoint_internal(revocation_endpoint)
.revocation_endpoint_auth_methods_supported(
metadata.revocation_endpoint_auth_methods_supported.clone(),
)
.maybe_mtls_revocation_endpoint_internal(
metadata
.mtls_endpoint_aliases
.as_ref()
.and_then(|a| a.revocation_endpoint.clone()),
),
)
}
pub async fn revoke<C: HttpClient>(
&self,
http_client: &C,
token: &impl RevocableToken,
) -> Result<
(),
RevocationError<C::Error, C::ResponseError, <Auth as ClientAuthentication>::Error>,
> {
let effective_endpoint = if http_client.uses_mtls() {
self.mtls_revocation_endpoint
.as_ref()
.unwrap_or(&self.revocation_endpoint)
} else {
&self.revocation_endpoint
};
let auth_params = self
.client_auth
.authentication_params(
&self.client_id,
self.issuer.as_deref(),
effective_endpoint.as_uri(),
self.revocation_endpoint_auth_methods_supported.as_deref(),
)
.await
.context(AuthSnafu)?;
let form = RevocationForm {
token: token.token_value(),
token_type_hint: token.token_type_hint(),
};
OAuth2FormRequest::builder()
.auth_params(auth_params)
.form(&form)
.uri(effective_endpoint.as_uri())
.dpop(&NoDPoP)
.build()
.execute_empty_response(http_client)
.await
.context(RevocationSnafu)?;
Ok(())
}
}
impl<Auth: ClientAuthentication, S: builder::State> TokenRevocationBuilder<Auth, S> {
pub fn revocation_endpoint<U: IntoEndpointUrl>(
self,
url: U,
) -> Result<TokenRevocationBuilder<Auth, builder::SetRevocationEndpoint<S>>, U::Error>
where
S::RevocationEndpoint: builder::IsUnset,
{
Ok(self.revocation_endpoint_internal(url.into_endpoint_url()?))
}
}
#[derive(Debug, Serialize)]
struct RevocationForm<'a> {
token: &'a str,
token_type_hint: &'static str,
}
#[derive(Debug, Snafu)]
pub enum RevocationError<
HttpReqErr: crate::core::Error,
HttpRespErr: crate::core::Error,
AuthErr: crate::core::Error,
> {
Auth {
source: AuthErr,
},
Revocation {
source: OAuth2FormError<HttpReqErr, HttpRespErr, DPoPNotConfigured>,
},
}
impl<HttpReqErr: crate::core::Error, HttpRespErr: crate::core::Error, AuthErr: crate::core::Error>
crate::core::Error for RevocationError<HttpReqErr, HttpRespErr, AuthErr>
{
fn is_retryable(&self) -> bool {
match self {
Self::Auth { source } => source.is_retryable(),
Self::Revocation { source } => source.is_retryable(),
}
}
}