1use http::StatusCode;
8use serde::{Deserialize, Serialize};
9use std::fmt::{Display, Formatter};
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(from = "String", into = "String")]
28#[non_exhaustive]
29pub enum OAuthErrorCode {
30 InvalidRequest,
33 InvalidClient,
35 InvalidGrant,
37 UnauthorizedClient,
39 UnsupportedGrantType,
41 InvalidScope,
43 AccessDenied,
45 UnsupportedResponseType,
47 ServerError,
49 TemporarilyUnavailable,
51 InvalidToken,
53 InsufficientScope,
55 InvalidTarget,
57 InvalidRedirectUri,
59 InvalidClientMetadata,
61 InvalidSoftwareStatement,
63 UnapprovedSoftwareStatement,
66 Other(String),
68}
69
70impl OAuthErrorCode {
71 pub fn as_str(&self) -> &str {
73 match self {
74 OAuthErrorCode::InvalidRequest => "invalid_request",
75 OAuthErrorCode::InvalidClient => "invalid_client",
76 OAuthErrorCode::InvalidGrant => "invalid_grant",
77 OAuthErrorCode::UnauthorizedClient => "unauthorized_client",
78 OAuthErrorCode::UnsupportedGrantType => "unsupported_grant_type",
79 OAuthErrorCode::InvalidScope => "invalid_scope",
80 OAuthErrorCode::AccessDenied => "access_denied",
81 OAuthErrorCode::UnsupportedResponseType => "unsupported_response_type",
82 OAuthErrorCode::ServerError => "server_error",
83 OAuthErrorCode::TemporarilyUnavailable => "temporarily_unavailable",
84 OAuthErrorCode::InvalidToken => "invalid_token",
85 OAuthErrorCode::InsufficientScope => "insufficient_scope",
86 OAuthErrorCode::InvalidTarget => "invalid_target",
87 OAuthErrorCode::InvalidRedirectUri => "invalid_redirect_uri",
88 OAuthErrorCode::InvalidClientMetadata => "invalid_client_metadata",
89 OAuthErrorCode::InvalidSoftwareStatement => "invalid_software_statement",
90 OAuthErrorCode::UnapprovedSoftwareStatement => "unapproved_software_statement",
91 OAuthErrorCode::Other(code) => code,
92 }
93 }
94
95 pub fn status(&self) -> StatusCode {
103 match self {
104 OAuthErrorCode::InvalidToken | OAuthErrorCode::InvalidClient => {
105 StatusCode::UNAUTHORIZED
106 }
107 OAuthErrorCode::InsufficientScope | OAuthErrorCode::AccessDenied => {
108 StatusCode::FORBIDDEN
109 }
110 OAuthErrorCode::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
111 OAuthErrorCode::TemporarilyUnavailable => StatusCode::SERVICE_UNAVAILABLE,
112 _ => StatusCode::BAD_REQUEST,
113 }
114 }
115
116 fn from_known(code: &str) -> Option<Self> {
118 let known = match code {
119 "invalid_request" => OAuthErrorCode::InvalidRequest,
120 "invalid_client" => OAuthErrorCode::InvalidClient,
121 "invalid_grant" => OAuthErrorCode::InvalidGrant,
122 "unauthorized_client" => OAuthErrorCode::UnauthorizedClient,
123 "unsupported_grant_type" => OAuthErrorCode::UnsupportedGrantType,
124 "invalid_scope" => OAuthErrorCode::InvalidScope,
125 "access_denied" => OAuthErrorCode::AccessDenied,
126 "unsupported_response_type" => OAuthErrorCode::UnsupportedResponseType,
127 "server_error" => OAuthErrorCode::ServerError,
128 "temporarily_unavailable" => OAuthErrorCode::TemporarilyUnavailable,
129 "invalid_token" => OAuthErrorCode::InvalidToken,
130 "insufficient_scope" => OAuthErrorCode::InsufficientScope,
131 "invalid_target" => OAuthErrorCode::InvalidTarget,
132 "invalid_redirect_uri" => OAuthErrorCode::InvalidRedirectUri,
133 "invalid_client_metadata" => OAuthErrorCode::InvalidClientMetadata,
134 "invalid_software_statement" => OAuthErrorCode::InvalidSoftwareStatement,
135 "unapproved_software_statement" => OAuthErrorCode::UnapprovedSoftwareStatement,
136 _ => return None,
137 };
138 Some(known)
139 }
140}
141
142impl From<&str> for OAuthErrorCode {
143 #[inline]
144 fn from(code: &str) -> Self {
145 Self::from_known(code).unwrap_or_else(|| OAuthErrorCode::Other(code.into()))
146 }
147}
148
149impl From<String> for OAuthErrorCode {
150 #[inline]
151 fn from(code: String) -> Self {
152 Self::from_known(&code).unwrap_or(OAuthErrorCode::Other(code))
153 }
154}
155
156impl From<OAuthErrorCode> for String {
157 #[inline]
158 fn from(code: OAuthErrorCode) -> Self {
159 match code {
160 OAuthErrorCode::Other(code) => code,
161 known => known.as_str().into(),
162 }
163 }
164}
165
166impl Display for OAuthErrorCode {
167 #[inline]
168 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169 f.write_str(self.as_str())
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
182pub struct OAuthError {
183 pub error: OAuthErrorCode,
185
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub error_description: Option<String>,
189
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub error_uri: Option<String>,
193}
194
195impl OAuthError {
196 pub fn new(error: OAuthErrorCode) -> Self {
198 Self {
199 error,
200 error_description: None,
201 error_uri: None,
202 }
203 }
204
205 pub fn with_description(mut self, description: impl Into<String>) -> Self {
207 self.error_description = Some(description.into());
208 self
209 }
210
211 pub fn with_error_uri(mut self, uri: impl Into<String>) -> Self {
213 self.error_uri = Some(uri.into());
214 self
215 }
216}
217
218impl From<OAuthErrorCode> for OAuthError {
219 #[inline]
220 fn from(error: OAuthErrorCode) -> Self {
221 Self::new(error)
222 }
223}
224
225impl Display for OAuthError {
226 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
227 match &self.error_description {
228 Some(desc) => write!(f, "{}: {desc}", self.error),
229 None => Display::fmt(&self.error, f),
230 }
231 }
232}
233
234impl std::error::Error for OAuthError {}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn it_maps_known_codes_to_wire_form() {
242 let cases = [
243 (OAuthErrorCode::InvalidRequest, "invalid_request"),
244 (OAuthErrorCode::InvalidClient, "invalid_client"),
245 (OAuthErrorCode::InvalidGrant, "invalid_grant"),
246 (OAuthErrorCode::UnauthorizedClient, "unauthorized_client"),
247 (
248 OAuthErrorCode::UnsupportedGrantType,
249 "unsupported_grant_type",
250 ),
251 (OAuthErrorCode::InvalidScope, "invalid_scope"),
252 (OAuthErrorCode::AccessDenied, "access_denied"),
253 (
254 OAuthErrorCode::UnsupportedResponseType,
255 "unsupported_response_type",
256 ),
257 (OAuthErrorCode::ServerError, "server_error"),
258 (
259 OAuthErrorCode::TemporarilyUnavailable,
260 "temporarily_unavailable",
261 ),
262 (OAuthErrorCode::InvalidToken, "invalid_token"),
263 (OAuthErrorCode::InsufficientScope, "insufficient_scope"),
264 (OAuthErrorCode::InvalidTarget, "invalid_target"),
265 (OAuthErrorCode::InvalidRedirectUri, "invalid_redirect_uri"),
266 (
267 OAuthErrorCode::InvalidClientMetadata,
268 "invalid_client_metadata",
269 ),
270 (
271 OAuthErrorCode::InvalidSoftwareStatement,
272 "invalid_software_statement",
273 ),
274 (
275 OAuthErrorCode::UnapprovedSoftwareStatement,
276 "unapproved_software_statement",
277 ),
278 ];
279 for (code, wire) in cases {
280 assert_eq!(code.as_str(), wire);
281 assert_eq!(OAuthErrorCode::from(wire), code);
282 assert_eq!(OAuthErrorCode::from(wire.to_string()), code);
283 }
284 }
285
286 #[test]
287 fn it_preserves_unknown_codes() {
288 let code = OAuthErrorCode::from("use_dpop_nonce");
289 assert_eq!(code, OAuthErrorCode::Other("use_dpop_nonce".into()));
290 assert_eq!(code.as_str(), "use_dpop_nonce");
291 assert_eq!(String::from(code), "use_dpop_nonce");
292 }
293
294 #[test]
295 fn it_serializes_code_as_string() {
296 let json = serde_json::to_string(&OAuthErrorCode::InvalidToken).unwrap();
297 assert_eq!(json, r#""invalid_token""#);
298 }
299
300 #[test]
301 fn it_deserializes_code_from_string() {
302 let code: OAuthErrorCode = serde_json::from_str(r#""insufficient_scope""#).unwrap();
303 assert_eq!(code, OAuthErrorCode::InsufficientScope);
304
305 let code: OAuthErrorCode = serde_json::from_str(r#""something_custom""#).unwrap();
306 assert_eq!(code, OAuthErrorCode::Other("something_custom".into()));
307 }
308
309 #[test]
310 fn it_displays_code() {
311 assert_eq!(
312 OAuthErrorCode::TemporarilyUnavailable.to_string(),
313 "temporarily_unavailable"
314 );
315 }
316
317 #[test]
318 fn it_serializes_error_without_optional_fields() {
319 let err = OAuthError::new(OAuthErrorCode::InvalidGrant);
320 let json = serde_json::to_string(&err).unwrap();
321 assert_eq!(json, r#"{"error":"invalid_grant"}"#);
322 }
323
324 #[test]
325 fn it_serializes_error_with_all_fields() {
326 let err = OAuthError::new(OAuthErrorCode::InvalidRequest)
327 .with_description("Missing code_verifier")
328 .with_error_uri("https://example.com/errors/invalid_request");
329 let json = serde_json::to_string(&err).unwrap();
330 assert_eq!(
331 json,
332 r#"{"error":"invalid_request","error_description":"Missing code_verifier","error_uri":"https://example.com/errors/invalid_request"}"#
333 );
334 }
335
336 #[test]
337 fn it_deserializes_error_response() {
338 let err: OAuthError = serde_json::from_str(
339 r#"{"error":"invalid_token","error_description":"Token has expired"}"#,
340 )
341 .unwrap();
342 assert_eq!(err.error, OAuthErrorCode::InvalidToken);
343 assert_eq!(err.error_description.as_deref(), Some("Token has expired"));
344 assert!(err.error_uri.is_none());
345 }
346
347 #[test]
348 fn it_displays_error_with_and_without_description() {
349 let err = OAuthError::new(OAuthErrorCode::InvalidToken);
350 assert_eq!(err.to_string(), "invalid_token");
351
352 let err = err.with_description("Token has expired");
353 assert_eq!(err.to_string(), "invalid_token: Token has expired");
354 }
355
356 #[test]
357 fn it_converts_code_into_error() {
358 let err: OAuthError = OAuthErrorCode::AccessDenied.into();
359 assert_eq!(err.error, OAuthErrorCode::AccessDenied);
360 assert!(err.error_description.is_none());
361 }
362
363 #[test]
364 fn it_maps_codes_to_status() {
365 let cases = [
366 (OAuthErrorCode::InvalidToken, StatusCode::UNAUTHORIZED),
367 (OAuthErrorCode::InvalidClient, StatusCode::UNAUTHORIZED),
368 (OAuthErrorCode::InsufficientScope, StatusCode::FORBIDDEN),
369 (OAuthErrorCode::AccessDenied, StatusCode::FORBIDDEN),
370 (
371 OAuthErrorCode::ServerError,
372 StatusCode::INTERNAL_SERVER_ERROR,
373 ),
374 (
375 OAuthErrorCode::TemporarilyUnavailable,
376 StatusCode::SERVICE_UNAVAILABLE,
377 ),
378 (OAuthErrorCode::InvalidRequest, StatusCode::BAD_REQUEST),
379 (OAuthErrorCode::InvalidGrant, StatusCode::BAD_REQUEST),
380 (OAuthErrorCode::InvalidTarget, StatusCode::BAD_REQUEST),
381 (
382 OAuthErrorCode::Other("custom".into()),
383 StatusCode::BAD_REQUEST,
384 ),
385 ];
386 for (code, status) in cases {
387 assert_eq!(code.status(), status, "code: {code}");
388 }
389 }
390}