bitwarden_auth/login/api/response/
login_error_api_response.rs1use serde::Deserialize;
2
3#[derive(Deserialize, PartialEq, Eq, Debug)]
4#[serde(rename_all = "snake_case")]
5pub enum PasswordInvalidGrantError {
6 InvalidUsernameOrPassword,
8}
9
10#[derive(Deserialize, PartialEq, Eq, Debug)]
45#[serde(untagged)]
46pub enum InvalidGrantError {
47 Password(PasswordInvalidGrantError),
49
50 Unknown(String),
55}
56
57#[derive(Deserialize, PartialEq, Eq, Debug)]
60#[serde(rename_all = "snake_case")]
61#[serde(tag = "error")]
62pub enum OAuth2ErrorApiResponse {
63 InvalidRequest {
66 #[serde(default)]
70 error_description: Option<String>,
71 },
72
73 InvalidGrant {
75 #[serde(default)]
76 error_description: Option<InvalidGrantError>,
78 },
79
80 InvalidClient {
82 #[serde(default)]
83 error_description: Option<String>,
85 },
86
87 UnauthorizedClient {
89 #[serde(default)]
90 error_description: Option<String>,
92 },
93
94 UnsupportedGrantType {
96 #[serde(default)]
97 error_description: Option<String>,
99 },
100
101 InvalidScope {
103 #[serde(default)]
104 error_description: Option<String>,
106 },
107
108 InvalidTarget {
111 #[serde(default)]
112 error_description: Option<String>,
114 },
115}
116
117#[derive(Deserialize, PartialEq, Eq, Debug)]
118#[serde(untagged)]
122pub enum LoginErrorApiResponse {
123 OAuth2Error(OAuth2ErrorApiResponse),
124 UnexpectedError(String),
125}
126
127impl<T: std::error::Error> From<T> for LoginErrorApiResponse {
129 fn from(value: T) -> Self {
130 Self::UnexpectedError(format!("{value:?}"))
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 const ERROR_INVALID_USERNAME_OR_PASSWORD: &str = "invalid_username_or_password";
140 const ERROR_TYPE_INVALID_GRANT: &str = "invalid_grant";
141
142 mod invalid_grant_error_tests {
143 use serde_json::{from_str, json};
144
145 use super::*;
146
147 #[test]
148 fn password_invalid_username_or_password_deserializes() {
149 let json = format!(r#""{ERROR_INVALID_USERNAME_OR_PASSWORD}""#);
150 let error: InvalidGrantError = from_str(&json).unwrap();
151 assert_eq!(
152 error,
153 InvalidGrantError::Password(PasswordInvalidGrantError::InvalidUsernameOrPassword)
154 );
155 }
156
157 #[test]
158 fn unknown_error_description_maps_to_unknown() {
159 let json = r#""some_new_error_code""#;
160 let error: InvalidGrantError = from_str(json).unwrap();
161 assert_eq!(
162 error,
163 InvalidGrantError::Unknown("some_new_error_code".to_string())
164 );
165 }
166
167 #[test]
168 fn full_invalid_grant_response_with_invalid_username_or_password() {
169 let payload = json!({
170 "error": ERROR_TYPE_INVALID_GRANT,
171 "error_description": ERROR_INVALID_USERNAME_OR_PASSWORD
172 })
173 .to_string();
174
175 let parsed: OAuth2ErrorApiResponse = from_str(&payload).unwrap();
176 match parsed {
177 OAuth2ErrorApiResponse::InvalidGrant { error_description } => {
178 assert_eq!(
179 error_description,
180 Some(InvalidGrantError::Password(
181 PasswordInvalidGrantError::InvalidUsernameOrPassword
182 ))
183 );
184 }
185 _ => panic!("expected invalid_grant"),
186 }
187 }
188
189 #[test]
190 fn invalid_grant_without_error_description_is_allowed() {
191 let payload = json!({ "error": ERROR_TYPE_INVALID_GRANT }).to_string();
192 let parsed: OAuth2ErrorApiResponse = from_str(&payload).unwrap();
193 match parsed {
194 OAuth2ErrorApiResponse::InvalidGrant { error_description } => {
195 assert!(error_description.is_none());
196 }
197 _ => panic!("expected invalid_grant"),
198 }
199 }
200
201 #[test]
202 fn invalid_grant_null_error_description_becomes_none() {
203 let payload = json!({
204 "error": ERROR_TYPE_INVALID_GRANT,
205 "error_description": null
206 })
207 .to_string();
208
209 let parsed: OAuth2ErrorApiResponse = from_str(&payload).unwrap();
210 match parsed {
211 OAuth2ErrorApiResponse::InvalidGrant { error_description } => {
212 assert!(error_description.is_none());
213 }
214 _ => panic!("expected invalid_grant"),
215 }
216 }
217
218 #[test]
219 fn invalid_grant_with_unknown_error_description() {
220 let payload = json!({
221 "error": ERROR_TYPE_INVALID_GRANT,
222 "error_description": "brand_new_error_type"
223 })
224 .to_string();
225
226 let parsed: OAuth2ErrorApiResponse = from_str(&payload).unwrap();
227 match parsed {
228 OAuth2ErrorApiResponse::InvalidGrant { error_description } => {
229 assert_eq!(
230 error_description,
231 Some(InvalidGrantError::Unknown(
232 "brand_new_error_type".to_string()
233 ))
234 );
235 }
236 _ => panic!("expected invalid_grant"),
237 }
238 }
239 }
240
241 mod login_error_api_response_tests {
242 use serde_json::{from_str, json};
243
244 use super::*;
245
246 #[test]
247 fn full_server_response_with_error_model_deserializes() {
248 let payload = json!({
251 "error": ERROR_TYPE_INVALID_GRANT,
252 "error_description": ERROR_INVALID_USERNAME_OR_PASSWORD,
253 "ErrorModel": {
254 "Message": "Username or password is incorrect. Try again.",
255 "Object": "error"
256 }
257 })
258 .to_string();
259
260 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
261 match parsed {
262 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
263 error_description,
264 }) => {
265 assert_eq!(
266 error_description,
267 Some(InvalidGrantError::Password(
268 PasswordInvalidGrantError::InvalidUsernameOrPassword
269 ))
270 );
271 }
272 _ => panic!("expected OAuth2Error(InvalidGrant)"),
273 }
274 }
275
276 #[test]
277 fn oauth2_error_without_error_model_deserializes() {
278 let payload = json!({
279 "error": ERROR_TYPE_INVALID_GRANT,
280 "error_description": ERROR_INVALID_USERNAME_OR_PASSWORD
281 })
282 .to_string();
283
284 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
285 match parsed {
286 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
287 error_description,
288 }) => {
289 assert_eq!(
290 error_description,
291 Some(InvalidGrantError::Password(
292 PasswordInvalidGrantError::InvalidUsernameOrPassword
293 ))
294 );
295 }
296 _ => panic!("expected OAuth2Error(InvalidGrant)"),
297 }
298 }
299
300 #[test]
301 fn invalid_request_error_deserializes() {
302 let payload = json!({
303 "error": "invalid_request",
304 "error_description": "password is required"
305 })
306 .to_string();
307
308 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
309 match parsed {
310 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidRequest {
311 error_description,
312 }) => {
313 assert_eq!(error_description.as_deref(), Some("password is required"));
314 }
315 _ => panic!("expected OAuth2Error(InvalidRequest)"),
316 }
317 }
318
319 #[test]
320 fn invalid_client_error_deserializes() {
321 let payload = json!({
322 "error": "invalid_client",
323 "error_description": "Invalid client credentials"
324 })
325 .to_string();
326
327 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
328 match parsed {
329 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidClient {
330 error_description,
331 }) => {
332 assert_eq!(
333 error_description.as_deref(),
334 Some("Invalid client credentials")
335 );
336 }
337 _ => panic!("expected OAuth2Error(InvalidClient)"),
338 }
339 }
340
341 #[test]
342 fn unauthorized_client_error_deserializes() {
343 let payload = json!({
344 "error": "unauthorized_client"
345 })
346 .to_string();
347
348 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
349 match parsed {
350 LoginErrorApiResponse::OAuth2Error(
351 OAuth2ErrorApiResponse::UnauthorizedClient { error_description },
352 ) => {
353 assert!(error_description.is_none());
354 }
355 _ => panic!("expected OAuth2Error(UnauthorizedClient)"),
356 }
357 }
358
359 #[test]
360 fn unsupported_grant_type_error_deserializes() {
361 let payload = json!({
362 "error": "unsupported_grant_type",
363 "error_description": "This grant type is not supported"
364 })
365 .to_string();
366
367 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
368 match parsed {
369 LoginErrorApiResponse::OAuth2Error(
370 OAuth2ErrorApiResponse::UnsupportedGrantType { error_description },
371 ) => {
372 assert_eq!(
373 error_description.as_deref(),
374 Some("This grant type is not supported")
375 );
376 }
377 _ => panic!("expected OAuth2Error(UnsupportedGrantType)"),
378 }
379 }
380
381 #[test]
382 fn invalid_scope_error_deserializes() {
383 let payload = json!({
384 "error": "invalid_scope"
385 })
386 .to_string();
387
388 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
389 match parsed {
390 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidScope {
391 error_description,
392 }) => {
393 assert!(error_description.is_none());
394 }
395 _ => panic!("expected OAuth2Error(InvalidScope)"),
396 }
397 }
398
399 #[test]
400 fn invalid_target_error_deserializes() {
401 let payload = json!({
402 "error": "invalid_target",
403 "error_description": "Resource not found"
404 })
405 .to_string();
406
407 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
408 match parsed {
409 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidTarget {
410 error_description,
411 }) => {
412 assert_eq!(error_description.as_deref(), Some("Resource not found"));
413 }
414 _ => panic!("expected OAuth2Error(InvalidTarget)"),
415 }
416 }
417
418 #[test]
419 fn missing_or_null_error_description_deserializes_to_none() {
420 let test_cases = vec![
422 json!({ "error": ERROR_TYPE_INVALID_GRANT }),
423 json!({ "error": ERROR_TYPE_INVALID_GRANT, "error_description": null }),
424 ];
425
426 for payload in test_cases {
427 let parsed: LoginErrorApiResponse = from_str(&payload.to_string()).unwrap();
428 match parsed {
429 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
430 error_description,
431 }) => {
432 assert!(error_description.is_none());
433 }
434 _ => panic!("expected OAuth2Error(InvalidGrant)"),
435 }
436 }
437 }
438
439 #[test]
440 fn unknown_error_description_value_maps_to_unknown() {
441 let payload = json!({
442 "error": ERROR_TYPE_INVALID_GRANT,
443 "error_description": "some_future_error_code"
444 })
445 .to_string();
446
447 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
448 match parsed {
449 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
450 error_description,
451 }) => {
452 assert_eq!(
453 error_description,
454 Some(InvalidGrantError::Unknown(
455 "some_future_error_code".to_string()
456 ))
457 );
458 }
459 _ => panic!("expected OAuth2Error(InvalidGrant)"),
460 }
461 }
462
463 #[test]
464 fn error_with_extra_fields_ignores_them() {
465 let payload = json!({
466 "error": ERROR_TYPE_INVALID_GRANT,
467 "error_description": ERROR_INVALID_USERNAME_OR_PASSWORD,
468 "extra_field": "should be ignored",
469 "another_field": 123,
470 "ErrorModel": {
471 "Message": "Some message",
472 "Object": "error"
473 }
474 })
475 .to_string();
476
477 let parsed: LoginErrorApiResponse = from_str(&payload).unwrap();
478 match parsed {
479 LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
480 error_description,
481 }) => {
482 assert_eq!(
483 error_description,
484 Some(InvalidGrantError::Password(
485 PasswordInvalidGrantError::InvalidUsernameOrPassword
486 ))
487 );
488 }
489 _ => panic!("expected OAuth2Error(InvalidGrant)"),
490 }
491 }
492 }
493}