use std::sync::Arc;
use bon::Builder;
use http::{
HeaderMap, HeaderName, Method, Uri,
header::{AUTHORIZATION, InvalidHeaderValue},
};
use snafu::prelude::*;
use crate::{
cache::{GetTokenError, TokenCache},
core::{dpop::ResourceServerDPoP, http::HttpClient},
grant::core::TokenResponse,
token::AccessToken,
};
#[derive(Builder)]
pub struct HttpAuthorizer<T: TokenCache> {
cache: T,
#[builder(default = AUTHORIZATION)]
authorization_header: HeaderName,
}
impl<T: TokenCache> core::fmt::Debug for HttpAuthorizer<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("HttpAuthorizer")
.field("authorization_header", &self.authorization_header)
.finish_non_exhaustive()
}
}
#[derive(Debug, Snafu)]
pub enum AuthorizerError<TcErr: crate::core::Error + 'static, DPoPErr: crate::core::Error + 'static>
{
TokenCache {
source: TcErr,
},
DPoP {
source: DPoPErr,
},
#[snafu(display("Received DPoP token but no DPoP configuration present"))]
UnexpectedDPoPToken,
InvalidHeader {
source: InvalidHeaderValue,
},
}
impl<TcErr: crate::core::Error + 'static, DPoPErr: crate::core::Error + 'static> crate::core::Error
for AuthorizerError<TcErr, DPoPErr>
{
fn is_retryable(&self) -> bool {
match self {
Self::TokenCache { source } => source.is_retryable(),
Self::DPoP { source } => source.is_retryable(),
Self::UnexpectedDPoPToken | Self::InvalidHeader { .. } => false,
}
}
}
impl<T: TokenCache> HttpAuthorizer<T> {
pub async fn get_headers<C: HttpClient>(
&self,
http_client: &C,
method: &Method,
uri: &Uri,
) -> Result<
HeaderMap,
AuthorizerError<GetTokenError<T::Error<C>>, <T::DPoP as ResourceServerDPoP>::Error>,
> {
let token = self
.cache
.get_token_response(http_client)
.await
.context(TokenCacheSnafu)?;
let mut headers = HeaderMap::new();
match token.access_token() {
AccessToken::Dpop(dpop_access_token) => {
if let Some(proof) = self
.cache
.resource_server_dpop()
.proof(
method,
uri,
dpop_access_token.token(),
dpop_access_token.jkt(),
)
.await
.context(DPoPSnafu)?
{
headers.insert(
"DPoP",
proof.expose_secret().parse().context(InvalidHeaderSnafu)?,
);
headers.insert(
&self.authorization_header,
dpop_access_token
.expose_header_value()
.context(InvalidHeaderSnafu)?,
);
} else {
return UnexpectedDPoPTokenSnafu.fail();
}
}
AccessToken::Bearer(bearer_access_token) => {
headers.insert(
&self.authorization_header,
bearer_access_token
.expose_header_value()
.context(InvalidHeaderSnafu)?,
);
}
}
Ok(headers)
}
pub fn cache(&self) -> &T {
&self.cache
}
pub async fn prime(&self, response: Arc<TokenResponse>) {
self.cache.prime(response).await;
}
pub fn invalidate(&self) {
self.cache.invalidate();
}
pub fn set_nonce(&self, uri: &Uri, nonce: String) {
self.cache.resource_server_dpop().update_nonce(uri, nonce);
}
pub fn update_from_response_headers(&self, uri: &Uri, headers: &HeaderMap) {
if let Some(nonce) = extract_dpop_nonce(headers) {
self.cache.resource_server_dpop().update_nonce(uri, nonce);
}
}
}
#[must_use]
pub fn extract_dpop_nonce(headers: &HeaderMap) -> Option<String> {
headers
.get("DPoP-Nonce")
.and_then(|v| v.to_str().ok())
.map(std::borrow::ToOwned::to_owned)
}