pub use crate::aadsts_err_gen::*;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
pub const INVALID_CRED: u32 = 0xC3CE;
pub const REQUIRES_MFA: u32 = 0xC39C;
pub const INVALID_USER: u32 = 0xC372;
pub const NO_CONSENT: u32 = 0xFDE9;
pub const NO_GROUP_CONSENT: u32 = 0xFDEA;
pub const NO_SECRET: u32 = 0x6AD09A;
pub const AUTH_PENDING: u32 = 0x11180;
pub const DEVICE_AUTH_FAIL: u32 = 0xC3EB;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: String,
#[serde(default)]
pub error_description: String,
#[serde(default)]
pub suberror: Option<String>,
#[serde(default)]
pub error_codes: Vec<u32>,
}
#[derive(Serialize, Debug)]
pub enum MsalError {
InvalidJson(String),
InvalidBase64(String),
InvalidRegex(String),
InvalidParse(String),
AcquireTokenFailed(ErrorResponse),
GeneralFailure(String),
RequestFailed(String),
AuthTypeUnsupported,
TPMFail(String),
URLFormatFailed(String),
DeviceEnrollmentFail(String),
CryptoFail(String),
NotImplemented,
ConfigError(String),
MFAPollContinue,
AADSTSError(AADSTSError),
Missing(String),
FormatError(String),
#[cfg(feature = "changepassword")]
ChangePassword,
PasswordRequired,
SkipMfaRegistration(String, Option<String>, String),
ConsentRequested(String),
AuthCodeReceived(String),
MFARequired,
AuthorizationDenied,
MFAInvalidCode(String),
MFADAGFallbackDisabled,
#[cfg(feature = "on_behalf_of")]
OboInteractionRequired {
error: ErrorResponse,
claims: Option<String>,
},
}
impl fmt::Display for MsalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MsalError::InvalidJson(msg) => write!(f, "Invalid JSON: {}", msg),
MsalError::InvalidBase64(msg) => write!(f, "Invalid base64: {}", msg),
MsalError::InvalidRegex(msg) => write!(f, "Invalid regex: {}", msg),
MsalError::InvalidParse(msg) => write!(f, "Parse error: {}", msg),
MsalError::AcquireTokenFailed(err) => write!(
f,
"Token acquisition failed: {} ({})",
err.error, err.error_description
),
MsalError::GeneralFailure(msg) => write!(f, "General failure: {}", msg),
MsalError::RequestFailed(msg) => write!(f, "Request failed: {}", msg),
MsalError::AuthTypeUnsupported => write!(f, "Authentication type is not supported"),
MsalError::TPMFail(msg) => write!(f, "TPM error: {}", msg),
MsalError::URLFormatFailed(msg) => write!(f, "URL format error: {}", msg),
MsalError::DeviceEnrollmentFail(msg) => write!(f, "Device enrollment failed: {}", msg),
MsalError::CryptoFail(msg) => write!(f, "Cryptography failure: {}", msg),
MsalError::ConfigError(msg) => write!(f, "Configuration error: {}", msg),
MsalError::AADSTSError(err) => write!(f, "{}", err),
MsalError::Missing(msg) => write!(f, "Missing value: {}", msg),
MsalError::FormatError(msg) => write!(f, "Formatting error: {}", msg),
#[cfg(feature = "changepassword")]
MsalError::ChangePassword => write!(f, "Unexpected error"),
MsalError::NotImplemented
| MsalError::MFAPollContinue
| MsalError::PasswordRequired
| MsalError::SkipMfaRegistration(..) => write!(f, "Unexpected error"),
MsalError::ConsentRequested(msg) => write!(f, "{}", msg),
MsalError::AuthCodeReceived(_) => {
write!(f, "Authorization code received directly from login")
}
MsalError::MFARequired => {
write!(f, "MFA is required to complete authentication")
}
MsalError::AuthorizationDenied => write!(f, "Authorization denied"),
MsalError::MFAInvalidCode(msg) => {
write!(f, "AuthResponse indicates failure: {}", msg)
}
MsalError::MFADAGFallbackDisabled => {
write!(f, "MFA failed and DAG fallback is disabled")
}
#[cfg(feature = "on_behalf_of")]
MsalError::OboInteractionRequired { ref error, .. } => {
write!(
f,
"OBO interaction required: {} ({})",
error.error, error.error_description
)
}
}
}
}
impl MsalError {
pub fn request_failed(e: &reqwest::Error) -> Self {
let mut msg = format!("{}", e);
let mut src = e.source();
while let Some(s) = src {
msg.push_str(&format!(": {}", s));
src = s.source();
}
MsalError::RequestFailed(msg)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn error_response_full_deserialization() {
let json = r#"{
"error": "invalid_grant",
"error_description": "AADSTS65001: Consent required",
"error_codes": [65001]
}"#;
let resp: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.error, "invalid_grant");
assert_eq!(resp.error_description, "AADSTS65001: Consent required");
assert_eq!(resp.suberror, None);
assert_eq!(resp.error_codes, vec![65001]);
}
#[test]
fn error_response_missing_error_codes() {
let json = r#"{
"error": "interaction_required",
"error_description": "AADSTS50076"
}"#;
let resp: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.error, "interaction_required");
assert_eq!(resp.suberror, None);
assert!(resp.error_codes.is_empty());
}
#[test]
fn error_response_missing_description_and_codes() {
let json = r#"{"error": "server_error"}"#;
let resp: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.error, "server_error");
assert_eq!(resp.error_description, "");
assert_eq!(resp.suberror, None);
assert!(resp.error_codes.is_empty());
}
#[test]
fn error_response_multiple_error_codes() {
let json = r#"{
"error": "invalid_request",
"error_description": "Multiple errors",
"error_codes": [50076, 50074, 16000]
}"#;
let resp: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.suberror, None);
assert_eq!(resp.error_codes, vec![50076, 50074, 16000]);
}
#[test]
fn error_response_with_suberror() {
let json = r#"{
"error": "invalid_grant",
"error_description": "AADSTS50076",
"suberror": "basic_action",
"error_codes": [50076]
}"#;
let resp: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.suberror, Some("basic_action".to_string()));
}
#[cfg(feature = "on_behalf_of")]
#[test]
fn obo_interaction_required_display() {
let error = MsalError::OboInteractionRequired {
error: ErrorResponse {
error: "interaction_required".to_string(),
error_description: "AADSTS50076: MFA required".to_string(),
suberror: None,
error_codes: vec![50076],
},
claims: Some("{\"access_token\":{}}".to_string()),
};
let display = format!("{}", error);
assert!(display.contains("OBO interaction required"));
assert!(display.contains("interaction_required"));
assert!(display.contains("AADSTS50076"));
}
#[cfg(feature = "on_behalf_of")]
#[test]
fn obo_interaction_required_without_claims_display() {
let error = MsalError::OboInteractionRequired {
error: ErrorResponse {
error: "interaction_required".to_string(),
error_description: "AADSTS16000".to_string(),
suberror: None,
error_codes: vec![16000],
},
claims: None,
};
let display = format!("{}", error);
assert!(display.contains("OBO interaction required"));
assert!(display.contains("AADSTS16000"));
}
#[test]
fn error_response_roundtrip_serialization() {
let resp = ErrorResponse {
error: "test_error".to_string(),
error_description: "A test error".to_string(),
suberror: Some("other".to_string()),
error_codes: vec![12345],
};
let json = serde_json::to_string(&resp).unwrap();
let deserialized: ErrorResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.error, resp.error);
assert_eq!(deserialized.error_description, resp.error_description);
assert_eq!(deserialized.suberror, resp.suberror);
assert_eq!(deserialized.error_codes, resp.error_codes);
}
}