Skip to content

Commit 331eeb7

Browse files
committed
feat: Implement passkey support from keystone-ng
Allow user register and authenticate with the passkeys (security device) using the keystone-ng project. This is currently an optional feature since the keystone-ng is not deployed by anybody productively.
1 parent d99a753 commit 331eeb7

13 files changed

Lines changed: 757 additions & 9 deletions

File tree

‎.github/workflows/functional.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@ jobs:
7777
- name: Execute functional tests
7878
env:
7979
OS_CLOUD: devstack
80-
run: cargo nextest run --test functional --all-features -- --skip network::v2::auto_allocated_topology::list::list
80+
run: cargo nextest run --test functional

‎Cargo.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ repository = "https://github.com/gtema/openstack"
1919

2020
[workspace.dependencies]
2121
async-trait = { version = "^0.1" } #, optional = true }
22+
base64 = "0.22"
2223
bytes = "^1.10"
2324
chrono = { version = "^0.4", default-features = false, features = ["clock", "serde"] }
2425
clap = { version = "^4.5", features = ["cargo", "color", "derive", "env"] }
@@ -51,6 +52,8 @@ tracing = "^0.1"
5152
tracing-error = { version = "^0.2" }
5253
tracing-subscriber = { version = "^0.3", features = ["env-filter", "serde"] }
5354
url = { version = "^2.5", features = ["serde"] }
55+
webauthn-authenticator-rs = { version = "0.5", features = ["ctap2", "mozilla", "ui-cli"]}
56+
webauthn-rs-proto = { version = "0.5" }
5457

5558
[profile.dev]
5659
debug = 0

‎openstack_cli/Cargo.toml‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ network = ["openstack_sdk/network"]
4646
object_store = ["openstack_sdk/object_store"]
4747
placement = ["openstack_sdk/placement"]
4848
keystone_ng = ["openstack_sdk/keystone_ng", "openstack_types/keystone_ng"]
49-
passkey = ["dep:webauthn-authenticator-rs", "dep:webauthn-rs-proto"]
49+
passkey = ["keystone_ng", "openstack_sdk/passkey", "dep:webauthn-authenticator-rs", "dep:webauthn-rs-proto"]
5050

5151
_test_admin = []
5252
_test_net_auto-allocated-topology = []
@@ -57,10 +57,9 @@ _test_net_logging = []
5757
_test_net_network-ip-availability = []
5858
_test_net_network-segment-range = []
5959
_test_net_vpn = []
60-
webauthn-rs-proto = ["dep:webauthn-rs-proto"]
6160

6261
[dependencies]
63-
base64 = "0.22"
62+
base64 = { workspace = true }
6463
bytes = { workspace = true }
6564
config.workspace = true
6665
chrono = { workspace= true }
@@ -72,6 +71,7 @@ dialoguer = { workspace = true, features=["fuzzy-select"] }
7271
dirs = { workspace = true }
7372
eyre = { workspace = true }
7473
http = { workspace = true }
74+
itertools = { workspace = true }
7575
json-patch = { workspace = true }
7676
openstack_sdk = { path="../openstack_sdk", version = "^0.22", default-features = false, features = ["async", "identity"] }
7777
openstack_types = { path="../openstack_types", version = "^0.22" }
@@ -90,10 +90,8 @@ thiserror = { workspace = true }
9090
tracing = { workspace = true}
9191
tracing-subscriber = { workspace = true }
9292
url = { workspace = true }
93-
itertools = { workspace = true }
94-
webauthn-authenticator-rs = { optional = true, version = "0.5", features = ["ctap2", "mozilla", "ui-cli"]}
95-
webauthn-rs-proto = { version = "0.5", optional = true }
96-
93+
webauthn-authenticator-rs = { workspace = true, optional = true }
94+
webauthn-rs-proto = { workspace = true, optional = true }
9795

9896
[dev-dependencies]
9997
assert_cmd = "^2.0"

‎openstack_sdk/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ async = []
4141
client_der = []
4242
client_pem = []
4343
keystone_ng = []
44+
passkey = ["keystone_ng", "dep:webauthn-authenticator-rs", "dep:webauthn-rs-proto"]
4445

4546
[dependencies]
4647
async-trait = {workspace = true}
48+
base64 = { workspace = true }
4749
bincode = { version = "^2.0", default-features = false, features = ["serde", "std"] }
4850
bytes = {workspace = true}
4951
chrono = { workspace= true }
@@ -73,6 +75,8 @@ tokio = { workspace = true }
7375
tokio-util = {workspace = true}
7476
tracing = { workspace = true}
7577
url = { workspace = true }
78+
webauthn-authenticator-rs = { workspace = true, optional = true }
79+
webauthn-rs-proto = { workspace = true, optional = true }
7680

7781
[dev-dependencies]
7882
httpmock = "^0.7"

‎openstack_sdk/src/auth.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub mod v3totp;
3838
pub mod v3websso;
3939
#[cfg(feature = "keystone_ng")]
4040
pub mod v4federation;
41+
#[cfg(feature = "passkey")]
42+
pub mod v4passkey;
4143

4244
use authtoken::{AuthToken, AuthTokenError};
4345
use authtoken_scope::AuthTokenScopeError;
@@ -103,6 +105,15 @@ impl From<FederationError> for AuthError {
103105
}
104106
}
105107

