use bon::Builder;
use bytes::Bytes;
use http::{HeaderValue, Method, Request, Uri, header::CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use crate::core::{
Error, ErrorKind,
client_auth::AuthenticationParams,
dpop::AuthorizationServerDPoP,
http::{HttpClient, Idempotency},
};
#[derive(Builder)]
pub struct OAuth2FormRequest<'a, F: Serialize> {
uri: &'a Uri,
form: &'a F,
auth_params: AuthenticationParams<'a>,
dpop: &'a dyn AuthorizationServerDPoP,
dpop_jkt: Option<&'a str>,
}
impl<F: Serialize> OAuth2FormRequest<'_, F> {
pub async fn build_request(&self) -> Result<Request<Bytes>, Error> {
let headers = self.auth_params.headers.clone().unwrap_or_default();
let mut body = serde_html_form::to_string(self.form)
.map_err(|e| serialize_form_error(e).with_context("serializing exchange parameters"))?;
if let Some(kv) = &self.auth_params.form_params {
if !body.is_empty() {
body.push('&');
}
serde_html_form::push_to_string(&mut body, kv).map_err(|e| {
serialize_form_error(e).with_context("serializing authentication parameters")
})?;
}
let (mut parts, ()) = http::Request::new(()).into_parts();
parts.method = Method::POST;
parts.uri = self.uri.clone();
if let Some(proof) = self
.dpop
.proof(&parts.method, &parts.uri, self.dpop_jkt)
.await?
{
parts.headers.insert(
"DPoP",
HeaderValue::from_str(proof.expose_secret()).map_err(|e| {
Error::new(ErrorKind::Dpop, e)
.with_context("DPoP proof is not a valid header value")
})?,
);
}
parts.headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("application/x-www-form-urlencoded"),
);
parts.headers.extend(headers);
Ok(Request::from_parts(parts, body.into()))
}
pub async fn execute<R: for<'de> Deserialize<'de>>(
&self,
http_client: &dyn HttpClient,
) -> Result<R, Error> {
let request = self.build_request().await?;
let response = http_client.execute(request, Idempotency::Unknown).await?;
let content_type = if response.status.is_success() {
None
} else {
response.headers.get(CONTENT_TYPE).cloned()
};
if let Some(nonce) = response.headers.get("DPoP-Nonce")
&& let Ok(nonce_str) = nonce.to_str()
{
self.dpop.update_nonce(nonce_str.to_string());
}
parse_oauth2_response(response.status, content_type, &response.body)
}
pub async fn execute_empty_response(&self, http_client: &dyn HttpClient) -> Result<(), Error> {
let request = self.build_request().await?;
let response = http_client.execute(request, Idempotency::Unknown).await?;
if response.status.is_success() {
return Ok(());
}
let content_type = response.headers.get(CONTENT_TYPE).cloned();
Err(parse_oauth2_error_response(
response.status,
content_type,
&response.body,
))
}
}
fn serialize_form_error(source: serde_html_form::ser::Error) -> Error {
Error::new(ErrorKind::Config, source)
}
fn parse_oauth2_error_response(
status: http::StatusCode,
content_type: Option<HeaderValue>,
body: &Bytes,
) -> Error {
match serde_json::from_slice::<OAuth2ErrorBody>(body) {
Ok(error_body) => {
let code = error_body.error.clone();
let kind = match code.as_str() {
"invalid_grant" => ErrorKind::InvalidGrant,
"use_dpop_nonce" => ErrorKind::Dpop,
_ if status.is_server_error() => ErrorKind::Transport { retryable: true },
_ => ErrorKind::Protocol,
};
Error::new(
kind,
HandleResponseError::OAuth2 {
body: error_body,
status,
content_type,
},
)
.with_oauth_error_code(code)
}
Err(source) => {
let kind = if status.is_server_error() {
ErrorKind::Transport { retryable: true }
} else {
ErrorKind::Protocol
};
Error::new(
kind,
HandleResponseError::UnparseableErrorResponse {
body: String::from_utf8_lossy(body).into_owned(),
status,
content_type,
source,
},
)
}
}
}
fn parse_oauth2_response<T: for<'de> Deserialize<'de>>(
status: http::StatusCode,
content_type: Option<HeaderValue>,
body: &Bytes,
) -> Result<T, Error> {
if !status.is_success() {
return Err(parse_oauth2_error_response(status, content_type, body));
}
serde_json::from_slice(body).map_err(|source| {
Error::new(
ErrorKind::Protocol,
HandleResponseError::UnparseableSuccessResponse {
body: String::from_utf8_lossy(body).into_owned(),
source,
},
)
})
}
#[derive(Debug, Snafu)]
pub enum HandleResponseError {
#[snafu(display(
"Failed to parse error response as OAuth2 error: status={status}, content-type={ct}, body={body}",
ct = content_type.as_ref().map(|s| s.to_str().ok().unwrap_or_default()).unwrap_or_default()
))]
UnparseableErrorResponse {
body: String,
status: http::StatusCode,
content_type: Option<http::HeaderValue>,
source: serde_json::Error,
},
#[snafu(display("Failed to parse successful response as an OAuth2 payload"))]
UnparseableSuccessResponse {
body: String,
source: serde_json::Error,
},
#[snafu(display("OAuth2 request failed with an OAuth2 error payload: {body}"))]
OAuth2 {
body: OAuth2ErrorBody,
status: http::StatusCode,
content_type: Option<http::HeaderValue>,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct OAuth2ErrorBody {
pub error: String,
pub error_description: Option<String>,
pub error_uri: Option<String>,
}
impl std::fmt::Display for OAuth2ErrorBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.error)?;
if let Some(description) = &self.error_description {
write!(f, ": {description}")?;
}
if let Some(uri) = &self.error_uri {
write!(f, " (see {uri})")?;
}
Ok(())
}
}
macro_rules! with_dpop_nonce_retry {
($body:block) => {{
let result = $body;
if let Err(ref e) = result
&& e.is_dpop_nonce_required()
{
$body
} else {
result
}
}};
}
pub(crate) use with_dpop_nonce_retry;