use super::*;
use crate::passkey::PasskeyCredential;
use crate::test_utils::init_test_environment;
use crate::userdb::User;
use serial_test::serial;
const FIRST_USER_PRIVATE_KEY: &[u8] = &[
48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3,
1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 139, 153, 75, 135, 130, 135, 200, 113, 147, 74, 215,
126, 194, 20, 14, 216, 17, 194, 26, 44, 245, 110, 139, 6, 6, 189, 51, 208, 44, 171, 153, 197,
161, 68, 3, 66, 0, 4, 27, 78, 131, 131, 196, 142, 118, 54, 201, 9, 43, 62, 50, 252, 223, 99,
155, 195, 74, 137, 198, 36, 126, 188, 138, 20, 142, 51, 38, 144, 166, 242, 54, 51, 184, 181,
61, 219, 148, 144, 37, 60, 142, 88, 223, 217, 195, 136, 217, 39, 237, 73, 228, 8, 86, 72, 75,
127, 92, 98, 159, 103, 44, 251,
];
fn build_auth_data_for_registration(origin: &str, credential_id_bytes: &[u8]) -> Vec<u8> {
use ciborium::value::{Integer, Value as CborValue};
use ring::signature;
let rp_id = origin
.trim_start_matches("https://")
.trim_start_matches("http://")
.split(':')
.next()
.unwrap_or("127.0.0.1");
let mut auth_data = Vec::new();
let rp_id_hash = ring::digest::digest(&ring::digest::SHA256, rp_id.as_bytes());
auth_data.extend_from_slice(rp_id_hash.as_ref());
auth_data.push(0x45);
auth_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
auth_data.extend_from_slice(&[0x00; 16]);
auth_data.extend_from_slice(&(credential_id_bytes.len() as u16).to_be_bytes());
auth_data.extend_from_slice(credential_id_bytes);
let rng = ring::rand::SystemRandom::new();
let key_pair = signature::EcdsaKeyPair::from_pkcs8(
&signature::ECDSA_P256_SHA256_ASN1_SIGNING,
FIRST_USER_PRIVATE_KEY,
&rng,
)
.expect("Fixed key pair should be valid");
let pk_bytes = ring::signature::KeyPair::public_key(&key_pair).as_ref();
let x_coord = &pk_bytes[1..33];
let y_coord = &pk_bytes[33..65];
let cose_key = CborValue::Map(vec![
(
CborValue::Integer(Integer::from(1)),
CborValue::Integer(Integer::from(2)),
), (
CborValue::Integer(Integer::from(3)),
CborValue::Integer(Integer::from(-7)),
), (
CborValue::Integer(Integer::from(-1)),
CborValue::Integer(Integer::from(1)),
), (
CborValue::Integer(Integer::from(-2)),
CborValue::Bytes(x_coord.to_vec()),
), (
CborValue::Integer(Integer::from(-3)),
CborValue::Bytes(y_coord.to_vec()),
), ]);
let mut cose_key_bytes = Vec::new();
ciborium::ser::into_writer(&cose_key, &mut cose_key_bytes).unwrap();
auth_data.extend_from_slice(&cose_key_bytes);
auth_data
}
fn build_none_registration_response(
challenge: &str,
user_handle: &str,
origin: &str,
) -> serde_json::Value {
use base64::{Engine as _, engine::general_purpose};
use ciborium::value::Value as CborValue;
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
let credential_id = format!("test_cred_{ts}");
let credential_id_bytes = credential_id.as_bytes();
let client_data = serde_json::json!({
"type": "webauthn.create",
"challenge": challenge,
"origin": origin
});
let client_data_json =
general_purpose::URL_SAFE_NO_PAD.encode(client_data.to_string().as_bytes());
let auth_data = build_auth_data_for_registration(origin, credential_id_bytes);
let attestation_obj = CborValue::Map(vec![
(
CborValue::Text("fmt".to_string()),
CborValue::Text("none".to_string()),
),
(
CborValue::Text("authData".to_string()),
CborValue::Bytes(auth_data),
),
(
CborValue::Text("attStmt".to_string()),
CborValue::Map(vec![]),
),
]);
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&attestation_obj, &mut cbor_bytes).unwrap();
let attestation_object = general_purpose::URL_SAFE_NO_PAD.encode(&cbor_bytes);
serde_json::json!({
"id": credential_id,
"raw_id": general_purpose::URL_SAFE_NO_PAD.encode(credential_id_bytes),
"response": {
"client_data_json": client_data_json,
"attestation_object": attestation_object,
},
"type": "public-key",
"user_handle": user_handle
})
}
fn build_signed_authentication_response(
credential_id: &str,
challenge: &str,
auth_id: &str,
user_handle: &str,
origin: &str,
) -> serde_json::Value {
use base64::{Engine as _, engine::general_purpose};
let rng = ring::rand::SystemRandom::new();
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
FIRST_USER_PRIVATE_KEY,
&rng,
)
.expect("Fixed key pair should be valid");
let client_data = serde_json::json!({
"type": "webauthn.get",
"challenge": challenge,
"origin": origin
});
let client_data_str = client_data.to_string();
let client_data_hash = ring::digest::digest(&ring::digest::SHA256, client_data_str.as_bytes());
let client_data_json = general_purpose::URL_SAFE_NO_PAD.encode(client_data_str.as_bytes());
let rp_id = origin
.trim_start_matches("https://")
.trim_start_matches("http://")
.split(':')
.next()
.unwrap_or("127.0.0.1");
let mut auth_data = Vec::new();
let rp_id_hash = ring::digest::digest(&ring::digest::SHA256, rp_id.as_bytes());
auth_data.extend_from_slice(rp_id_hash.as_ref());
auth_data.push(0x05);
use std::sync::atomic::{AtomicU32, Ordering};
static AUTH_COUNTER: AtomicU32 = AtomicU32::new(100);
let counter = AUTH_COUNTER.fetch_add(1, Ordering::Relaxed);
auth_data.extend_from_slice(&counter.to_be_bytes());
let mut signed_data = Vec::new();
signed_data.extend_from_slice(&auth_data);
signed_data.extend_from_slice(client_data_hash.as_ref());
let sig = key_pair
.sign(&rng, &signed_data)
.expect("Signing should succeed");
let auth_data_b64 = general_purpose::URL_SAFE_NO_PAD.encode(&auth_data);
let signature_b64 = general_purpose::URL_SAFE_NO_PAD.encode(sig.as_ref());
serde_json::json!({
"id": credential_id,
"raw_id": general_purpose::URL_SAFE_NO_PAD.encode(credential_id.as_bytes()),
"response": {
"client_data_json": client_data_json,
"authenticator_data": auth_data_b64,
"signature": signature_b64,
"user_handle": user_handle
},
"type": "public-key",
"authenticator_attachment": "platform",
"auth_id": auth_id
})
}
async fn create_test_user_in_db(user_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let user = User {
id: user_id.to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
UserStore::upsert_user(user).await?;
Ok(())
}
async fn insert_test_passkey_credential(
credential_id: &str,
user_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let user = serde_json::json!({
"name": "Test User",
"displayName": "Test Display Name",
"user_handle": user_id.to_string()
});
let passkey_user = serde_json::from_value(user).expect("Failed to create user entity");
let credential = PasskeyCredential {
sequence_number: None,
credential_id: credential_id.to_string(),
user_id: user_id.to_string(),
public_key: "test_public_key".to_string(),
aaguid: "test-aaguid".to_string(),
rp_id: "127.0.0.1".to_string(),
user: passkey_user,
counter: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
last_used_at: Utc::now(),
};
PasskeyStore::store_credential(
CredentialId::new(credential.credential_id.clone()).expect("Valid test credential ID"),
credential,
)
.await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_delete_passkey_credential_core_not_found() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let user_id = "test_user_4";
let credential_id = "nonexistent_credential";
create_test_user_in_db(user_id).await?;
let result = delete_passkey_credential_core(
UserId::new(user_id.to_string()).expect("Valid user ID"),
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(
matches!(result, Err(CoordinationError::ResourceNotFound { .. })),
"Expected ResourceNotFound error, got: {result:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_update_passkey_credential_core_success() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let user_id = "test_user_6";
let credential_id = "test_credential_6";
create_test_user_in_db(user_id).await?;
insert_test_passkey_credential(credential_id, user_id).await?;
let session_user = SessionUser {
id: user_id.to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let new_name = "Updated Name";
let new_display_name = "Updated Display Name";
let result = update_passkey_credential_core(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
new_name,
new_display_name,
Some(session_user),
)
.await;
assert!(
result.is_ok(),
"Failed to update passkey credential: {result:?}"
);
let updated_credential = PasskeyStore::get_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await?
.unwrap();
assert_eq!(
updated_credential.user.name, new_name,
"Name was not updated"
);
assert_eq!(
updated_credential.user.display_name, new_display_name,
"Display name was not updated"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_update_passkey_credential_core_unauthorized() -> Result<(), Box<dyn std::error::Error>>
{
init_test_environment().await;
let user_id = "test_user_7";
let other_user_id = "test_user_8";
let credential_id = "test_credential_7";
create_test_user_in_db(user_id).await?;
create_test_user_in_db(other_user_id).await?;
insert_test_passkey_credential(credential_id, user_id).await?;
let session_user = SessionUser {
id: other_user_id.to_string(),
account: "other_account".to_string(),
label: "Other User".to_string(),
is_admin: false,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let result = update_passkey_credential_core(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
"Updated Name",
"Updated Display Name",
Some(session_user),
)
.await;
assert!(
matches!(result, Err(CoordinationError::Unauthorized)),
"Expected Unauthorized error, got: {result:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_update_passkey_credential_core_no_session() -> Result<(), Box<dyn std::error::Error>>
{
init_test_environment().await;
let user_id = "test_user_9";
let credential_id = "test_credential_9";
create_test_user_in_db(user_id).await?;
insert_test_passkey_credential(credential_id, user_id).await?;
let result = update_passkey_credential_core(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
"Updated Name",
"Updated Display Name",
None,
)
.await;
assert!(
matches!(result, Err(CoordinationError::Unauthorized)),
"Expected Unauthorized error, got: {result:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_list_credentials_core_with_credentials() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let user_id = "test_user_list_1";
create_test_user_in_db(user_id).await?;
insert_test_passkey_credential("cred_list_1a", user_id).await?;
insert_test_passkey_credential("cred_list_1b", user_id).await?;
let result =
list_credentials_core(UserId::new(user_id.to_string()).expect("Valid user ID")).await?;
assert_eq!(result.len(), 2, "Should return 2 credentials");
let credential_ids: Vec<&str> = result.iter().map(|c| c.credential_id.as_str()).collect();
assert!(
credential_ids.contains(&"cred_list_1a"),
"Should contain first credential"
);
assert!(
credential_ids.contains(&"cred_list_1b"),
"Should contain second credential"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_list_credentials_core_empty() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let user_id = "test_user_list_empty";
create_test_user_in_db(user_id).await?;
let result =
list_credentials_core(UserId::new(user_id.to_string()).expect("Valid user ID")).await?;
assert!(
result.is_empty(),
"Should return empty list for user with no credentials"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_registration_core_create_user_mode() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let body = RegistrationStartRequest {
username: "new_user@example.com".to_string(),
displayname: "New User".to_string(),
mode: RegistrationMode::CreateUser,
};
let result = handle_start_registration_core(None, body).await;
assert!(
result.is_ok(),
"CreateUser mode without auth should succeed: {result:?}"
);
let options = result.unwrap();
let options_json = serde_json::to_value(&options)?;
assert!(
!options_json["challenge"].as_str().unwrap_or("").is_empty(),
"Should contain a non-empty challenge"
);
assert_eq!(
options_json["rpId"].as_str().unwrap_or(""),
"127.0.0.1",
"RP ID should match the test origin host"
);
assert_eq!(
options_json["rp"]["id"].as_str().unwrap_or(""),
"127.0.0.1",
"RP entity ID should match rpId"
);
assert!(
options_json["user"]["user_handle"].is_string(),
"Should contain a user handle"
);
assert_eq!(
options_json["user"]["name"].as_str().unwrap_or(""),
"new_user@example.com",
"User name should match the requested username"
);
assert_eq!(
options_json["user"]["displayName"].as_str().unwrap_or(""),
"New User",
"Display name should match the requested displayname"
);
assert!(
options_json["pubKeyCredParams"].is_array(),
"Should contain pubKeyCredParams"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_registration_core_create_user_rejects_authenticated()
-> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let session_user = SessionUser {
id: "test_user_reg_create".to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let body = RegistrationStartRequest {
username: "new_user@example.com".to_string(),
displayname: "New User".to_string(),
mode: RegistrationMode::CreateUser,
};
let result = handle_start_registration_core(Some(&session_user), body).await;
assert!(
matches!(result, Err(CoordinationError::UnexpectedlyAuthorized)),
"CreateUser mode with auth should fail with UnexpectedlyAuthorized: {result:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_registration_core_add_to_user_mode() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let user_id = "test_user_reg_add";
create_test_user_in_db(user_id).await?;
let session_user = SessionUser {
id: user_id.to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let body = RegistrationStartRequest {
username: "test_user@example.com".to_string(),
displayname: "Test User".to_string(),
mode: RegistrationMode::AddToUser,
};
let result = handle_start_registration_core(Some(&session_user), body).await;
assert!(
result.is_ok(),
"AddToUser mode with auth should succeed: {result:?}"
);
let options = result.unwrap();
let options_json = serde_json::to_value(&options)?;
assert!(
!options_json["challenge"].as_str().unwrap_or("").is_empty(),
"Should contain a non-empty challenge"
);
assert_eq!(
options_json["rpId"].as_str().unwrap_or(""),
"127.0.0.1",
"RP ID should match the test origin host"
);
assert!(
options_json["user"]["user_handle"].is_string(),
"Should contain a user handle"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_registration_core_add_to_user_rejects_unauthenticated()
-> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let body = RegistrationStartRequest {
username: "test_user@example.com".to_string(),
displayname: "Test User".to_string(),
mode: RegistrationMode::AddToUser,
};
let result = handle_start_registration_core(None, body).await;
assert!(
matches!(result, Err(CoordinationError::Unauthorized)),
"AddToUser mode without auth should fail with Unauthorized: {result:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_authentication_core_no_username() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let body = serde_json::json!({});
let result = handle_start_authentication_core(&body).await;
assert!(
result.is_ok(),
"Authentication without username should succeed: {result:?}"
);
let options = result.unwrap();
let options_json = serde_json::to_value(&options)?;
assert!(
!options_json["challenge"].as_str().unwrap_or("").is_empty(),
"Should contain a non-empty challenge"
);
assert_eq!(
options_json["rpId"].as_str().unwrap_or(""),
"127.0.0.1",
"RP ID should match the test origin host"
);
assert!(
options_json["authId"].is_string(),
"Should contain an authId"
);
assert!(
options_json["allowCredentials"]
.as_array()
.is_none_or(|a| a.is_empty()),
"Discoverable flow should have empty allowCredentials"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_authentication_core_with_username() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let user_id = "test_user_auth_start";
create_test_user_in_db(user_id).await?;
insert_test_passkey_credential("cred_auth_start_1", user_id).await?;
let body = serde_json::json!({ "username": "Test User" });
let result = handle_start_authentication_core(&body).await;
assert!(
result.is_ok(),
"Authentication with known username should succeed: {result:?}"
);
let options = result.unwrap();
let options_json = serde_json::to_value(&options)?;
assert!(
!options_json["challenge"].as_str().unwrap_or("").is_empty(),
"Should contain a non-empty challenge"
);
assert_eq!(
options_json["rpId"].as_str().unwrap_or(""),
"127.0.0.1",
"RP ID should match the test origin host"
);
assert!(
options_json["authId"].is_string(),
"Should contain an authId"
);
let allow_creds = options_json["allowCredentials"]
.as_array()
.expect("Should have allowCredentials array");
assert!(
!allow_creds.is_empty(),
"Should have non-empty allowCredentials for known username"
);
assert!(
allow_creds
.iter()
.any(|c| c["id"].as_str() == Some("cred_auth_start_1")),
"allowCredentials should contain the test credential"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_authentication_core_nonexistent_username()
-> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let body = serde_json::json!({ "username": "nonexistent_user_12345" });
let result = handle_start_authentication_core(&body).await;
assert!(
result.is_ok(),
"Authentication with nonexistent username should succeed: {result:?}"
);
let options = result.unwrap();
let options_json = serde_json::to_value(&options)?;
assert!(
!options_json["challenge"].as_str().unwrap_or("").is_empty(),
"Should contain a non-empty challenge"
);
assert_eq!(
options_json["rpId"].as_str().unwrap_or(""),
"127.0.0.1",
"RP ID should match the test origin host"
);
let allow_creds = options_json["allowCredentials"]
.as_array()
.expect("Should have allowCredentials array");
assert!(
allow_creds.is_empty(),
"Should have empty allowCredentials for nonexistent username"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_start_authentication_core_string_body() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let body = serde_json::json!("some_username");
let result = handle_start_authentication_core(&body).await;
assert!(
result.is_ok(),
"Authentication with string body should succeed: {result:?}"
);
let options = result.unwrap();
let options_json = serde_json::to_value(&options)?;
assert!(
!options_json["challenge"].as_str().unwrap_or("").is_empty(),
"Should contain a non-empty challenge"
);
assert_eq!(
options_json["rpId"].as_str().unwrap_or(""),
"127.0.0.1",
"RP ID should match the test origin host"
);
assert!(
options_json["authId"].is_string(),
"Should contain an authId"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_finish_registration_core_create_user() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let origin = crate::test_utils::get_test_origin();
let body = RegistrationStartRequest {
username: "finish_reg_user@example.com".to_string(),
displayname: "Finish Reg User".to_string(),
mode: RegistrationMode::CreateUser,
};
let options = handle_start_registration_core(None, body).await?;
let options_json = serde_json::to_value(&options)?;
let challenge = options_json["challenge"]
.as_str()
.expect("Options should contain challenge");
let user_handle = options_json["user"]["user_handle"]
.as_str()
.expect("Options should contain user.user_handle");
let reg_data_json = build_none_registration_response(challenge, user_handle, &origin);
let reg_data: RegisterCredential = serde_json::from_value(reg_data_json)?;
let result = handle_finish_registration_core(None, reg_data).await;
assert!(
result.is_ok(),
"CreateUser finish registration should succeed: {result:?}"
);
let (headers, message) = result.unwrap();
assert!(!message.is_empty(), "Should return a success message");
assert!(
!headers.is_empty(),
"Should contain session headers for newly created user"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_finish_registration_core_add_to_user() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let origin = crate::test_utils::get_test_origin();
let user_id = "test_user_finish_reg_add";
create_test_user_in_db(user_id).await?;
let session_user = SessionUser {
id: user_id.to_string(),
account: "test_account".to_string(),
label: "Test User".to_string(),
is_admin: false,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let body = RegistrationStartRequest {
username: "add_cred_user@example.com".to_string(),
displayname: "Add Cred User".to_string(),
mode: RegistrationMode::AddToUser,
};
let options = handle_start_registration_core(Some(&session_user), body).await?;
let options_json = serde_json::to_value(&options)?;
let challenge = options_json["challenge"]
.as_str()
.expect("Options should contain challenge");
let user_handle = options_json["user"]["user_handle"]
.as_str()
.expect("Options should contain user.user_handle");
let reg_data_json = build_none_registration_response(challenge, user_handle, &origin);
let reg_data: RegisterCredential = serde_json::from_value(reg_data_json)?;
let result = handle_finish_registration_core(Some(&session_user), reg_data).await;
assert!(
result.is_ok(),
"AddToUser finish registration should succeed: {result:?}"
);
let (headers, message) = result.unwrap();
assert!(!message.is_empty(), "Should return a success message");
assert!(
headers.is_empty(),
"Should not contain session headers for existing user"
);
let credentials =
list_credentials_core(UserId::new(user_id.to_string()).expect("Valid user ID")).await?;
assert!(
!credentials.is_empty(),
"User should have at least one credential after registration"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_finish_authentication_core_success() -> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let origin = crate::test_utils::get_test_origin();
let credential_id = "first-user-test-passkey-credential";
let user_handle = "first-user-handle";
let body = serde_json::json!({});
let auth_options = handle_start_authentication_core(&body).await?;
let options_json = serde_json::to_value(&auth_options)?;
let challenge = options_json["challenge"]
.as_str()
.expect("Options should contain challenge");
let auth_id = options_json["authId"]
.as_str()
.expect("Options should contain authId");
let auth_response_json = build_signed_authentication_response(
credential_id,
challenge,
auth_id,
user_handle,
&origin,
);
let auth_response: AuthenticatorResponse = serde_json::from_value(auth_response_json)?;
let result = handle_finish_authentication_core(auth_response, None).await;
assert!(result.is_ok(), "Authentication should succeed: {result:?}");
let (auth_resp, headers) = result.unwrap();
assert_eq!(
auth_resp.user_handle, user_handle,
"Should return correct user_handle"
);
assert!(
!auth_resp.credential_ids.is_empty(),
"Should return at least one credential ID"
);
assert!(
auth_resp
.credential_ids
.contains(&credential_id.to_string()),
"Should contain the authenticated credential ID"
);
assert!(!headers.is_empty(), "Should contain session headers");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_finish_authentication_core_tampered_signature()
-> Result<(), Box<dyn std::error::Error>> {
use base64::{Engine as _, engine::general_purpose};
init_test_environment().await;
let origin = crate::test_utils::get_test_origin();
let credential_id = "first-user-test-passkey-credential";
let user_handle = "first-user-handle";
let body = serde_json::json!({});
let auth_options = handle_start_authentication_core(&body).await?;
let options_json = serde_json::to_value(&auth_options)?;
let challenge = options_json["challenge"]
.as_str()
.expect("Options should contain challenge");
let auth_id = options_json["authId"]
.as_str()
.expect("Options should contain authId");
let mut auth_response_json = build_signed_authentication_response(
credential_id,
challenge,
auth_id,
user_handle,
&origin,
);
let sig_str = auth_response_json["response"]["signature"]
.as_str()
.expect("Response should contain signature");
let mut sig_bytes = general_purpose::URL_SAFE_NO_PAD
.decode(sig_str)
.expect("Signature should be valid base64url");
sig_bytes[10] ^= 0xFF;
let tampered_sig = general_purpose::URL_SAFE_NO_PAD.encode(&sig_bytes);
auth_response_json["response"]["signature"] = serde_json::Value::String(tampered_sig);
let auth_response: AuthenticatorResponse = serde_json::from_value(auth_response_json)?;
let result = handle_finish_authentication_core(auth_response, None).await;
assert!(
result.is_err(),
"Tampered signature should be rejected, but got: {result:?}"
);
assert!(
matches!(result, Err(CoordinationError::PasskeyError(_))),
"Should return PasskeyError for tampered signature: {result:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_finish_authentication_core_challenge_mismatch()
-> Result<(), Box<dyn std::error::Error>> {
init_test_environment().await;
let origin = crate::test_utils::get_test_origin();
let credential_id = "first-user-test-passkey-credential";
let user_handle = "first-user-handle";
let body = serde_json::json!({});
let auth_options = handle_start_authentication_core(&body).await?;
let options_json = serde_json::to_value(&auth_options)?;
let auth_id = options_json["authId"]
.as_str()
.expect("Options should contain authId");
let wrong_challenge = "d3JvbmdfY2hhbGxlbmdlX3ZhbHVlX2Zvcl90ZXN0aW5n";
let auth_response_json = build_signed_authentication_response(
credential_id,
wrong_challenge,
auth_id,
user_handle,
&origin,
);
let auth_response: AuthenticatorResponse = serde_json::from_value(auth_response_json)?;
let result = handle_finish_authentication_core(auth_response, None).await;
assert!(
result.is_err(),
"Challenge mismatch should be rejected, but got: {result:?}"
);
assert!(
matches!(result, Err(CoordinationError::PasskeyError(_))),
"Should return PasskeyError for challenge mismatch: {result:?}"
);
Ok(())
}
#[test]
fn test_get_passkey_field_mappings_defaults() {
let (account_field, label_field) = get_passkey_field_mappings();
assert_eq!(
account_field, "name",
"Default account field should be 'name'"
);
assert_eq!(
label_field, "display_name",
"Default label field should be 'display_name'"
);
}