bitwarden_auth/login/login_via_password/
password_login_error.rs

1use bitwarden_core::key_management::MasterPasswordError;
2use bitwarden_error::bitwarden_error;
3use thiserror::Error;
4
5use crate::login::api::response::{
6    InvalidGrantError, LoginErrorApiResponse, OAuth2ErrorApiResponse, PasswordInvalidGrantError,
7};
8
9/// Errors that can occur during password-based login.
10///
11/// This enum covers errors specific to the password authentication flow, including
12/// credential validation, KDF processing, and API communication errors.
13#[bitwarden_error(flat)]
14#[derive(Debug, Error)]
15pub enum PasswordLoginError {
16    /// The username (email) or password provided was invalid.
17    ///
18    /// This error is returned by the server when:
19    /// - The email address doesn't exist in the system
20    /// - The master password hash doesn't match the stored hash
21    ///
22    /// # Note
23    /// For security reasons, the server doesn't distinguish between "user not found"
24    /// and "wrong password" to prevent user enumeration attacks.
25    #[error("Invalid username or password provided.")]
26    InvalidUsernameOrPassword,
27
28    /// Failed to derive master password authentication data from the provided password and KDF
29    /// settings.
30    ///
31    /// This error can occur during local cryptographic processing before the API call when:
32    /// - The KDF parameters are invalid (e.g., iterations below minimum threshold)
33    /// - The KDF algorithm is unsupported or corrupted
34    /// - Memory allocation fails during Argon2id processing
35    #[error(transparent)]
36    PasswordAuthenticationDataDerivation(#[from] MasterPasswordError),
37
38    /// An unknown or unexpected error occurred during login.
39    ///
40    /// This variant captures errors that don't fit other categories, including:
41    /// - Unexpected OAuth2 error codes from the server
42    /// - Network errors (timeouts, connection refused, DNS failures)
43    /// - Malformed server responses
44    /// - Future error types not yet handled by this SDK version
45    ///
46    /// The contained string provides details about what went wrong.
47    ///
48    /// # Forward Compatibility
49    /// This variant ensures the SDK can handle new error types introduced by the server
50    /// without breaking existing client code.
51    #[error("Unknown password login error: {0}")]
52    Unknown(String),
53}
54
55// TODO: When adding 2FA support, consider how we can avoid having each login mechanism have to
56// implement a conversion for 2FA errors TODO: per discussion with Dani, investigate adding a
57// display property for each error variant that maps to unknown so we don't have to manually build
58// the string each time here and in each login mechanism error file.
59
60impl From<LoginErrorApiResponse> for PasswordLoginError {
61    fn from(error: LoginErrorApiResponse) -> Self {
62        match error {
63            LoginErrorApiResponse::OAuth2Error(oauth_error) => match oauth_error {
64                OAuth2ErrorApiResponse::InvalidGrant { error_description } => {
65                    match error_description {
66                        Some(InvalidGrantError::Password(
67                            PasswordInvalidGrantError::InvalidUsernameOrPassword,
68                        )) => Self::InvalidUsernameOrPassword,
69                        Some(InvalidGrantError::Unknown(error_code)) => {
70                            Self::Unknown(format!("Invalid grant - unknown error: {error_code}"))
71                        }
72                        None => {
73                            Self::Unknown("Invalid grant with no error description".to_string())
74                        }
75                    }
76                }
77                OAuth2ErrorApiResponse::InvalidRequest { error_description } => {
78                    let desc = error_description.unwrap_or("no error description".to_string());
79                    Self::Unknown(format!("Invalid request: {desc}"))
80                }
81                OAuth2ErrorApiResponse::InvalidClient { error_description } => {
82                    let desc = error_description.unwrap_or("no error description".to_string());
83                    Self::Unknown(format!("Invalid client: {desc}"))
84                }
85                OAuth2ErrorApiResponse::UnauthorizedClient { error_description } => {
86                    let desc = error_description.unwrap_or("no error description".to_string());
87                    Self::Unknown(format!("Unauthorized client: {desc}"))
88                }
89                OAuth2ErrorApiResponse::UnsupportedGrantType { error_description } => {
90                    let desc = error_description.unwrap_or("no error description".to_string());
91                    Self::Unknown(format!("Unsupported grant type: {desc}"))
92                }
93                OAuth2ErrorApiResponse::InvalidScope { error_description } => {
94                    let desc = error_description.unwrap_or("no error description".to_string());
95                    Self::Unknown(format!("Invalid scope: {desc}"))
96                }
97                OAuth2ErrorApiResponse::InvalidTarget { error_description } => {
98                    let desc = error_description.unwrap_or("no error description".to_string());
99                    Self::Unknown(format!("Invalid target: {desc}"))
100                }
101            },
102            LoginErrorApiResponse::UnexpectedError(msg) => {
103                Self::Unknown(format!("Unexpected error: {msg}"))
104            }
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    // Test constants for strings used multiple times
114    const ERROR_DESC_NO_DESCRIPTION: &str = "no error description";
115    const TEST_ERROR_DESC: &str = "Test error description";
116
117    mod from_login_error_api_response {
118        use super::*;
119
120        #[test]
121        fn invalid_grant_with_invalid_username_or_password() {
122            let api_error =
123                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
124                    error_description: Some(InvalidGrantError::Password(
125                        PasswordInvalidGrantError::InvalidUsernameOrPassword,
126                    )),
127                });
128
129            let result: PasswordLoginError = api_error.into();
130
131            assert!(matches!(
132                result,
133                PasswordLoginError::InvalidUsernameOrPassword
134            ));
135        }
136
137        #[test]
138        fn invalid_grant_with_unknown_error() {
139            let api_error =
140                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
141                    error_description: Some(InvalidGrantError::Unknown(
142                        "unknown_error_code".to_string(),
143                    )),
144                });
145
146            let result: PasswordLoginError = api_error.into();
147
148            match result {
149                PasswordLoginError::Unknown(msg) => {
150                    assert_eq!(msg, "Invalid grant - unknown error: unknown_error_code");
151                }
152                _ => panic!("Expected Unknown variant"),
153            }
154        }
155
156        #[test]
157        fn invalid_grant_with_no_error_description() {
158            let api_error =
159                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidGrant {
160                    error_description: None,
161                });
162
163            let result: PasswordLoginError = api_error.into();
164
165            match result {
166                PasswordLoginError::Unknown(msg) => {
167                    assert_eq!(msg, "Invalid grant with no error description");
168                }
169                _ => panic!("Expected Unknown variant"),
170            }
171        }
172
173        #[test]
174        fn invalid_request_with_error_description() {
175            let api_error =
176                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidRequest {
177                    error_description: Some(TEST_ERROR_DESC.to_string()),
178                });
179
180            let result: PasswordLoginError = api_error.into();
181
182            match result {
183                PasswordLoginError::Unknown(msg) => {
184                    assert_eq!(msg, format!("Invalid request: {TEST_ERROR_DESC}"));
185                }
186                _ => panic!("Expected Unknown variant"),
187            }
188        }
189
190        #[test]
191        fn invalid_request_without_error_description() {
192            let api_error =
193                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidRequest {
194                    error_description: None,
195                });
196
197            let result: PasswordLoginError = api_error.into();
198
199            match result {
200                PasswordLoginError::Unknown(msg) => {
201                    assert_eq!(msg, format!("Invalid request: {ERROR_DESC_NO_DESCRIPTION}"));
202                }
203                _ => panic!("Expected Unknown variant"),
204            }
205        }
206
207        #[test]
208        fn invalid_client_with_error_description() {
209            let api_error =
210                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidClient {
211                    error_description: Some(TEST_ERROR_DESC.to_string()),
212                });
213
214            let result: PasswordLoginError = api_error.into();
215
216            match result {
217                PasswordLoginError::Unknown(msg) => {
218                    assert_eq!(msg, format!("Invalid client: {TEST_ERROR_DESC}"));
219                }
220                _ => panic!("Expected Unknown variant"),
221            }
222        }
223
224        #[test]
225        fn invalid_client_without_error_description() {
226            let api_error =
227                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidClient {
228                    error_description: None,
229                });
230
231            let result: PasswordLoginError = api_error.into();
232
233            match result {
234                PasswordLoginError::Unknown(msg) => {
235                    assert_eq!(msg, format!("Invalid client: {ERROR_DESC_NO_DESCRIPTION}"));
236                }
237                _ => panic!("Expected Unknown variant"),
238            }
239        }
240
241        #[test]
242        fn unauthorized_client_with_error_description() {
243            let api_error =
244                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::UnauthorizedClient {
245                    error_description: Some(TEST_ERROR_DESC.to_string()),
246                });
247
248            let result: PasswordLoginError = api_error.into();
249
250            match result {
251                PasswordLoginError::Unknown(msg) => {
252                    assert_eq!(msg, format!("Unauthorized client: {TEST_ERROR_DESC}"));
253                }
254                _ => panic!("Expected Unknown variant"),
255            }
256        }
257
258        #[test]
259        fn unauthorized_client_without_error_description() {
260            let api_error =
261                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::UnauthorizedClient {
262                    error_description: None,
263                });
264
265            let result: PasswordLoginError = api_error.into();
266
267            match result {
268                PasswordLoginError::Unknown(msg) => {
269                    assert_eq!(
270                        msg,
271                        format!("Unauthorized client: {ERROR_DESC_NO_DESCRIPTION}")
272                    );
273                }
274                _ => panic!("Expected Unknown variant"),
275            }
276        }
277
278        #[test]
279        fn unsupported_grant_type_with_error_description() {
280            let api_error =
281                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::UnsupportedGrantType {
282                    error_description: Some(TEST_ERROR_DESC.to_string()),
283                });
284
285            let result: PasswordLoginError = api_error.into();
286
287            match result {
288                PasswordLoginError::Unknown(msg) => {
289                    assert_eq!(msg, format!("Unsupported grant type: {TEST_ERROR_DESC}"));
290                }
291                _ => panic!("Expected Unknown variant"),
292            }
293        }
294
295        #[test]
296        fn unsupported_grant_type_without_error_description() {
297            let api_error =
298                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::UnsupportedGrantType {
299                    error_description: None,
300                });
301
302            let result: PasswordLoginError = api_error.into();
303
304            match result {
305                PasswordLoginError::Unknown(msg) => {
306                    assert_eq!(
307                        msg,
308                        format!("Unsupported grant type: {ERROR_DESC_NO_DESCRIPTION}")
309                    );
310                }
311                _ => panic!("Expected Unknown variant"),
312            }
313        }
314
315        #[test]
316        fn invalid_scope_with_error_description() {
317            let api_error =
318                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidScope {
319                    error_description: Some(TEST_ERROR_DESC.to_string()),
320                });
321
322            let result: PasswordLoginError = api_error.into();
323
324            match result {
325                PasswordLoginError::Unknown(msg) => {
326                    assert_eq!(msg, format!("Invalid scope: {TEST_ERROR_DESC}"));
327                }
328                _ => panic!("Expected Unknown variant"),
329            }
330        }
331
332        #[test]
333        fn invalid_scope_without_error_description() {
334            let api_error =
335                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidScope {
336                    error_description: None,
337                });
338
339            let result: PasswordLoginError = api_error.into();
340
341            match result {
342                PasswordLoginError::Unknown(msg) => {
343                    assert_eq!(msg, format!("Invalid scope: {ERROR_DESC_NO_DESCRIPTION}"));
344                }
345                _ => panic!("Expected Unknown variant"),
346            }
347        }
348
349        #[test]
350        fn invalid_target_with_error_description() {
351            let api_error =
352                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidTarget {
353                    error_description: Some(TEST_ERROR_DESC.to_string()),
354                });
355
356            let result: PasswordLoginError = api_error.into();
357
358            match result {
359                PasswordLoginError::Unknown(msg) => {
360                    assert_eq!(msg, format!("Invalid target: {TEST_ERROR_DESC}"));
361                }
362                _ => panic!("Expected Unknown variant"),
363            }
364        }
365
366        #[test]
367        fn invalid_target_without_error_description() {
368            let api_error =
369                LoginErrorApiResponse::OAuth2Error(OAuth2ErrorApiResponse::InvalidTarget {
370                    error_description: None,
371                });
372
373            let result: PasswordLoginError = api_error.into();
374
375            match result {
376                PasswordLoginError::Unknown(msg) => {
377                    assert_eq!(msg, format!("Invalid target: {ERROR_DESC_NO_DESCRIPTION}"));
378                }
379                _ => panic!("Expected Unknown variant"),
380            }
381        }
382
383        #[test]
384        fn unexpected_error() {
385            let api_error = LoginErrorApiResponse::UnexpectedError("Network timeout".to_string());
386
387            let result: PasswordLoginError = api_error.into();
388
389            match result {
390                PasswordLoginError::Unknown(msg) => {
391                    assert_eq!(msg, "Unexpected error: Network timeout");
392                }
393                _ => panic!("Expected Unknown variant"),
394            }
395        }
396    }
397}