use http::StatusCode;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum OAuthErrorCode {
InvalidRequest,
InvalidClient,
InvalidGrant,
UnauthorizedClient,
UnsupportedGrantType,
InvalidScope,
AccessDenied,
UnsupportedResponseType,
ServerError,
TemporarilyUnavailable,
InvalidToken,
InsufficientScope,
InvalidTarget,
InvalidRedirectUri,
InvalidClientMetadata,
InvalidSoftwareStatement,
UnapprovedSoftwareStatement,
Other(String),
}
impl OAuthErrorCode {
pub fn as_str(&self) -> &str {
match self {
OAuthErrorCode::InvalidRequest => "invalid_request",
OAuthErrorCode::InvalidClient => "invalid_client",
OAuthErrorCode::InvalidGrant => "invalid_grant",
OAuthErrorCode::UnauthorizedClient => "unauthorized_client",
OAuthErrorCode::UnsupportedGrantType => "unsupported_grant_type",
OAuthErrorCode::InvalidScope => "invalid_scope",
OAuthErrorCode::AccessDenied => "access_denied",
OAuthErrorCode::UnsupportedResponseType => "unsupported_response_type",
OAuthErrorCode::ServerError => "server_error",
OAuthErrorCode::TemporarilyUnavailable => "temporarily_unavailable",
OAuthErrorCode::InvalidToken => "invalid_token",
OAuthErrorCode::InsufficientScope => "insufficient_scope",
OAuthErrorCode::InvalidTarget => "invalid_target",
OAuthErrorCode::InvalidRedirectUri => "invalid_redirect_uri",
OAuthErrorCode::InvalidClientMetadata => "invalid_client_metadata",
OAuthErrorCode::InvalidSoftwareStatement => "invalid_software_statement",
OAuthErrorCode::UnapprovedSoftwareStatement => "unapproved_software_statement",
OAuthErrorCode::Other(code) => code,
}
}
pub fn status(&self) -> StatusCode {
match self {
OAuthErrorCode::InvalidToken | OAuthErrorCode::InvalidClient => {
StatusCode::UNAUTHORIZED
}
OAuthErrorCode::InsufficientScope | OAuthErrorCode::AccessDenied => {
StatusCode::FORBIDDEN
}
OAuthErrorCode::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
OAuthErrorCode::TemporarilyUnavailable => StatusCode::SERVICE_UNAVAILABLE,
_ => StatusCode::BAD_REQUEST,
}
}
fn from_known(code: &str) -> Option<Self> {
let known = match code {
"invalid_request" => OAuthErrorCode::InvalidRequest,
"invalid_client" => OAuthErrorCode::InvalidClient,
"invalid_grant" => OAuthErrorCode::InvalidGrant,
"unauthorized_client" => OAuthErrorCode::UnauthorizedClient,
"unsupported_grant_type" => OAuthErrorCode::UnsupportedGrantType,
"invalid_scope" => OAuthErrorCode::InvalidScope,
"access_denied" => OAuthErrorCode::AccessDenied,
"unsupported_response_type" => OAuthErrorCode::UnsupportedResponseType,
"server_error" => OAuthErrorCode::ServerError,
"temporarily_unavailable" => OAuthErrorCode::TemporarilyUnavailable,
"invalid_token" => OAuthErrorCode::InvalidToken,
"insufficient_scope" => OAuthErrorCode::InsufficientScope,
"invalid_target" => OAuthErrorCode::InvalidTarget,
"invalid_redirect_uri" => OAuthErrorCode::InvalidRedirectUri,
"invalid_client_metadata" => OAuthErrorCode::InvalidClientMetadata,
"invalid_software_statement" => OAuthErrorCode::InvalidSoftwareStatement,
"unapproved_software_statement" => OAuthErrorCode::UnapprovedSoftwareStatement,
_ => return None,
};
Some(known)
}
}
impl From<&str> for OAuthErrorCode {
#[inline]
fn from(code: &str) -> Self {
Self::from_known(code).unwrap_or_else(|| OAuthErrorCode::Other(code.into()))
}
}
impl From<String> for OAuthErrorCode {
#[inline]
fn from(code: String) -> Self {
Self::from_known(&code).unwrap_or(OAuthErrorCode::Other(code))
}
}
impl From<OAuthErrorCode> for String {
#[inline]
fn from(code: OAuthErrorCode) -> Self {
match code {
OAuthErrorCode::Other(code) => code,
known => known.as_str().into(),
}
}
}
impl Display for OAuthErrorCode {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OAuthError {
pub error: OAuthErrorCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_uri: Option<String>,
}
impl OAuthError {
pub fn new(error: OAuthErrorCode) -> Self {
Self {
error,
error_description: None,
error_uri: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.error_description = Some(description.into());
self
}
pub fn with_error_uri(mut self, uri: impl Into<String>) -> Self {
self.error_uri = Some(uri.into());
self
}
}
impl From<OAuthErrorCode> for OAuthError {
#[inline]
fn from(error: OAuthErrorCode) -> Self {
Self::new(error)
}
}
impl Display for OAuthError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.error_description {
Some(desc) => write!(f, "{}: {desc}", self.error),
None => Display::fmt(&self.error, f),
}
}
}
impl std::error::Error for OAuthError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_maps_known_codes_to_wire_form() {
let cases = [
(OAuthErrorCode::InvalidRequest, "invalid_request"),
(OAuthErrorCode::InvalidClient, "invalid_client"),
(OAuthErrorCode::InvalidGrant, "invalid_grant"),
(OAuthErrorCode::UnauthorizedClient, "unauthorized_client"),
(
OAuthErrorCode::UnsupportedGrantType,
"unsupported_grant_type",
),
(OAuthErrorCode::InvalidScope, "invalid_scope"),
(OAuthErrorCode::AccessDenied, "access_denied"),
(
OAuthErrorCode::UnsupportedResponseType,
"unsupported_response_type",
),
(OAuthErrorCode::ServerError, "server_error"),
(
OAuthErrorCode::TemporarilyUnavailable,
"temporarily_unavailable",
),
(OAuthErrorCode::InvalidToken, "invalid_token"),
(OAuthErrorCode::InsufficientScope, "insufficient_scope"),
(OAuthErrorCode::InvalidTarget, "invalid_target"),
(OAuthErrorCode::InvalidRedirectUri, "invalid_redirect_uri"),
(
OAuthErrorCode::InvalidClientMetadata,
"invalid_client_metadata",
),
(
OAuthErrorCode::InvalidSoftwareStatement,
"invalid_software_statement",
),
(
OAuthErrorCode::UnapprovedSoftwareStatement,
"unapproved_software_statement",
),
];
for (code, wire) in cases {
assert_eq!(code.as_str(), wire);
assert_eq!(OAuthErrorCode::from(wire), code);
assert_eq!(OAuthErrorCode::from(wire.to_string()), code);
}
}
#[test]
fn it_preserves_unknown_codes() {
let code = OAuthErrorCode::from("use_dpop_nonce");
assert_eq!(code, OAuthErrorCode::Other("use_dpop_nonce".into()));
assert_eq!(code.as_str(), "use_dpop_nonce");
assert_eq!(String::from(code), "use_dpop_nonce");
}
#[test]
fn it_serializes_code_as_string() {
let json = serde_json::to_string(&OAuthErrorCode::InvalidToken).unwrap();
assert_eq!(json, r#""invalid_token""#);
}
#[test]
fn it_deserializes_code_from_string() {
let code: OAuthErrorCode = serde_json::from_str(r#""insufficient_scope""#).unwrap();
assert_eq!(code, OAuthErrorCode::InsufficientScope);
let code: OAuthErrorCode = serde_json::from_str(r#""something_custom""#).unwrap();
assert_eq!(code, OAuthErrorCode::Other("something_custom".into()));
}
#[test]
fn it_displays_code() {
assert_eq!(
OAuthErrorCode::TemporarilyUnavailable.to_string(),
"temporarily_unavailable"
);
}
#[test]
fn it_serializes_error_without_optional_fields() {
let err = OAuthError::new(OAuthErrorCode::InvalidGrant);
let json = serde_json::to_string(&err).unwrap();
assert_eq!(json, r#"{"error":"invalid_grant"}"#);
}
#[test]
fn it_serializes_error_with_all_fields() {
let err = OAuthError::new(OAuthErrorCode::InvalidRequest)
.with_description("Missing code_verifier")
.with_error_uri("https://example.com/errors/invalid_request");
let json = serde_json::to_string(&err).unwrap();
assert_eq!(
json,
r#"{"error":"invalid_request","error_description":"Missing code_verifier","error_uri":"https://example.com/errors/invalid_request"}"#
);
}
#[test]
fn it_deserializes_error_response() {
let err: OAuthError = serde_json::from_str(
r#"{"error":"invalid_token","error_description":"Token has expired"}"#,
)
.unwrap();
assert_eq!(err.error, OAuthErrorCode::InvalidToken);
assert_eq!(err.error_description.as_deref(), Some("Token has expired"));
assert!(err.error_uri.is_none());
}
#[test]
fn it_displays_error_with_and_without_description() {
let err = OAuthError::new(OAuthErrorCode::InvalidToken);
assert_eq!(err.to_string(), "invalid_token");
let err = err.with_description("Token has expired");
assert_eq!(err.to_string(), "invalid_token: Token has expired");
}
#[test]
fn it_converts_code_into_error() {
let err: OAuthError = OAuthErrorCode::AccessDenied.into();
assert_eq!(err.error, OAuthErrorCode::AccessDenied);
assert!(err.error_description.is_none());
}
#[test]
fn it_maps_codes_to_status() {
let cases = [
(OAuthErrorCode::InvalidToken, StatusCode::UNAUTHORIZED),
(OAuthErrorCode::InvalidClient, StatusCode::UNAUTHORIZED),
(OAuthErrorCode::InsufficientScope, StatusCode::FORBIDDEN),
(OAuthErrorCode::AccessDenied, StatusCode::FORBIDDEN),
(
OAuthErrorCode::ServerError,
StatusCode::INTERNAL_SERVER_ERROR,
),
(
OAuthErrorCode::TemporarilyUnavailable,
StatusCode::SERVICE_UNAVAILABLE,
),
(OAuthErrorCode::InvalidRequest, StatusCode::BAD_REQUEST),
(OAuthErrorCode::InvalidGrant, StatusCode::BAD_REQUEST),
(OAuthErrorCode::InvalidTarget, StatusCode::BAD_REQUEST),
(
OAuthErrorCode::Other("custom".into()),
StatusCode::BAD_REQUEST,
),
];
for (code, status) in cases {
assert_eq!(code.status(), status, "code: {code}");
}
}
}