diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index e7fec3bf..c67f08f1 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -2507,6 +2507,8 @@ struct PasskeyRegistrationOptionsRequest { handle: String, #[serde(default)] display_name: Option, + #[serde(default)] + browser_origin: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -2527,6 +2529,8 @@ struct PasskeyRegistrationCompleteRequest { struct PasskeyLoginOptionsRequest { #[serde(default)] handle: Option, + #[serde(default)] + browser_origin: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -2619,7 +2623,12 @@ async fn post_passkey_registration_options( Json(request): Json, ) -> ApiResult> { 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 exclude_credentials = passkeys_for_user(&api, &user.user_id)? .into_iter() @@ -2746,7 +2755,12 @@ async fn post_passkey_login_options( ) .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 (public_key, state) = webauthn .start_passkey_authentication(&passkeys) @@ -3087,9 +3101,17 @@ fn auth_config_for_origin( "request Origin header does not contain a host", ) })?; - if host == auth.rp_id { - auth.origin = request_origin.to_string(); + if host != auth.rp_id { + 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) } diff --git a/web/workspace/src/lib/workspace/auth/api.ts b/web/workspace/src/lib/workspace/auth/api.ts index 262aba36..08210e6b 100644 --- a/web/workspace/src/lib/workspace/auth/api.ts +++ b/web/workspace/src/lib/workspace/auth/api.ts @@ -19,6 +19,10 @@ async function jsonOrThrow(response: Response): Promise { 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 { return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(jsonOrThrow); } @@ -32,7 +36,7 @@ export async function registerPasskey( method: "POST", headers: { "content-type": "application/json" }, credentials: "same-origin", - body: JSON.stringify({ handle, display_name: displayName }), + body: JSON.stringify({ handle, display_name: displayName, browser_origin: browserOrigin() }), }).then(jsonOrThrow); const credential = await navigator.credentials.create({ @@ -61,7 +65,7 @@ export async function loginWithPasskey( method: "POST", headers: { "content-type": "application/json" }, credentials: "same-origin", - body: JSON.stringify({ handle }), + body: JSON.stringify({ handle, browser_origin: browserOrigin() }), }).then(jsonOrThrow); const credential = await navigator.credentials.get({