use crate::error::{CirrusError, CirrusResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub(super) struct TokenResponse {
pub(super) access_token: String,
pub(super) instance_url: String,
#[serde(default)]
pub(super) refresh_token: Option<String>,
#[serde(default)]
pub(super) id_token: Option<String>,
#[serde(default)]
pub(super) scope: Option<String>,
#[serde(default)]
pub(super) issued_at: Option<String>,
#[serde(default)]
pub(super) id: Option<String>,
#[serde(default)]
pub(super) signature: Option<String>,
#[serde(default)]
#[allow(dead_code)]
pub(super) token_type: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OAuthErrorResponse {
error: String,
#[serde(default)]
error_description: Option<String>,
}
pub(super) async fn exchange<B>(
http: &reqwest::Client,
login_url: &str,
body: &B,
) -> CirrusResult<TokenResponse>
where
B: Serialize + ?Sized,
{
let url = format!("{login_url}/services/oauth2/token");
let response = http.post(&url).form(body).send().await?;
let status = response.status().as_u16();
let bytes = response.bytes().await?;
if !(200..300).contains(&status) {
if let Ok(oauth_err) = serde_json::from_slice::<OAuthErrorResponse>(&bytes) {
return Err(CirrusError::OAuth {
error: oauth_err.error,
error_description: oauth_err.error_description,
});
}
return Err(CirrusError::Auth(format!(
"token endpoint returned status {status}: {}",
String::from_utf8_lossy(&bytes)
)));
}
serde_json::from_slice::<TokenResponse>(&bytes)
.map_err(|e| CirrusError::Auth(format!("malformed token response: {e}")))
}
pub(super) fn check_instance_url(expected: &str, response: &TokenResponse) -> CirrusResult<()> {
let returned = response.instance_url.trim_end_matches('/');
if returned != expected {
return Err(CirrusError::Auth(format!(
"token response instance_url ({returned}) does not match configured instance_url ({expected})"
)));
}
Ok(())
}