Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@ pub enum SendAccessTokenInvalidGrantError {
#[allow(missing_docs)]
PasswordHashB64Invalid,

#[allow(missing_docs)]
OtpInvalid,

#[allow(missing_docs)]
OtpGenerationFailed,

/// Fallback for unknown variants for forward compatibility
#[serde(other)]
Unknown,
Expand Down Expand Up @@ -323,14 +317,6 @@ mod tests {
SendAccessTokenInvalidGrantError::PasswordHashB64Invalid,
"\"password_hash_b64_invalid\"",
),
(
SendAccessTokenInvalidGrantError::OtpInvalid,
"\"otp_invalid\"",
),
(
SendAccessTokenInvalidGrantError::OtpGenerationFailed,
"\"otp_generation_failed\"",
),
];

for (expected_variant, send_access_error_type_json) in cases {
Expand Down Expand Up @@ -399,7 +385,7 @@ mod tests {
let payload = r#"
{
"error": "invalid_grant",
"send_access_error_type": "otp_invalid"
"send_access_error_type": "password_hash_b64_invalid"
}"#;

let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
Expand All @@ -411,7 +397,7 @@ mod tests {
assert!(error_description.is_none());
assert_eq!(
send_access_error_type,
Some(SendAccessTokenInvalidGrantError::OtpInvalid)
Some(SendAccessTokenInvalidGrantError::PasswordHashB64Invalid)
);
}
_ => panic!("expected invalid_grant"),
Expand Down
150 changes: 105 additions & 45 deletions crates/bitwarden-auth/src/send_access/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,23 +548,19 @@ mod tests {
other => panic!("expected Response variant, got {:?}", other),
}
}
}

