fix: honor browser origin for passkey ceremonies
This commit is contained in:
parent
7740191194
commit
28304f13c3
|
|
@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
||||||
use axum::extract::{Path as AxumPath, Query, State};
|
use axum::extract::{Path as AxumPath, Query, State};
|
||||||
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, SET_COOKIE};
|
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE};
|
||||||
use axum::http::{HeaderMap, StatusCode, Uri};
|
use axum::http::{HeaderMap, StatusCode, Uri};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::{delete, get, post, put};
|
use axum::routing::{delete, get, post, put};
|
||||||
|
|
@ -2615,10 +2615,12 @@ async fn post_auth_bootstrap_user(
|
||||||
|
|
||||||
async fn post_passkey_registration_options(
|
async fn post_passkey_registration_options(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
|
headers: HeaderMap,
|
||||||
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 webauthn = webauthn(&api.config)?;
|
let auth = auth_config_for_origin(&api.config, request_origin(&headers).as_deref())?;
|
||||||
|
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()
|
||||||
.map(|passkey| passkey.cred_id().clone())
|
.map(|passkey| passkey.cred_id().clone())
|
||||||
|
|
@ -2633,7 +2635,6 @@ async fn post_passkey_registration_options(
|
||||||
)
|
)
|
||||||
.map_err(|error| auth_error("webauthn_registration_options_failed", &error.to_string()))?;
|
.map_err(|error| auth_error("webauthn_registration_options_failed", &error.to_string()))?;
|
||||||
let challenge_id = new_id("webauthn-registration");
|
let challenge_id = new_id("webauthn-registration");
|
||||||
let auth = auth_public_config(&api.config);
|
|
||||||
api.store.put_auth_challenge(&AuthChallengeRecord {
|
api.store.put_auth_challenge(&AuthChallengeRecord {
|
||||||
challenge_id: challenge_id.clone(),
|
challenge_id: challenge_id.clone(),
|
||||||
ceremony: "passkey_registration".to_string(),
|
ceremony: "passkey_registration".to_string(),
|
||||||
|
|
@ -2680,7 +2681,7 @@ async fn post_passkey_registration_complete(
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
let user_id = challenge.user_id.ok_or_else(|| {
|
let user_id = challenge.user_id.clone().ok_or_else(|| {
|
||||||
auth_error(
|
auth_error(
|
||||||
"invalid_passkey_challenge",
|
"invalid_passkey_challenge",
|
||||||
"passkey registration challenge is not bound to a user",
|
"passkey registration challenge is not bound to a user",
|
||||||
|
|
@ -2692,7 +2693,7 @@ async fn post_passkey_registration_complete(
|
||||||
"passkey registration user does not exist",
|
"passkey registration user does not exist",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let state_json = challenge.state_json.ok_or_else(|| {
|
let state_json = challenge.state_json.clone().ok_or_else(|| {
|
||||||
auth_error(
|
auth_error(
|
||||||
"missing_webauthn_state",
|
"missing_webauthn_state",
|
||||||
"passkey registration state was not persisted",
|
"passkey registration state was not persisted",
|
||||||
|
|
@ -2700,7 +2701,7 @@ async fn post_passkey_registration_complete(
|
||||||
})?;
|
})?;
|
||||||
let state: PasskeyRegistration = serde_json::from_str(&state_json)
|
let state: PasskeyRegistration = serde_json::from_str(&state_json)
|
||||||
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
|
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
|
||||||
let webauthn = webauthn(&api.config)?;
|
let webauthn = webauthn_for_challenge(&api.config, &challenge)?;
|
||||||
let passkey = webauthn
|
let passkey = webauthn
|
||||||
.finish_passkey_registration(&request.credential, &state)
|
.finish_passkey_registration(&request.credential, &state)
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
|
|
@ -2729,6 +2730,7 @@ async fn post_passkey_registration_complete(
|
||||||
|
|
||||||
async fn post_passkey_login_options(
|
async fn post_passkey_login_options(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
|
headers: HeaderMap,
|
||||||
Json(request): Json<PasskeyLoginOptionsRequest>,
|
Json(request): Json<PasskeyLoginOptionsRequest>,
|
||||||
) -> ApiResult<Json<PasskeyLoginOptionsResponse>> {
|
) -> ApiResult<Json<PasskeyLoginOptionsResponse>> {
|
||||||
let user = match request.handle.as_deref() {
|
let user = match request.handle.as_deref() {
|
||||||
|
|
@ -2744,12 +2746,12 @@ async fn post_passkey_login_options(
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
let webauthn = webauthn(&api.config)?;
|
let auth = auth_config_for_origin(&api.config, request_origin(&headers).as_deref())?;
|
||||||
|
let webauthn = webauthn_for_auth(&auth)?;
|
||||||
let (public_key, state) = webauthn
|
let (public_key, state) = webauthn
|
||||||
.start_passkey_authentication(&passkeys)
|
.start_passkey_authentication(&passkeys)
|
||||||
.map_err(|error| auth_error("webauthn_login_options_failed", &error.to_string()))?;
|
.map_err(|error| auth_error("webauthn_login_options_failed", &error.to_string()))?;
|
||||||
let challenge_id = new_id("webauthn-login");
|
let challenge_id = new_id("webauthn-login");
|
||||||
let auth = auth_public_config(&api.config);
|
|
||||||
api.store.put_auth_challenge(&AuthChallengeRecord {
|
api.store.put_auth_challenge(&AuthChallengeRecord {
|
||||||
challenge_id: challenge_id.clone(),
|
challenge_id: challenge_id.clone(),
|
||||||
ceremony: "passkey_login".to_string(),
|
ceremony: "passkey_login".to_string(),
|
||||||
|
|
@ -2796,7 +2798,7 @@ async fn post_passkey_login_complete(
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
let user_id = challenge.user_id.ok_or_else(|| {
|
let user_id = challenge.user_id.clone().ok_or_else(|| {
|
||||||
auth_error(
|
auth_error(
|
||||||
"invalid_passkey_challenge",
|
"invalid_passkey_challenge",
|
||||||
"passkey login challenge is not bound to a user",
|
"passkey login challenge is not bound to a user",
|
||||||
|
|
@ -2806,7 +2808,7 @@ async fn post_passkey_login_complete(
|
||||||
.store
|
.store
|
||||||
.get_user(&user_id)?
|
.get_user(&user_id)?
|
||||||
.ok_or_else(|| auth_error("unknown_auth_user", "passkey user does not exist"))?;
|
.ok_or_else(|| auth_error("unknown_auth_user", "passkey user does not exist"))?;
|
||||||
let state_json = challenge.state_json.ok_or_else(|| {
|
let state_json = challenge.state_json.clone().ok_or_else(|| {
|
||||||
auth_error(
|
auth_error(
|
||||||
"missing_webauthn_state",
|
"missing_webauthn_state",
|
||||||
"passkey login state was not persisted",
|
"passkey login state was not persisted",
|
||||||
|
|
@ -2814,7 +2816,7 @@ async fn post_passkey_login_complete(
|
||||||
})?;
|
})?;
|
||||||
let state: PasskeyAuthentication = serde_json::from_str(&state_json)
|
let state: PasskeyAuthentication = serde_json::from_str(&state_json)
|
||||||
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
|
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
|
||||||
let webauthn = webauthn(&api.config)?;
|
let webauthn = webauthn_for_challenge(&api.config, &challenge)?;
|
||||||
let auth_result = webauthn
|
let auth_result = webauthn
|
||||||
.finish_passkey_authentication(&request.credential, &state)
|
.finish_passkey_authentication(&request.credential, &state)
|
||||||
.map_err(|error| auth_error("webauthn_login_verification_failed", &error.to_string()))?;
|
.map_err(|error| auth_error("webauthn_login_verification_failed", &error.to_string()))?;
|
||||||
|
|
@ -3060,8 +3062,48 @@ async fn post_auth_logout(
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn webauthn(config: &ServerConfig) -> ApiResult<Webauthn> {
|
fn request_origin(headers: &HeaderMap) -> Option<String> {
|
||||||
let auth = auth_public_config(config);
|
headers
|
||||||
|
.get(ORIGIN)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_config_for_origin(
|
||||||
|
config: &ServerConfig,
|
||||||
|
request_origin: Option<&str>,
|
||||||
|
) -> ApiResult<AuthPublicConfig> {
|
||||||
|
let mut auth = auth_public_config(config);
|
||||||
|
let Some(request_origin) = request_origin else {
|
||||||
|
return Ok(auth);
|
||||||
|
};
|
||||||
|
let origin = Url::parse(request_origin)
|
||||||
|
.map_err(|error| auth_error("invalid_webauthn_request_origin", &error.to_string()))?;
|
||||||
|
let host = origin.host_str().ok_or_else(|| {
|
||||||
|
auth_error(
|
||||||
|
"invalid_webauthn_request_origin",
|
||||||
|
"request Origin header does not contain a host",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if host == auth.rp_id {
|
||||||
|
auth.origin = request_origin.to_string();
|
||||||
|
}
|
||||||
|
Ok(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn webauthn_for_challenge(
|
||||||
|
config: &ServerConfig,
|
||||||
|
challenge: &AuthChallengeRecord,
|
||||||
|
) -> ApiResult<Webauthn> {
|
||||||
|
let mut auth = auth_public_config(config);
|
||||||
|
auth.rp_id = challenge.rp_id.clone();
|
||||||
|
auth.origin = challenge.origin.clone();
|
||||||
|
webauthn_for_auth(&auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn webauthn_for_auth(auth: &AuthPublicConfig) -> ApiResult<Webauthn> {
|
||||||
let origin = Url::parse(&auth.origin)
|
let origin = Url::parse(&auth.origin)
|
||||||
.map_err(|error| auth_error("invalid_webauthn_origin", &error.to_string()))?;
|
.map_err(|error| auth_error("invalid_webauthn_origin", &error.to_string()))?;
|
||||||
WebauthnBuilder::new(&auth.rp_id, &origin)
|
WebauthnBuilder::new(&auth.rp_id, &origin)
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,18 @@ export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
||||||
const workspaceId = params.workspaceId;
|
const workspaceId = params.workspaceId;
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
|
|
||||||
const publicRoutes = new Set(["/account", "/login/device"]);
|
const publicRoutes = new Set(["/account", "/login/device"]);
|
||||||
if (workspace.data && !publicRoutes.has(url.pathname)) {
|
if (publicRoutes.has(url.pathname)) {
|
||||||
|
return {
|
||||||
|
workspace: null,
|
||||||
|
workspaceError: null,
|
||||||
|
repositories: null,
|
||||||
|
repositoriesError: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
|
||||||
|
if (workspace.data) {
|
||||||
const scopedPath = workspaceRoute(workspace.data.workspace_id);
|
const scopedPath = workspaceRoute(workspace.data.workspace_id);
|
||||||
throw redirect(307, `${scopedPath}${url.search}`);
|
throw redirect(307, `${scopedPath}${url.search}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user