#![allow(
clippy::expect_used,
reason = "Fixture setup and response assertions require successful fallible construction."
)]
use super::*;
use better_auth_core::{
AuthInitContext, FieldValue, Utf16String,
store::schema::EntityRole,
user_fields::{FieldTransforms, UserConfig, UserFieldConfig, UserFieldTransform},
};
#[tokio::test]
async fn options_consume_native_descriptors_without_utf16_loss_or_stricter_base64_validation() {
let plugin = passkey_plugin();
let (mut ctx, user, session) = test_helpers::create_test_context_with_user(
CreateUser::new()
.with_email("descriptor@example.com")
.with_name("Descriptor owner"),
Duration::hours(1),
)
.await;
let raw = ctx.database.clone();
let passkey = raw
.create_passkey(CreatePasskey {
additional_fields: Default::default(),
user_id: user.id.clone(),
name: None.into(),
credential_id: credential_id("existing"),
public_key: "unused by options".into(),
counter: 0,
device_type: "singleDevice".into(),
backed_up: false,
transports: Some("internal".into()),
credential: better_auth_core::PasskeyCredentialState::Legacy(
"unused by options".into(),
),
aaguid: None.into(),
})
.await
.unwrap();
let utf16 = Utf16String::from_units(vec![
b'u'.into(),
b's'.into(),
b'b'.into(),
b','.into(),
0xd800,
b','.into(),
]);
for (id, transports, expected_id, expected_transports) in [
(
FieldValue::from("Y=WI==="),
FieldValue::from(utf16),
Some("YWI"),
vec![
FieldValue::from("usb"),
Utf16String::from_units(vec![0xd800]).into(),
"".into(),
]
.into(),
),
(
"A".into(),
FieldValue::Null,
Some("A"),
FieldValue::Undefined,
),
(
"".into(),
FieldValue::Undefined,
Some(""),
FieldValue::Undefined,
),
(
"AB".into(),
"".into(),
Some("AB"),
vec![FieldValue::from("")].into(),
),
(
"bad+id".into(),
FieldValue::Null,
None,
FieldValue::Undefined,
),
(
FieldValue::Number(17.0),
FieldValue::Null,
None,
FieldValue::Undefined,
),
(
FieldValue::Undefined,
FieldValue::Null,
None,
FieldValue::Undefined,
),
(
Utf16String::from_units(vec![0xd800]).into(),
FieldValue::Null,
None,
FieldValue::Undefined,
),
(
"valid".into(),
FieldValue::Bool(false),
None,
FieldValue::Undefined,
),
] {
let mut model_fields = better_auth_core::plugin_runtime::ModelFields::default();
let mut init = AuthInitContext::new(ctx.config.clone(), raw.clone())
.expect("fixture context initializes");
better_auth_core::AuthPlugin::<
better_auth_seaorm::store::__private_test_support::bundled_schema::BundledSchema,
>::schema(&plugin, &mut model_fields)
.unwrap();
better_auth_core::AuthPlugin::on_init(&plugin, &mut init)
.await
.unwrap();
model_fields
.register_model_fields(
EntityRole::Passkey,
UserConfig {
additional_fields: Some(
[
("credentialID".into(), id),
("transports".into(), transports),
]
.into_iter()
.map(|(name, value)| {
(
name,
UserFieldConfig {
required: Some(false),
transform: Some(FieldTransforms {
input: None,
output: Some(UserFieldTransform::new(move |_| {
Ok(value.clone())
})),
}),
..Default::default()
},
)
})
.collect(),
),
},
)
.unwrap();
ctx.database = raw
.with_runtime(ctx.config.clone(), Vec::new(), model_fields)
.unwrap();
for (path, property) in [
("/passkey/generate-register-options", "excludeCredentials"),
("/passkey/generate-authenticate-options", "allowCredentials"),
] {
let req = test_helpers::create_auth_request_no_query(
HttpMethod::Get,
path,
Some(session.token.typed().unwrap()),
None,
);
let response = if property == "excludeCredentials" {
plugin.handle_generate_register_options(&req, &ctx).await
} else {
plugin
.handle_generate_authenticate_options(&req, &ctx)
.await
};
let Some(expected_id) = expected_id else {
assert_eq!(response.unwrap_err().status_code().unwrap(), 500);
continue;
};
let response = response.unwrap();
assert_eq!(response.status, 200);
assert!(cookie_header(&response).contains("better-auth-passkey="));
let body = response.body.field_value().unwrap();
let body = body.as_object().unwrap().snapshot_fields().unwrap();
let descriptor = body[property]
.as_array()
.unwrap()
.get(0)
.unwrap()
.unwrap()
.as_object()
.unwrap()
.snapshot_fields()
.unwrap();
assert_eq!(descriptor["id"], FieldValue::from(expected_id));
assert_eq!(descriptor["transports"], expected_transports);
assert_eq!(descriptor["type"], FieldValue::from("public-key"));
let bytes = response.body.bytes().unwrap();
let serialized = FieldValue::parse_json(std::str::from_utf8(&bytes).unwrap()).unwrap();
let serialized = serialized.as_object().unwrap().snapshot_fields().unwrap();
let descriptor = serialized[property]
.as_array()
.unwrap()
.get(0)
.unwrap()
.unwrap()
.as_object()
.unwrap()
.snapshot_fields()
.unwrap();
assert_eq!(
descriptor.get("transports"),
(!expected_transports.is_undefined()).then_some(&expected_transports)
);
}
let stored = raw
.get_passkey_by_id(passkey.id.typed().unwrap())
.await
.unwrap()
.unwrap();
assert_eq!(stored.credential_id, passkey.credential_id);
assert_eq!(stored.transports, passkey.transports);
}
}