Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/deploy_keystone/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions src/bin/keystone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod revoke;
mod security_compliance;
mod token;
mod trust;
mod webauthn;

use application_credentials::ApplicationCredentialProvider;
use assignment::AssignmentProvider;
Expand All @@ -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)]
Expand Down Expand Up @@ -120,6 +122,10 @@ pub struct Config {
/// Trust provider configuration.
#[serde(default)]
pub trust: TrustProvider,

/// Webauthn configuration.
#[serde(default)]
pub webauthn: WebauthnSection,
}

impl Config {
Expand Down
50 changes: 50 additions & 0 deletions src/config/webauthn.rs
Original file line number Diff line number Diff line change
@@ -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<RelyingParty>,
}

#[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<String>,

/// The relying party origin url. It must contain the scheme (i.e. `http://localhost`.
#[serde(rename = "relying_party_origin")]
pub origin: Url,
}
5 changes: 3 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
},
}

Expand Down
23 changes: 14 additions & 9 deletions src/webauthn/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -47,21 +47,26 @@ pub fn openapi_router() -> OpenApiRouter<CombinedExtensionState> {

/// Initialize the extension.
pub fn init_extension(main_state: ServiceState) -> Result<Router, KeystoneError> {
// 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(),
Expand Down
12 changes: 12 additions & 0 deletions src/webauthn/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
},
}
4 changes: 2 additions & 2 deletions tests/api/webauthn/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/api/webauthn/roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions tools/k8s/keystone/overlays/dev/conf/keystone.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading