use chrono::{DateTime, Utc};
use headers::{Authorization, HeaderMapExt};
use http::Request;
use mas_http::{CatchHttpCodesLayer, FormUrlencodedRequestLayer, JsonResponseLayer};
use mas_iana::oauth::OAuthTokenTypeHint;
use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse};
use rand::Rng;
use serde::Serialize;
use tower::{Layer, Service, ServiceExt};
use url::Url;
use crate::{
error::IntrospectionError,
http_service::HttpService,
types::client_credentials::{ClientCredentials, RequestWithClientCredentials},
utils::{http_all_error_status_codes, http_error_mapper},
};
pub enum IntrospectionAuthentication<'a> {
Credentials(ClientCredentials),
BearerToken(&'a str),
}
impl<'a> IntrospectionAuthentication<'a> {
#[must_use]
pub fn with_client_credentials(credentials: ClientCredentials) -> Self {
Self::Credentials(credentials)
}
#[must_use]
pub fn with_bearer_token(token: &'a str) -> Self {
Self::BearerToken(token)
}
fn apply_to_request<T: Serialize>(
self,
request: Request<T>,
now: DateTime<Utc>,
rng: &mut impl Rng,
) -> Result<Request<RequestWithClientCredentials<T>>, IntrospectionError> {
let res = match self {
IntrospectionAuthentication::Credentials(client_credentials) => {
client_credentials.apply_to_request(request, now, rng)?
}
IntrospectionAuthentication::BearerToken(access_token) => {
let (mut parts, body) = request.into_parts();
parts
.headers
.typed_insert(Authorization::bearer(access_token)?);
let body = RequestWithClientCredentials {
body,
credentials: None,
};
http::Request::from_parts(parts, body)
}
};
Ok(res)
}
}
impl<'a> From<ClientCredentials> for IntrospectionAuthentication<'a> {
fn from(credentials: ClientCredentials) -> Self {
Self::with_client_credentials(credentials)
}
}
#[tracing::instrument(skip_all, fields(introspection_endpoint))]
pub async fn introspect_token(
http_service: &HttpService,
authentication: IntrospectionAuthentication<'_>,
introspection_endpoint: &Url,
token: String,
token_type_hint: Option<OAuthTokenTypeHint>,
now: DateTime<Utc>,
rng: &mut impl Rng,
) -> Result<IntrospectionResponse, IntrospectionError> {
tracing::debug!("Introspecting token…");
let introspection_request = IntrospectionRequest {
token,
token_type_hint,
};
let introspection_request =
http::Request::post(introspection_endpoint.as_str()).body(introspection_request)?;
let introspection_request = authentication.apply_to_request(introspection_request, now, rng)?;
let service = (
FormUrlencodedRequestLayer::default(),
JsonResponseLayer::<IntrospectionResponse>::default(),
CatchHttpCodesLayer::new(http_all_error_status_codes(), http_error_mapper),
)
.layer(http_service.clone());
let introspection_response = service
.ready_oneshot()
.await?
.call(introspection_request)
.await?
.into_body();
Ok(introspection_response)
}