use thiserror::Error;
pub type AuthResult<T> = Result<T, AuthError>;
#[derive(Error)]
#[non_exhaustive]
pub enum AuthError {
#[error("missing required builder field: {0}")]
MissingField(&'static str),
#[error("OAuth error: {error}")]
OAuth {
error: String,
error_description: Option<String>,
},
#[error("authentication failed: {0}")]
Other(String),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("invalid URL: {0}")]
Url(#[from] url::ParseError),
}
impl std::fmt::Debug for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingField(field) => f.debug_tuple("MissingField").field(field).finish(),
Self::OAuth {
error,
error_description,
} => f
.debug_struct("OAuth")
.field("error", error)
.field(
"error_description",
&error_description.as_ref().map(|_| "[redacted]"),
)
.finish(),
Self::Other(msg) => f.debug_tuple("Other").field(msg).finish(),
Self::Http(e) => f.debug_tuple("Http").field(e).finish(),
Self::Serialization(e) => f.debug_tuple("Serialization").field(e).finish(),
Self::Url(e) => f.debug_tuple("Url").field(e).finish(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn oauth_error_redacts_description_in_display_and_debug() {
let err = AuthError::OAuth {
error: "invalid_grant".to_string(),
error_description: Some("00Dxx!AQ.SECRET_TOKEN_FRAGMENT".to_string()),
};
let display = err.to_string();
assert!(display.contains("invalid_grant"));
assert!(
!display.contains("SECRET_TOKEN_FRAGMENT"),
"Display leaked error_description: {display}"
);
let debug = format!("{err:?}");
assert!(debug.contains("invalid_grant"));
assert!(debug.contains("[redacted]"));
assert!(
!debug.contains("SECRET_TOKEN_FRAGMENT"),
"Debug leaked error_description: {debug}"
);
match err {
AuthError::OAuth {
error_description, ..
} => assert!(error_description.is_some()),
other => panic!("expected OAuth, got {other:?}"),
}
}
#[test]
fn oauth_error_debug_shows_none_description_without_redaction_marker() {
let err = AuthError::OAuth {
error: "invalid_client".to_string(),
error_description: None,
};
let debug = format!("{err:?}");
assert!(debug.contains("invalid_client"));
assert!(debug.contains("None"));
assert!(!debug.contains("[redacted]"));
}
}