fix: send browser origin for passkey options

This commit is contained in:
Keisuke Hirata 2026-07-23 18:03:22 +09:00
parent 28304f13c3
commit cd6468bb92
No known key found for this signature in database
2 changed files with 32 additions and 6 deletions

View File

@ -2507,6 +2507,8 @@ struct PasskeyRegistrationOptionsRequest {
handle: String, handle: String,
#[serde(default)] #[serde(default)]
display_name: Option<String>, display_name: Option<String>,
#[serde(default)]
browser_origin: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -2527,6 +2529,8 @@ struct PasskeyRegistrationCompleteRequest {
struct PasskeyLoginOptionsRequest { struct PasskeyLoginOptionsRequest {
#[serde(default)] #[serde(default)]
handle: Option<String>, handle: Option<String>,
#[serde(default)]
browser_origin: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -2619,7 +2623,12 @@ async fn post_passkey_registration_options(
Json(request): Json<PasskeyRegistrationOptionsRequest>, Json(request): Json<PasskeyRegistrationOptionsRequest>,
) -> ApiResult<Json<PasskeyRegistrationOptionsResponse>> { ) -> ApiResult<Json<PasskeyRegistrationOptionsResponse>> {
let user = ensure_user_account(&api, &request.handle, request.display_name.as_deref())?; let user = ensure_user_account(&api, &request.handle, request.display_name.as_deref())?;
let auth = auth_config_for_origin(&api.config, request_origin(&headers).as_deref())?; let header_origin = request_origin(&headers);
let requested_origin = request
.browser_origin
.as_deref()
.or(header_origin.as_deref());
let auth = auth_config_for_origin(&api.config, requested_origin)?;
let webauthn = webauthn_for_auth(&auth)?; let webauthn = webauthn_for_auth(&auth)?;
let exclude_credentials = passkeys_for_user(&api, &user.user_id)? let exclude_credentials = passkeys_for_user(&api, &user.user_id)?
.into_iter() .into_iter()
@ -2746,7 +2755,12 @@ async fn post_passkey_login_options(
) )
.into()); .into());
} }
let auth = auth_config_for_origin(&api.config, request_origin(&headers).as_deref())?; let header_origin = request_origin(&headers);
let requested_origin = request
.browser_origin
.as_deref()
.or(header_origin.as_deref());
let auth = auth_config_for_origin(&api.config, requested_origin)?;
let webauthn = webauthn_for_auth(&auth)?; let webauthn = webauthn_for_auth(&auth)?;
let (public_key, state) = webauthn let (public_key, state) = webauthn
.start_passkey_authentication(&passkeys) .start_passkey_authentication(&passkeys)
@ -3087,9 +3101,17 @@ fn auth_config_for_origin(
"request Origin header does not contain a host", "request Origin header does not contain a host",
) )
})?; })?;
if host == auth.rp_id { if host != auth.rp_id {
auth.origin = request_origin.to_string(); return Err(auth_error(
"webauthn_origin_rp_id_mismatch",
&format!(
"browser origin host {host} does not match configured RP ID {}",
auth.rp_id
),
)
.into());
} }
auth.origin = request_origin.to_string();
Ok(auth) Ok(auth)
} }

View File

@ -19,6 +19,10 @@ async function jsonOrThrow<T>(response: Response): Promise<T> {
return text ? JSON.parse(text) as T : (null as T); return text ? JSON.parse(text) as T : (null as T);
} }
function browserOrigin(): string | null {
return globalThis.location?.origin ?? null;
}
export async function loadWhoami(fetcher: typeof fetch = fetch): Promise<WhoamiResponse> { export async function loadWhoami(fetcher: typeof fetch = fetch): Promise<WhoamiResponse> {
return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(jsonOrThrow<WhoamiResponse>); return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(jsonOrThrow<WhoamiResponse>);
} }
@ -32,7 +36,7 @@ export async function registerPasskey(
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
credentials: "same-origin", credentials: "same-origin",
body: JSON.stringify({ handle, display_name: displayName }), body: JSON.stringify({ handle, display_name: displayName, browser_origin: browserOrigin() }),
}).then(jsonOrThrow<PasskeyRegistrationOptionsResponse>); }).then(jsonOrThrow<PasskeyRegistrationOptionsResponse>);
const credential = await navigator.credentials.create({ const credential = await navigator.credentials.create({
@ -61,7 +65,7 @@ export async function loginWithPasskey(
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
credentials: "same-origin", credentials: "same-origin",
body: JSON.stringify({ handle }), body: JSON.stringify({ handle, browser_origin: browserOrigin() }),
}).then(jsonOrThrow<PasskeyLoginOptionsResponse>); }).then(jsonOrThrow<PasskeyLoginOptionsResponse>);
const credential = await navigator.credentials.get({ const credential = await navigator.credentials.get({