Skip to main content

bitwarden_auth/registration/
post_keys_for_jit_password_registration.rs

1//! Initializes a new cryptographic state for a user and posts it to the server;
2//! enrolls the user to master password unlock.
3use bitwarden_api_api::models::{
4    OrganizationUserResetPasswordEnrollmentRequestModel, SetInitialPasswordRequestModel,
5};
6use bitwarden_core::{
7    OrganizationId, UserId,
8    key_management::{
9        MasterPasswordUnlockData, account_cryptographic_state::WrappedAccountCryptographicState,
10    },
11};
12use bitwarden_encoding::B64;
13use tracing::{error, info};
14
15use crate::registration::{RegistrationClient, RegistrationError};
16
17/// Request parameters for SSO JIT master password registration.
18#[bitwarden_ffi::wasm_record]
19#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
20#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
21pub struct JitMasterPasswordRegistrationRequest {
22    /// Organization ID to enroll in
23    pub org_id: OrganizationId,
24    /// Organization's public key for encrypting the reset password key. This should be verified by
25    /// the client and not verifying may compromise the security of the user's account.
26    pub org_public_key: B64,
27    /// Organization SSO identifier
28    pub organization_sso_identifier: String,
29    /// User ID for the account being initialized
30    pub user_id: UserId,
31    /// Salt for master password hashing, usually email
32    pub salt: String,
33    /// Master password for the account
34    pub master_password: String,
35    /// Optional hint for the master password
36    pub master_password_hint: Option<String>,
37    /// Should enroll user into admin password reset
38    pub reset_password_enroll: bool,
39}
40
41/// Result of JIT master password registration process.
42#[bitwarden_ffi::wasm_record]
43#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
44#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
45pub struct JitMasterPasswordRegistrationResponse {
46    /// The account cryptographic state of the user
47    pub account_cryptographic_state: WrappedAccountCryptographicState,
48    /// The master password unlock data
49    pub master_password_unlock: MasterPasswordUnlockData,
50    /// The decrypted user key.
51    pub user_key: B64,
52}
53
54#[bitwarden_ffi::wasm_export]
55impl RegistrationClient {
56    /// Initializes a new cryptographic state for a user and posts it to the server;
57    /// enrolls the user to master password unlock.
58    pub async fn post_keys_for_jit_password_registration(
59        &self,
60        request: JitMasterPasswordRegistrationRequest,
61    ) -> Result<JitMasterPasswordRegistrationResponse, RegistrationError> {
62        let client = &self.client.internal;
63        let api_client = &client.get_api_configurations().api_client;
64        internal_post_keys_for_jit_password_registration(self, api_client, request).await
65    }
66}
67
68async fn internal_post_keys_for_jit_password_registration(
69    registration_client: &RegistrationClient,
70    api_client: &bitwarden_api_api::apis::ApiClient,
71    request: JitMasterPasswordRegistrationRequest,
72) -> Result<JitMasterPasswordRegistrationResponse, RegistrationError> {
73    // First call crypto API to get all keys
74    info!("Initializing account cryptography");
75    let registration_crypto_result = registration_client
76        .client
77        .crypto()
78        .make_user_jit_master_password_registration(
79            request.master_password,
80            request.salt,
81            request.org_public_key,
82        )
83        .map_err(|_| RegistrationError::Crypto)?;
84
85    // Post the generated keys to the API here. The user now has keys and is "registered", but
86    // has no unlock method.
87    let api_request = SetInitialPasswordRequestModel {
88        account_keys: Some(Box::new(
89            registration_crypto_result.account_keys_request.clone(),
90        )),
91        master_password_unlock: Some(Box::new(
92            (&registration_crypto_result.master_password_unlock_data).into(),
93        )),
94        master_password_authentication: Some(Box::new(
95            (&registration_crypto_result.master_password_authentication_data).into(),
96        )),
97        master_password_hint: request.master_password_hint,
98        org_identifier: request.organization_sso_identifier,
99        // TODO Deprecated fields below, to be removed with https://bitwarden.atlassian.net/browse/PM-27327
100        kdf_parallelism: None,
101        master_password_hash: None,
102        key: None,
103        keys: None,
104        kdf: None,
105        kdf_iterations: None,
106        kdf_memory: None,
107    };
108    info!("Posting user account cryptographic state to server");
109    api_client
110        .accounts_api()
111        .post_set_password(Some(api_request))
112        .await
113        .map_err(|e| {
114            error!("Failed to post account keys: {e:?}");
115            RegistrationError::Api
116        })?;
117
118    // Enroll the user for reset password using the reset password key generated above.
119    if request.reset_password_enroll {
120        info!("Enrolling into admin account recovery");
121        api_client
122            .organization_users_api()
123            .put_reset_password_enrollment(
124                request.org_id.into(),
125                request.user_id.into(),
126                Some(OrganizationUserResetPasswordEnrollmentRequestModel {
127                    reset_password_key: Some(
128                        registration_crypto_result.reset_password_key.to_string(),
129                    ),
130                    master_password_hash: Some(
131                        registration_crypto_result
132                            .master_password_authentication_data
133                            .master_password_authentication_hash
134                            .to_string(),
135                    ),
136                }),
137            )
138            .await
139            .map_err(|e| {
140                error!("Failed to enroll for reset password: {e:?}");
141                RegistrationError::Api
142            })?;
143    }
144
145    info!("User initialized!");
146    // Note: This passing out of state and keys is temporary. Once SDK state management is more
147    // mature, the account cryptographic state and keys should be set directly here.
148    Ok(JitMasterPasswordRegistrationResponse {
149        account_cryptographic_state: registration_crypto_result.account_cryptographic_state,
150        master_password_unlock: registration_crypto_result.master_password_unlock_data,
151        user_key: registration_crypto_result
152            .user_key
153            .to_encoded()
154            .to_vec()
155            .into(),
156    })
157}
158
159#[cfg(test)]
160mod tests {
161    use std::num::NonZeroU32;
162
163    use bitwarden_api_api::{
164        apis::ApiClient,
165        models::{KdfRequestModel, KdfType},
166    };
167    use bitwarden_core::Client;
168    use bitwarden_crypto::{EncString, Kdf};
169
170    use super::*;
171
172    const TEST_USER_ID: &str = "060000fb-0922-4dd3-b170-6e15cb5df8c8";
173    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
174    const TEST_SSO_ORG_IDENTIFIER: &str = "test-org";
175
176    const TEST_ORG_PUBLIC_KEY: &[u8] = &[
177        48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
178        48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148,
179        115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250,
180        229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134,
181        243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92,
182        88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100,
183        76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88,
184        195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130,
185        14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39,
186        80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213,
187        211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16,
188        12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144,
189        255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190,
190        248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108,
191        201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1,
192    ];
193
194    #[tokio::test]
195    async fn test_post_keys_for_jit_password_registration_success() {
196        let client = Client::new(None);
197        let registration_client = RegistrationClient::new(client);
198
199        let expected_hint = "test hint";
200
201        let api_client = ApiClient::new_mocked(|mock| {
202            mock.accounts_api
203                .expect_post_set_password()
204                .once()
205                .withf(move |body| {
206                    if let Some(req) = body {
207                        assert_eq!(req.org_identifier, TEST_SSO_ORG_IDENTIFIER);
208                        assert_eq!(req.master_password_hint, Some(expected_hint.to_string()));
209                        assert!(req.account_keys.is_some());
210                        let account_keys = req.account_keys.as_ref().unwrap();
211                        assert!(
212                            account_keys
213                                .user_key_encrypted_account_private_key
214                                .is_some()
215                        );
216                        assert!(account_keys.account_public_key.is_some());
217                        assert!(account_keys.public_key_encryption_key_pair.is_some());
218                        let public_key_encryption_key_pair = account_keys
219                            .public_key_encryption_key_pair
220                            .as_ref()
221                            .unwrap();
222                        assert!(public_key_encryption_key_pair.public_key.is_some());
223                        assert!(public_key_encryption_key_pair.signed_public_key.is_some());
224                        assert!(public_key_encryption_key_pair.wrapped_private_key.is_some());
225                        assert!(account_keys.signature_key_pair.is_some());
226                        let signature_key_pair = account_keys.signature_key_pair.as_ref().unwrap();
227                        assert_eq!(
228                            signature_key_pair.signature_algorithm,
229                            Some("mldsa44".to_string())
230                        );
231                        assert!(signature_key_pair.verifying_key.is_some());
232                        assert!(signature_key_pair.wrapped_signing_key.is_some());
233                        assert!(account_keys.security_state.is_some());
234                        let security_state = account_keys.security_state.as_ref().unwrap();
235                        assert!(security_state.security_state.is_some());
236                        assert_eq!(security_state.security_version, 2);
237                        assert!(req.master_password_unlock.is_some());
238                        let master_password_unlock = req.master_password_unlock.as_ref().unwrap();
239                        assert_eq!(master_password_unlock.salt, "[email protected]".to_string());
240                        assert_eq!(
241                            master_password_unlock.kdf,
242                            Box::new(KdfRequestModel {
243                                kdf_type: KdfType::Argon2id,
244                                iterations: 6,
245                                memory: Some(32),
246                                parallelism: Some(4),
247                            })
248                        );
249                        assert!(req.master_password_authentication.is_some());
250                        let master_password_authentication =
251                            req.master_password_authentication.as_ref().unwrap();
252                        assert_eq!(
253                            master_password_authentication.salt,
254                            "[email protected]".to_string()
255                        );
256                        assert_eq!(
257                            master_password_authentication.kdf,
258                            Box::new(KdfRequestModel {
259                                kdf_type: KdfType::Argon2id,
260                                iterations: 6,
261                                memory: Some(32),
262                                parallelism: Some(4),
263                            })
264                        );
265                        true
266                    } else {
267                        false
268                    }
269                })
270                .returning(move |_body| Ok(()));
271            mock.organization_users_api
272                .expect_put_reset_password_enrollment()
273                .once()
274                .withf(move |org_id, user_id, body| {
275                    assert_eq!(*org_id, uuid::uuid!(TEST_ORG_ID));
276                    assert_eq!(*user_id, uuid::uuid!(TEST_USER_ID));
277                    if let Some(enrollment_request) = body {
278                        assert!(enrollment_request.reset_password_key.is_some());
279                        assert!(enrollment_request.master_password_hash.is_some());
280                        true
281                    } else {
282                        false
283                    }
284                })
285                .returning(move |_org_id, _user_id, _body| Ok(()));
286        });
287
288        let request = JitMasterPasswordRegistrationRequest {
289            org_id: TEST_ORG_ID.parse().unwrap(),
290            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
291            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
292            user_id: TEST_USER_ID.parse().unwrap(),
293            salt: "[email protected]".to_string(),
294            master_password: "test-password-123".to_string(),
295            master_password_hint: Some(expected_hint.to_string()),
296            reset_password_enroll: true,
297        };
298
299        let result = internal_post_keys_for_jit_password_registration(
300            &registration_client,
301            &api_client,
302            request,
303        )
304        .await;
305
306        assert!(result.is_ok());
307        let result = result.unwrap();
308        assert!(matches!(
309            result.account_cryptographic_state,
310            WrappedAccountCryptographicState::V2 { .. }
311        ));
312        assert_eq!(result.master_password_unlock.salt, "[email protected]");
313        assert!(matches!(
314            result.master_password_unlock.master_key_wrapped_user_key,
315            EncString::Aes256Cbc_HmacSha256_B64 { .. }
316        ));
317        assert_eq!(
318            result.master_password_unlock.kdf,
319            Kdf::Argon2id {
320                iterations: NonZeroU32::new(6).unwrap(),
321                memory: NonZeroU32::new(32).unwrap(),
322                parallelism: NonZeroU32::new(4).unwrap(),
323            }
324        );
325
326        // Assert that the mock expectations were met
327        if let ApiClient::Mock(mut mock) = api_client {
328            mock.accounts_api.checkpoint();
329            mock.organization_users_api.checkpoint();
330        }
331    }
332
333    #[tokio::test]
334    async fn test_post_keys_for_jit_password_registration_api_failure() {
335        let client = Client::new(None);
336        let registration_client = RegistrationClient::new(client);
337
338        let api_client = ApiClient::new_mocked(|mock| {
339            mock.accounts_api
340                .expect_post_set_password()
341                .once()
342                .returning(move |_body| {
343                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
344                });
345            mock.organization_users_api
346                .expect_put_reset_password_enrollment()
347                .never();
348        });
349
350        let request = JitMasterPasswordRegistrationRequest {
351            org_id: TEST_ORG_ID.parse().unwrap(),
352            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
353            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
354            user_id: TEST_USER_ID.parse().unwrap(),
355            salt: "[email protected]".to_string(),
356            master_password: "test-password-123".to_string(),
357            master_password_hint: Some("test hint".to_string()),
358            reset_password_enroll: true,
359        };
360
361        let result = internal_post_keys_for_jit_password_registration(
362            &registration_client,
363            &api_client,
364            request,
365        )
366        .await;
367
368        assert!(result.is_err());
369        assert!(matches!(result.unwrap_err(), RegistrationError::Api));
370
371        // Assert that the mock expectations were met
372        if let ApiClient::Mock(mut mock) = api_client {
373            mock.accounts_api.checkpoint();
374            mock.organization_users_api.checkpoint();
375        }
376    }
377
378    #[tokio::test]
379    async fn test_post_keys_for_jit_password_registration_reset_password_enrollment_failure() {
380        let client = Client::new(None);
381        let registration_client = RegistrationClient::new(client);
382
383        let api_client = ApiClient::new_mocked(|mock| {
384            mock.accounts_api
385                .expect_post_set_password()
386                .once()
387                .returning(move |_body| Ok(()));
388            mock.organization_users_api
389                .expect_put_reset_password_enrollment()
390                .once()
391                .returning(move |_org_id, _user_id, _body| {
392                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
393                });
394        });
395
396        let request = JitMasterPasswordRegistrationRequest {
397            org_id: TEST_ORG_ID.parse().unwrap(),
398            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
399            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
400            user_id: TEST_USER_ID.parse().unwrap(),
401            salt: "[email protected]".to_string(),
402            master_password: "test-password-123".to_string(),
403            master_password_hint: Some("test hint".to_string()),
404            reset_password_enroll: true,
405        };
406
407        let result = internal_post_keys_for_jit_password_registration(
408            &registration_client,
409            &api_client,
410            request,
411        )
412        .await;
413
414        assert!(result.is_err());
415        assert!(matches!(result.unwrap_err(), RegistrationError::Api));
416
417        // Assert that the mock expectations were met
418        if let ApiClient::Mock(mut mock) = api_client {
419            mock.accounts_api.checkpoint();
420            mock.organization_users_api.checkpoint();
421        }
422    }
423
424    #[tokio::test]
425    async fn test_post_keys_for_jit_password_registration_reset_password_enroll_false() {
426        let client = Client::new(None);
427        let registration_client = RegistrationClient::new(client);
428
429        let api_client = ApiClient::new_mocked(|mock| {
430            mock.accounts_api
431                .expect_post_set_password()
432                .once()
433                .returning(move |_body| Ok(()));
434            mock.organization_users_api
435                .expect_put_reset_password_enrollment()
436                .never();
437        });
438
439        let request = JitMasterPasswordRegistrationRequest {
440            org_id: TEST_ORG_ID.parse().unwrap(),
441            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
442            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
443            user_id: TEST_USER_ID.parse().unwrap(),
444            salt: "[email protected]".to_string(),
445            master_password: "test-password-123".to_string(),
446            master_password_hint: Some("test hint".to_string()),
447            reset_password_enroll: false,
448        };
449
450        let result = internal_post_keys_for_jit_password_registration(
451            &registration_client,
452            &api_client,
453            request,
454        )
455        .await;
456
457        assert!(result.is_ok());
458
459        // Assert that the mock expectations were met
460        if let ApiClient::Mock(mut mock) = api_client {
461            mock.accounts_api.checkpoint();
462            mock.organization_users_api.checkpoint();
463        }
464    }
465}