use std::sync::Arc;
use crate::adapters::secondary::oauth::http_body::{describe, read_capped};
use crate::application::oauth::redeem::{
RefreshRequest, TokenEndpoint, TokenEndpointError, TokenRequest,
};
use crate::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
use crate::domain::oauth::registration::{ClientSecret, TokenAuthMethod};
use crate::domain::oauth::resource::ResourceParameters;
#[derive(Debug, Clone)]
pub struct HttpTokenEndpoint {
endpoint: Arc<str>,
http: reqwest::Client,
}
impl HttpTokenEndpoint {
#[must_use]
pub fn new(endpoint: impl AsRef<str>) -> Self {
Self {
endpoint: Arc::from(endpoint.as_ref()),
http: reqwest::Client::new(),
}
}
#[must_use]
pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
#[must_use]
pub fn endpoint(&self) -> &str {
&self.endpoint
}
}
impl TokenEndpoint for HttpTokenEndpoint {
async fn refresh(
&self,
request: RefreshRequest<'_>,
) -> Result<IssuedToken, TokenEndpointError> {
let mut form = vec![
("grant_type", "refresh_token"),
("refresh_token", request.refresh_token()),
];
self.post(
&mut form,
request.client_id().as_str(),
request.client_secret().map(ClientSecret::expose),
request.auth_method(),
request.resource(),
request.scope(),
)
.await
}
async fn redeem(&self, request: TokenRequest<'_>) -> Result<IssuedToken, TokenEndpointError> {
let mut form = vec![
("grant_type", "authorization_code"),
("code", request.code()),
("redirect_uri", request.redirect_uri()),
("code_verifier", request.code_verifier().as_str()),
];
self.post(
&mut form,
request.client_id().as_str(),
request.client_secret().map(ClientSecret::expose),
request.auth_method(),
request.resource(),
request.scope(),
)
.await
}
}
impl HttpTokenEndpoint {
async fn post<'a>(
&self,
form: &mut Vec<(&'a str, &'a str)>,
client_id: &'a str,
client_secret: Option<&'a str>,
auth_method: TokenAuthMethod,
resource: &'a ResourceParameters,
scope: Option<&'a str>,
) -> Result<IssuedToken, TokenEndpointError> {
let mut basic = None;
match (auth_method, client_secret) {
(TokenAuthMethod::ClientSecretBasic, Some(secret)) => {
basic = Some(basic_credentials(client_id, secret));
}
(TokenAuthMethod::ClientSecretPost, Some(secret)) => {
form.push(("client_id", client_id));
form.push(("client_secret", secret));
}
_ => form.push(("client_id", client_id)),
}
for indicator in resource.iter() {
form.push(("resource", indicator));
}
if let Some(scope) = scope {
form.push(("scope", scope));
}
let mut post = self.http.post(&*self.endpoint).form(&*form);
if let Some(credentials) = basic {
post = post.header(reqwest::header::AUTHORIZATION, credentials);
}
let response = post
.send()
.await
.map_err(|failure| TokenEndpointError::Unavailable(describe(failure)))?;
let status = response.status();
let body = read_capped(response)
.await
.map_err(TokenEndpointError::Unavailable)?;
if !status.is_success() {
return match IssuedToken::parse(&body) {
Err(refused @ TokenResponseError::AuthorizationServer { .. }) => {
Err(TokenEndpointError::Refused(refused))
}
_ => Err(TokenEndpointError::Unavailable(format!(
"token endpoint answered {}",
status.as_u16(),
))),
};
}
IssuedToken::parse(&body).map_err(TokenEndpointError::Refused)
}
}
fn basic_credentials(id: &str, secret: &str) -> String {
use base64::Engine as _;
let encode =
|text: &str| url::form_urlencoded::byte_serialize(text.as_bytes()).collect::<String>();
let pair = format!("{}:{}", encode(id), encode(secret));
format!(
"Basic {}",
base64::engine::general_purpose::STANDARD.encode(pair.as_bytes())
)
}
#[cfg(test)]
mod tests;