use crate::error::{AuthError, AuthResult};
use serde::{Deserialize, Serialize};
#[derive(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>,
}
impl std::fmt::Debug for TokenResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenResponse")
.field("access_token", &"[redacted]")
.field("instance_url", &self.instance_url)
.field(
"refresh_token",
&self.refresh_token.as_ref().map(|_| "[redacted]"),
)
.field("id_token", &self.id_token.as_ref().map(|_| "[redacted]"))
.field("scope", &self.scope)
.field("issued_at", &self.issued_at)
.field("id", &self.id)
.field("signature", &self.signature.as_ref().map(|_| "[redacted]"))
.field("token_type", &self.token_type)
.finish()
}
}
#[derive(Deserialize)]
struct OAuthErrorResponse {
error: String,
#[serde(default)]
error_description: Option<String>,
}
impl std::fmt::Debug for OAuthErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OAuthErrorResponse")
.field("error", &self.error)
.field(
"error_description",
&self.error_description.as_ref().map(|_| "[redacted]"),
)
.finish()
}
}
pub(super) async fn exchange<B>(
http: &reqwest::Client,
login_url: &str,
body: &B,
) -> AuthResult<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(AuthError::OAuth {
error: oauth_err.error,
error_description: oauth_err.error_description,
});
}
return Err(AuthError::Other(format!(
"token endpoint returned status {status}: {}",
String::from_utf8_lossy(&bytes)
)));
}
serde_json::from_slice::<TokenResponse>(&bytes)
.map_err(|e| AuthError::Other(format!("malformed token response: {e}")))
}
pub(super) fn check_instance_url(expected: &str, response: &TokenResponse) -> AuthResult<()> {
let returned = response.instance_url.trim_end_matches('/');
if returned != expected {
return Err(AuthError::Other(format!(
"token response instance_url ({returned}) does not match configured instance_url ({expected})"
)));
}
Ok(())
}