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