From a8af4cbc9a5a5042fbfbca640272912cf309dacd Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 20 Jan 2026 17:23:54 +0100 Subject: [PATCH] feat: Add WebAuthN RP support Add relying_party configuration to the config so that the webauthn_authenticator can start checking the RP. --- .github/actions/deploy_keystone/action.yml | 4 ++ src/bin/keystone.rs | 13 +++-- src/config.rs | 6 +++ src/config/webauthn.rs | 50 +++++++++++++++++++ src/error.rs | 5 +- src/webauthn/api.rs | 23 +++++---- src/webauthn/error.rs | 12 +++++ tests/api/webauthn/register.rs | 4 +- tests/api/webauthn/roundtrip.rs | 2 +- .../keystone/overlays/dev/conf/keystone.conf | 5 ++ 10 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 src/config/webauthn.rs diff --git a/.github/actions/deploy_keystone/action.yml b/.github/actions/deploy_keystone/action.yml index a50cbd8b..351e1eb6 100644 --- a/.github/actions/deploy_keystone/action.yml +++ b/.github/actions/deploy_keystone/action.yml @@ -45,6 +45,10 @@ runs: key_repository = $(pwd)/etc/fernet-keys [fernet_tokens] key_repository = $(pwd)/etc/fernet-keys + [webauthn] + enabled = true + relying_party_id = local + relying_party_origin = https://keystone.local EOF cat etc/keystone.conf echo "2Rlc-npWYOGqqG1zM-bmfBj2apLacLXhIbBsdyqQ0zg=" > etc/fernet-keys/0 diff --git a/src/bin/keystone.rs b/src/bin/keystone.rs index 7b322b79..8d32a2a5 100644 --- a/src/bin/keystone.rs +++ b/src/bin/keystone.rs @@ -215,10 +215,15 @@ async fn main() -> Result<(), Report> { // propagate the header to the response before the response reaches `TraceLayer` .layer(PropagateRequestIdLayer::new(x_request_id)); - let webauthn_extension = webauthn::api::init_extension(shared_state.clone())?; - let app = Router::new() - .merge(main_router.with_state(shared_state.clone())) - .nest("/v4", webauthn_extension) + let mut app = Router::new().merge(main_router.with_state(shared_state.clone())); + + if shared_state.config.webauthn.enabled { + info!("Not enabling the WebAuthN extension due to the `config.webauthn.enabled` flag."); + let webauthn_extension = webauthn::api::init_extension(shared_state.clone())?; + app = app.nest("/v4", webauthn_extension); + } + + app = app .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi)) .layer(middleware); diff --git a/src/config.rs b/src/config.rs index 3062da1a..4b4228e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,6 +36,7 @@ mod revoke; mod security_compliance; mod token; mod trust; +mod webauthn; use application_credentials::ApplicationCredentialProvider; use assignment::AssignmentProvider; @@ -54,6 +55,7 @@ use security_compliance::SecurityComplianceProvider; use token::TokenProvider; pub use token::TokenProviderDriver; use trust::TrustProvider; +use webauthn::WebauthnSection; /// Keystone configuration. #[derive(Debug, Default, Deserialize, Clone)] @@ -120,6 +122,10 @@ pub struct Config { /// Trust provider configuration. #[serde(default)] pub trust: TrustProvider, + + /// Webauthn configuration. + #[serde(default)] + pub webauthn: WebauthnSection, } impl Config { diff --git a/src/config/webauthn.rs b/src/config/webauthn.rs new file mode 100644 index 00000000..2db2b956 --- /dev/null +++ b/src/config/webauthn.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//! # Keystone configuration +//! +//! Parsing of the Keystone configuration file implementation. +use serde::{Deserialize, Serialize}; +use url::Url; + +/// WebauthN configuration. +#[derive(Clone, Debug, Default, Deserialize)] +pub struct WebauthnSection { + /// Enable WebauthN support. + #[serde(default)] + pub enabled: bool, + /// The relying party configuration for the WebauthN. + #[serde(default, flatten)] + pub relying_party: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RelyingParty { + /// The relying party ID. `relying_party_id` is what Credentials + /// (Authenticators) bind themselves to - `relying_party_id` can NOT be + /// changed without breaking all of users' associated credentials in the + /// future! `relying_party_id` must be an effective domain of + /// `relying_party_origin`. This means that if you are hosting `https://idm.example.com`, + /// `relying_party_id` must be `idm.example.com`, `example.com` + /// or `com`. + #[serde(rename = "relying_party_id")] + pub id: String, + + /// The relying party name. This may be shown to the user. + #[serde(default, rename = "relying_party_name")] + pub name: Option, + + /// The relying party origin url. It must contain the scheme (i.e. `http://localhost`. + #[serde(rename = "relying_party_origin")] + pub origin: Url, +} diff --git a/src/error.rs b/src/error.rs index 6a2d7617..c31774a0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,6 +27,7 @@ use crate::resource::error::*; use crate::revoke::error::*; use crate::token::TokenProviderError; use crate::trust::TrustError; +use crate::webauthn::WebauthnError; /// Keystone error. #[derive(Debug, Error)] @@ -147,11 +148,11 @@ pub enum KeystoneError { }, /// WebauthN error. - #[error("webauthn error: {}", source)] + #[error(transparent)] Webauthn { /// The source of the error. #[from] - source: webauthn_rs::prelude::WebauthnError, + source: WebauthnError, }, } diff --git a/src/webauthn/api.rs b/src/webauthn/api.rs index 022bd374..e0a46a64 100644 --- a/src/webauthn/api.rs +++ b/src/webauthn/api.rs @@ -17,7 +17,7 @@ use axum::Router; use std::sync::Arc; use utoipa::OpenApi; use utoipa_axum::router::OpenApiRouter; -use webauthn_rs::{WebauthnBuilder, prelude::Url}; +use webauthn_rs::WebauthnBuilder; use crate::error::KeystoneError; use crate::keystone::ServiceState; @@ -26,7 +26,7 @@ mod auth; mod register; pub mod types; -use crate::webauthn::driver::SqlDriver; +use crate::webauthn::{WebauthnError, driver::SqlDriver}; use types::{CombinedExtensionState, ExtensionState}; /// OpenApi specification for the user passkey support. @@ -47,21 +47,26 @@ pub fn openapi_router() -> OpenApiRouter { /// Initialize the extension. pub fn init_extension(main_state: ServiceState) -> Result { - // Effective domain name. - let rp_id = "localhost"; // Url containing the effective domain name - // TODO: This must come from the configuration file. // MUST include the port number! - let rp_origin = Url::parse("http://localhost:8080")?; - let builder = WebauthnBuilder::new(rp_id, &rp_origin)?; + let rp = main_state + .config + .webauthn + .relying_party + .as_ref() + .ok_or(WebauthnError::RelyingPartyConfigurationUnset)?; + + let mut builder = WebauthnBuilder::new(&rp.id, &rp.origin).map_err(WebauthnError::from)?; // Now, with the builder you can define other options. // Set a "nice" relying party name. Has no security properties and // may be changed in the future. - let builder = builder.rp_name("Keystone"); + if let Some(name) = &rp.name { + builder = builder.rp_name(name); + } // Consume the builder and create our webauthn instance. - let webauthn = builder.build()?; + let webauthn = builder.build().map_err(WebauthnError::from)?; let extension_state = Arc::new(ExtensionState { provider: SqlDriver::default(), diff --git a/src/webauthn/error.rs b/src/webauthn/error.rs index eb5fbad5..5cf0b5bc 100644 --- a/src/webauthn/error.rs +++ b/src/webauthn/error.rs @@ -51,6 +51,10 @@ pub enum WebauthnError { source: DatabaseError, }, + /// Relying party configuration is missing. + #[error("webauthn relying party configuration is missing")] + RelyingPartyConfigurationUnset, + /// (de)serialization error. #[error(transparent)] Serde { @@ -62,4 +66,12 @@ pub enum WebauthnError { /// Int conversion error. #[error(transparent)] TryFromIntError(#[from] std::num::TryFromIntError), + + /// WebauthN error. + #[error("webauthn error: {}", source)] + Webauthn { + /// The source of the error. + #[from] + source: webauthn_rs::prelude::WebauthnError, + }, } diff --git a/tests/api/webauthn/register.rs b/tests/api/webauthn/register.rs index 3be8a4cb..1ea9469a 100644 --- a/tests/api/webauthn/register.rs +++ b/tests/api/webauthn/register.rs @@ -40,7 +40,7 @@ async fn test_register_empty_description() -> Result<()> { let authenticator_backend = SoftToken::new(true)?.0; let mut authenticator = WebauthnAuthenticator::new(authenticator_backend); - let origin = Url::parse("http://localhost:8080")?; + let origin = Url::parse("https://keystone.local")?; register_user_passkey( &test_client, @@ -69,7 +69,7 @@ async fn test_register_description() -> Result<()> { let authenticator_backend = SoftToken::new(true)?.0; let mut authenticator = WebauthnAuthenticator::new(authenticator_backend); - let origin = Url::parse("http://localhost:8080")?; + let origin = Url::parse("https://keystone.local")?; register_user_passkey( &test_client, diff --git a/tests/api/webauthn/roundtrip.rs b/tests/api/webauthn/roundtrip.rs index 36d8a34b..351ac97a 100644 --- a/tests/api/webauthn/roundtrip.rs +++ b/tests/api/webauthn/roundtrip.rs @@ -40,7 +40,7 @@ async fn test_register_auth() -> Result<()> { let authenticator_backend = SoftToken::new(true)?.0; let mut authenticator = WebauthnAuthenticator::new(authenticator_backend); - let origin = Url::parse("http://localhost:8080")?; + let origin = Url::parse("https://keystone.local")?; register_user_passkey( &test_client, diff --git a/tools/k8s/keystone/overlays/dev/conf/keystone.conf b/tools/k8s/keystone/overlays/dev/conf/keystone.conf index 8502e2a1..5f8ae309 100644 --- a/tools/k8s/keystone/overlays/dev/conf/keystone.conf +++ b/tools/k8s/keystone/overlays/dev/conf/keystone.conf @@ -6,3 +6,8 @@ methods = password,token,openid,application_credential,x509 [api_policy] opa_base_url = http://localhost:8181 + +[webauthn] +relying_party_id = local +relying_party_origin = https://keystone.local +relying_party_name = keystone