108+
#[cfg(feature = "passkey")]
109+
impl From<v4passkey::PasskeyError> for AuthError {
110+
fn from(source: v4passkey::PasskeyError) -> Self {
111+
Self::AuthToken {
112+
source: source.into(),
113+
}
114+
}
115+
}
116+
106117
/// Authentication state enum
107118
#[derive(Debug, Eq, PartialEq)]
108119
pub enum AuthState {

‎openstack_sdk/src/auth/authtoken.rs‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ use crate::api::RestEndpoint;
2828
use crate::auth::auth_token_endpoint as token_v3;
2929
#[cfg(feature = "keystone_ng")]
3030
use crate::auth::v4federation;
31+
#[cfg(feature = "passkey")]
32+
use crate::auth::v4passkey;
3133
use crate::auth::{
3234
auth_helper::AuthHelper, authtoken_scope, v3applicationcredential, v3oidcaccesstoken,
3335
v3password, v3token, v3totp, v3websso, AuthState,
@@ -166,6 +168,15 @@ pub enum AuthTokenError {
166168
#[from]
167169
source: v4federation::FederationError,
168170
},
171+
172+
/// Passkey error
173+
#[cfg(feature = "passkey")]
174+
#[error("Passkey based authentication error: {}", source)]
175+
Passkey {
176+
/// The error source
177+
#[from]
178+
source: v4passkey::PasskeyError,
179+
},
169180
}
170181

171182
type AuthResult<T> = Result<T, AuthTokenError>;
@@ -261,9 +272,12 @@ pub enum AuthType {
261272
V3Multifactor,
262273
/// WebSSO
263274
V3WebSso,
264-
/// Federation
265275
#[cfg(feature = "keystone_ng")]
276+
/// Federation.
266277
V4Federation,
278+
#[cfg(feature = "passkey")]
279+
/// Passkey.
280+
V4Passkey,
267281
}
268282

269283
impl FromStr for AuthType {
@@ -282,6 +296,8 @@ impl FromStr for AuthType {
282296
"v3websso" => Ok(Self::V3WebSso),
283297
#[cfg(feature = "keystone_ng")]
284298
"v4federation" | "federation" => Ok(Self::V4Federation),
299+
#[cfg(feature = "passkey")]
300+
"v4passkey" | "passkey" => Ok(Self::V4Passkey),
285301
other => Err(Self::Err::IdentityMethod {
286302
auth_type: other.into(),
287303
}),
@@ -311,6 +327,8 @@ impl AuthType {
311327
Self::V3WebSso => "v3websso",
312328
#[cfg(feature = "keystone_ng")]
313329
Self::V4Federation => "v4federation",
330+
#[cfg(feature = "passkey")]
331+
Self::V4Passkey => "v4passkey",
314332
}
315333
}
316334
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License");
2+
// you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
//
13+
// SPDX-License-Identifier: Apache-2.0
14+
15+
//! Passkey (security device) based login.
16+
//!
17+
18+
mod error;
19+
mod finish;
20+
mod start;
21+
22+
pub use error::PasskeyError;
23+
pub use finish::get_finish_auth_ep;
24+
pub use start::{get_init_auth_ep, PasskeyAuthenticationStartResponse};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License");
2+
// you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
//
13+
// SPDX-License-Identifier: Apache-2.0
14+
15+
//! Passkey (security device) based login.
16+
//!
17+
18+
use thiserror::Error;
19+
use tracing::error;
20+
21+
use super::finish;
22+
use super::start;
23+
24+
/// Passkey auth related errors.
25+
#[derive(Debug, Error)]
26+
#[non_exhaustive]
27+
pub enum PasskeyError {
28+
/// Auth data is missing
29+
#[error("auth data is missing")]
30+
MissingAuthData,
31+
32+
/// Start authentication request builder.
33+
#[error("error preparing auth request: {}", source)]
34+
InitAuthBuilder {
35+
#[from]
36+
source: start::AuthStartRequestBuilderError,
37+
},
38+
39+
/// Start authentication passkey request builder.
40+
#[error("error preparing auth request: {}", source)]
41+
InitPasskeyBuilder {
42+
#[from]
43+
source: start::PasskeyBuilderError,
44+
},
45+
46+
/// Finish authentication request builder.
47+
#[error(transparent)]
48+
FinishPasskeyBuilder {
49+
#[from]
50+
source: finish::AuthFinishRequestBuilderError,
51+
},
52+
53+
/// HmacGetSecretOutput.
54+
#[error(transparent)]
55+
HmacGetSecretOutput {
56+
#[from]
57+
source: finish::HmacGetSecretOutputBuilderError,
58+
},
59+
60+
/// Passkey auth extensions.
61+
#[error(transparent)]
62+
AuthExtensions {
63+
#[from]
64+
source: finish::AuthenticationExtensionsClientOutputsBuilderError,
65+
},
66+
67+
/// Passkey auth response.
68+
#[error(transparent)]
69+
AuthResponse {
70+
#[from]
71+
source: finish::AuthenticatorAssertionResponseRawBuilderError,
72+
},
73+
/// Base64 decode.
74+
#[error(transparent)]
75+
Base64 {
76+
#[from]
77+
source: base64::DecodeError,
78+
},
79+
80+
/// WebAuthn.
81+
#[error(transparent)]
82+
WebAuthn {
83+
#[from]
84+
source: webauthn_authenticator_rs::error::WebauthnCError,
85+
},
86+
}

0 commit comments

Comments
 (0)