mod request_send_access_token_invalid_grant_tests {

use super::*;

#[tokio::test]
async fn request_send_access_token_invalid_grant_invalid_send_id_error() {
async fn request_send_access_token_invalid_request_email_credential_unrecognized_email_masked_as_otp_required()
{
// Create a mock error response
let error_description = "send_id is invalid.".into();
let error_description = "email and otp are required.".into();
let raw_error = serde_json::json!({
"error": "invalid_grant",
"error": "invalid_request",
"error_description": error_description,
"send_access_error_type": "send_id_invalid"
"send_access_error_type": "email_and_otp_required"
});

// Create the mock for the request
// Register the mock for the request
let mock = Mock::given(matchers::method("POST"))
.and(matchers::path("identity/connect/token"))
.respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
Expand All @@ -575,10 +571,13 @@ mod tests {
// Create a send access client
let send_access_client = make_send_client(&mock_server);

// Construct the request without a send_id to trigger an error
// Construct the request
let email_credentials = SendEmailCredentials {
email: "invalid-email".into(),
};
let req = SendAccessTokenRequest {
send_id: "invalid-send-id".into(),
send_access_credentials: None, // No credentials for this test
send_id: "valid-send-id".into(),
send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
};

let result = send_access_client.request_send_access_token(req).await;
Expand All @@ -591,9 +590,9 @@ mod tests {
// Now assert the inner enum:
assert_eq!(
api_err,
SendAccessTokenApiErrorResponse::InvalidGrant {
SendAccessTokenApiErrorResponse::InvalidRequest {
send_access_error_type: Some(
SendAccessTokenInvalidGrantError::SendIdInvalid
SendAccessTokenInvalidRequestError::EmailAndOtpRequired
),
error_description: Some(error_description),
}
Expand All @@ -604,13 +603,15 @@ mod tests {
}

#[tokio::test]
async fn request_send_access_token_invalid_grant_invalid_password_hash_error() {
// Create a mock error response
let error_description = "password_hash_b64 is invalid.".into();
async fn request_send_access_token_invalid_request_email_otp_credential_invalid_otp_masked_as_otp_required()
{
// When an email+OTP is sent with an invalid OTP, the server returns
// email_and_otp_required (not otp_invalid) to prevent email enumeration.
let error_description = "email and otp are required.".into();
let raw_error = serde_json::json!({
"error": "invalid_grant",
"error": "invalid_request",
"error_description": error_description,
"send_access_error_type": "password_hash_b64_invalid"
"send_access_error_type": "email_and_otp_required"
});

// Create the mock for the request
Expand All @@ -625,14 +626,14 @@ mod tests {
let send_access_client = make_send_client(&mock_server);

// Construct the request
let password_credentials = SendPasswordCredentials {
password_hash_b64: "invalid-hash".into(),
let email_otp_credentials = SendEmailOtpCredentials {
email: "valid@email.com".into(),
otp: "invalid_otp".into(),
};

let req = SendAccessTokenRequest {
send_id: "valid-send-id".into(),
send_access_credentials: Some(SendAccessCredentials::Password(
password_credentials,
send_access_credentials: Some(SendAccessCredentials::EmailOtp(
email_otp_credentials,
)),
};

Expand All @@ -643,12 +644,11 @@ mod tests {
let err = result.unwrap_err();
match err {
SendAccessTokenError::Expected(api_err) => {
// Now assert the inner enum:
assert_eq!(
api_err,
SendAccessTokenApiErrorResponse::InvalidGrant {
SendAccessTokenApiErrorResponse::InvalidRequest {
send_access_error_type: Some(
SendAccessTokenInvalidGrantError::PasswordHashB64Invalid
SendAccessTokenInvalidRequestError::EmailAndOtpRequired
),
error_description: Some(error_description),
}
Expand All @@ -659,16 +659,20 @@ mod tests {
}

#[tokio::test]
async fn request_send_access_token_invalid_request_invalid_email_error() {
// Create a mock error response
async fn request_send_access_token_invalid_request_email_otp_credential_unrecognized_email_masked_as_otp_required()
{
// When an email+OTP is sent where the email is not in the Send's allowed list,
// the server returns email_and_otp_required (not email_invalid) to prevent email
// enumeration. The server checks email validity before OTP, so even a valid OTP
// paired with an unrecognized email returns the same generic response.
let error_description = "email and otp are required.".into();
let raw_error = serde_json::json!({
"error": "invalid_request",
"error_description": error_description,
"send_access_error_type": "email_and_otp_required"
});

// Register the mock for the request
// Create the mock for the request
let mock = Mock::given(matchers::method("POST"))
.and(matchers::path("identity/connect/token"))
.respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
Expand All @@ -679,13 +683,16 @@ mod tests {
// Create a send access client
let send_access_client = make_send_client(&mock_server);

// Construct the request
let email_credentials = SendEmailCredentials {
email: "invalid-email".into(),
// Construct the request with an email not in the Send's allowed list
let email_otp_credentials = SendEmailOtpCredentials {
email: "notallowed@example.com".into(),
otp: "any_otp".into(),
};
let req = SendAccessTokenRequest {
send_id: "valid-send-id".into(),
send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
send_access_credentials: Some(SendAccessCredentials::EmailOtp(
email_otp_credentials,
)),
};

let result = send_access_client.request_send_access_token(req).await;
Expand All @@ -695,7 +702,6 @@ mod tests {
let err = result.unwrap_err();
match err {
SendAccessTokenError::Expected(api_err) => {
// Now assert the inner enum:
assert_eq!(
api_err,
SendAccessTokenApiErrorResponse::InvalidRequest {
Expand All @@ -709,15 +715,69 @@ mod tests {
other => panic!("expected Response variant, got {:?}", other),
}
}
}

mod request_send_access_token_invalid_grant_tests {

use super::*;

#[tokio::test]
async fn request_send_access_token_invalid_grant_invalid_otp_error() {
async fn request_send_access_token_invalid_grant_invalid_send_id_error() {
// Create a mock error response
let error_description = "otp is invalid.".into();
let error_description = "send_id is invalid.".into();
let raw_error = serde_json::json!({
"error": "invalid_grant",
"error_description": error_description,
"send_access_error_type": "otp_invalid"
"send_access_error_type": "send_id_invalid"
});

// Create the mock for the request
let mock = Mock::given(matchers::method("POST"))
.and(matchers::path("identity/connect/token"))
.respond_with(ResponseTemplate::new(400).set_body_json(raw_error));

// Spin up a server and register mock with it
let (mock_server, _api_config) = start_api_mock(vec![mock]).await;

// Create a send access client
let send_access_client = make_send_client(&mock_server);

// Construct the request with an invalid send_id to trigger an error
let req = SendAccessTokenRequest {
send_id: "invalid-send-id".into(),
send_access_credentials: None, // No credentials for this test
};

let result = send_access_client.request_send_access_token(req).await;

assert!(result.is_err());

let err = result.unwrap_err();
match err {
SendAccessTokenError::Expected(api_err) => {
// Now assert the inner enum:
assert_eq!(
api_err,
SendAccessTokenApiErrorResponse::InvalidGrant {
send_access_error_type: Some(
SendAccessTokenInvalidGrantError::SendIdInvalid
),
error_description: Some(error_description),
}
);
}
other => panic!("expected Response variant, got {:?}", other),
}
}

#[tokio::test]
async fn request_send_access_token_invalid_grant_invalid_password_hash_error() {
// Create a mock error response
let error_description = "password_hash_b64 is invalid.".into();
let raw_error = serde_json::json!({
"error": "invalid_grant",
"error_description": error_description,
"send_access_error_type": "password_hash_b64_invalid"
});

// Create the mock for the request
Expand All @@ -732,14 +792,14 @@ mod tests {
let send_access_client = make_send_client(&mock_server);

// Construct the request
let email_otp_credentials = SendEmailOtpCredentials {
email: "valid@email.com".into(),
otp: "valid_otp".into(),
let password_credentials = SendPasswordCredentials {
password_hash_b64: "invalid-hash".into(),
};

let req = SendAccessTokenRequest {
send_id: "valid-send-id".into(),
send_access_credentials: Some(SendAccessCredentials::EmailOtp(
email_otp_credentials,
send_access_credentials: Some(SendAccessCredentials::Password(
password_credentials,
)),
};

Expand All @@ -755,7 +815,7 @@ mod tests {
api_err,
SendAccessTokenApiErrorResponse::InvalidGrant {
send_access_error_type: Some(
SendAccessTokenInvalidGrantError::OtpInvalid
SendAccessTokenInvalidGrantError::PasswordHashB64Invalid
),
error_description: Some(error_description),
}
Expand Down
Loading