mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 02:35:58 +03:00
Update webauthn-rs to 0.5.x (#5934)
* update webauthn to 0.5 * add basic migration impl * fix clippy warnings * clear up `COSEKeyType::EC_OKP` case * fix TODOs * use same timeout as in webauthn 0.3 impl * fix: clippy warnings and formatting * Update Cargo.toml Co-authored-by: Daniel <daniel.barabasa@gmail.com> * Update src/api/core/two_factor/webauthn.rs Co-authored-by: Daniel <daniel.barabasa@gmail.com> * Update src/api/core/two_factor/webauthn.rs Co-authored-by: Daniel <daniel.barabasa@gmail.com> * Update src/api/core/two_factor/webauthn.rs Co-authored-by: Daniel <daniel.barabasa@gmail.com> * regenerate Cargo.lock * Use securitykey methods * use CredentialsV3 from webauthn-rs instead of own webauthn_0_3 module * fix cargo fmt issue --------- Co-authored-by: Helmut K. C. Tessarek <tessarek@evermeet.cx> Co-authored-by: Daniel <daniel.barabasa@gmail.com> Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
This commit is contained in:
174
Cargo.lock
generated
174
Cargo.lock
generated
@@ -103,6 +103,45 @@ dependencies = [
|
|||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs-derive",
|
||||||
|
"asn1-rs-impl",
|
||||||
|
"displaydoc",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-derive"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-impl"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -661,12 +700,6 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.7"
|
version = "0.21.7"
|
||||||
@@ -695,6 +728,17 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64urlsafedata"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5913e643e4dfb43d5908e9e6f1386f8e0dfde086ecef124a6450c6195d89160"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"pastey",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bigdecimal"
|
name = "bigdecimal"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -1269,6 +1313,20 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der-parser"
|
||||||
|
version = "9.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"displaydoc",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -3164,6 +3222,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oid-registry"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -4330,6 +4397,15 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusticata-macros"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||||
|
dependencies = [
|
||||||
|
"nom 7.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
@@ -4606,10 +4682,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_cbor"
|
name = "serde_cbor_2"
|
||||||
version = "0.11.2"
|
version = "0.12.0-dev"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"half",
|
"half",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5657,6 +5733,8 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webauthn-rs",
|
"webauthn-rs",
|
||||||
|
"webauthn-rs-core",
|
||||||
|
"webauthn-rs-proto",
|
||||||
"which",
|
"which",
|
||||||
"yubico_ng",
|
"yubico_ng",
|
||||||
]
|
]
|
||||||
@@ -5818,22 +5896,71 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webauthn-rs"
|
name = "webauthn-attestation-ca"
|
||||||
version = "0.3.2"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90b266eccb4b32595876f5c73ea443b0516da0b1df72ca07bc08ed9ba7f96ec1"
|
checksum = "384e43534efe4e8f56c4eb1615a27e24d2ff29281385c843cf9f16ac1077dbdc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64urlsafedata",
|
||||||
|
"openssl",
|
||||||
|
"openssl-sys",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-rs"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed1f861a94557baeb0cf711e3e55d623c46b68f4aab7aa932562f785b8b5f1ab"
|
||||||
|
dependencies = [
|
||||||
|
"base64urlsafedata",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"webauthn-rs-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-rs-core"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "269c210cd5f183aaca860bb5733187d1dd110ebed54640f8fc1aca31a04aa4dc"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"base64urlsafedata",
|
||||||
|
"der-parser",
|
||||||
|
"hex",
|
||||||
"nom 7.1.3",
|
"nom 7.1.3",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
"openssl-sys",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"rand_chacha 0.3.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_cbor",
|
"serde_cbor_2",
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"webauthn-attestation-ca",
|
||||||
|
"webauthn-rs-proto",
|
||||||
|
"x509-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-rs-proto"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "144dbee9abb4bfad78fd283a2613f0312a0ed5955051b7864cfc98679112ae60"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"base64urlsafedata",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6295,6 +6422,23 @@ version = "0.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x509-parser"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"data-encoding",
|
||||||
|
"der-parser",
|
||||||
|
"lazy_static",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"oid-registry",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.27"
|
version = "0.8.27"
|
||||||
|
@@ -124,7 +124,12 @@ totp-lite = "2.0.1"
|
|||||||
yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio"], default-features = false }
|
yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
webauthn-rs = "0.3.2"
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
|
# danger-credential-internals is needed to support U2F to Webauthn migration
|
||||||
|
# danger-user-presence-only-security-keys is needed to disable UV
|
||||||
|
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals", "danger-user-presence-only-security-keys"] }
|
||||||
|
webauthn-rs-proto = "0.5.2"
|
||||||
|
webauthn-rs-core = "0.5.2"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
|
@@ -1,9 +1,3 @@
|
|||||||
use rocket::serde::json::Json;
|
|
||||||
use rocket::Route;
|
|
||||||
use serde_json::Value;
|
|
||||||
use url::Url;
|
|
||||||
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{log_user_event, two_factor::_generate_recover_code},
|
core::{log_user_event, two_factor::_generate_recover_code},
|
||||||
@@ -18,6 +12,38 @@ use crate::{
|
|||||||
util::NumberOrString,
|
util::NumberOrString,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::Route;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use webauthn_rs::prelude::{Base64UrlSafeData, SecurityKey, SecurityKeyAuthentication, SecurityKeyRegistration};
|
||||||
|
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||||
|
use webauthn_rs_proto::{
|
||||||
|
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
|
||||||
|
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
|
||||||
|
RequestAuthenticationExtensions,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
||||||
|
let domain = CONFIG.domain();
|
||||||
|
let domain_origin = CONFIG.domain_origin();
|
||||||
|
let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default();
|
||||||
|
let rp_origin = Url::parse(&domain_origin).unwrap();
|
||||||
|
|
||||||
|
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
||||||
|
.expect("Creating WebauthnBuilder failed")
|
||||||
|
.rp_name(&domain)
|
||||||
|
.timeout(Duration::from_millis(60000))
|
||||||
|
.danger_set_user_presence_only_security_keys(true);
|
||||||
|
|
||||||
|
Arc::new(webauthn.build().expect("Building Webauthn failed"))
|
||||||
|
});
|
||||||
|
|
||||||
|
pub type Webauthn2FaConfig<'a> = &'a rocket::State<Arc<Webauthn>>;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
||||||
@@ -45,52 +71,13 @@ pub struct U2FRegistration {
|
|||||||
pub migrated: Option<bool>,
|
pub migrated: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebauthnConfig {
|
|
||||||
url: String,
|
|
||||||
origin: Url,
|
|
||||||
rpid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebauthnConfig {
|
|
||||||
fn load() -> Webauthn<Self> {
|
|
||||||
let domain = CONFIG.domain();
|
|
||||||
let domain_origin = CONFIG.domain_origin();
|
|
||||||
Webauthn::new(Self {
|
|
||||||
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
|
|
||||||
url: domain,
|
|
||||||
origin: Url::parse(&domain_origin).unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl webauthn_rs::WebauthnConfig for WebauthnConfig {
|
|
||||||
fn get_relying_party_name(&self) -> &str {
|
|
||||||
&self.url
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_origin(&self) -> &Url {
|
|
||||||
&self.origin
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_relying_party_id(&self) -> &str {
|
|
||||||
&self.rpid
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We have WebAuthn configured to discourage user verification
|
|
||||||
/// if we leave this enabled, it will cause verification issues when a keys send UV=1.
|
|
||||||
/// Upstream (the library they use) ignores this when set to discouraged, so we should too.
|
|
||||||
fn get_require_uv_consistency(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct WebauthnRegistration {
|
pub struct WebauthnRegistration {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub migrated: bool,
|
pub migrated: bool,
|
||||||
|
|
||||||
pub credential: Credential,
|
pub credential: SecurityKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebauthnRegistration {
|
impl WebauthnRegistration {
|
||||||
@@ -125,7 +112,12 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
||||||
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn generate_webauthn_challenge(
|
||||||
|
data: Json<PasswordOrOtpData>,
|
||||||
|
headers: Headers,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
let data: PasswordOrOtpData = data.into_inner();
|
let data: PasswordOrOtpData = data.into_inner();
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -135,13 +127,13 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
|
|||||||
.await?
|
.await?
|
||||||
.1
|
.1
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options(
|
let (challenge, state) = webauthn.start_securitykey_registration(
|
||||||
user.uuid.as_bytes().to_vec(),
|
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
||||||
user.email,
|
&user.email,
|
||||||
user.name,
|
&user.name,
|
||||||
Some(registrations),
|
Some(registrations),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -193,8 +185,10 @@ impl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential {
|
|||||||
response: AuthenticatorAttestationResponseRaw {
|
response: AuthenticatorAttestationResponseRaw {
|
||||||
attestation_object: r.response.attestation_object,
|
attestation_object: r.response.attestation_object,
|
||||||
client_data_json: r.response.client_data_json,
|
client_data_json: r.response.client_data_json,
|
||||||
|
transports: None,
|
||||||
},
|
},
|
||||||
type_: r.r#type,
|
type_: r.r#type,
|
||||||
|
extensions: RegistrationExtensionsClientOutputs::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +199,7 @@ pub struct PublicKeyCredentialCopy {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub raw_id: Base64UrlSafeData,
|
pub raw_id: Base64UrlSafeData,
|
||||||
pub response: AuthenticatorAssertionResponseRawCopy,
|
pub response: AuthenticatorAssertionResponseRawCopy,
|
||||||
pub extensions: Option<AuthenticationExtensionsClientOutputs>,
|
pub extensions: AuthenticationExtensionsClientOutputs,
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +232,12 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/webauthn", data = "<data>")]
|
#[post("/two-factor/webauthn", data = "<data>")]
|
||||||
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn activate_webauthn(
|
||||||
|
data: Json<EnableWebauthnData>,
|
||||||
|
headers: Headers,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
let data: EnableWebauthnData = data.into_inner();
|
let data: EnableWebauthnData = data.into_inner();
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -253,7 +252,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
|
|||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: RegistrationState = serde_json::from_str(&tf.data)?;
|
let state: SecurityKeyRegistration = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(&mut conn).await?;
|
tf.delete(&mut conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -261,8 +260,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Verify the credentials with the saved state
|
// Verify the credentials with the saved state
|
||||||
let (credential, _data) =
|
let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?;
|
||||||
WebauthnConfig::load().register_credential(&data.device_response.into(), &state, |_| Ok(false))?;
|
|
||||||
|
|
||||||
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||||
// TODO: Check for repeated ID's
|
// TODO: Check for repeated ID's
|
||||||
@@ -291,8 +289,13 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/webauthn", data = "<data>")]
|
#[put("/two-factor/webauthn", data = "<data>")]
|
||||||
async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_webauthn_put(
|
||||||
activate_webauthn(data, headers, conn).await
|
data: Json<EnableWebauthnData>,
|
||||||
|
headers: Headers,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
activate_webauthn(data, headers, webauthn, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -335,7 +338,7 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
|
|||||||
Err(_) => err!("Error parsing U2F data"),
|
Err(_) => err!("Error parsing U2F data"),
|
||||||
};
|
};
|
||||||
|
|
||||||
data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id);
|
data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id().as_slice());
|
||||||
let new_data_str = serde_json::to_string(&data)?;
|
let new_data_str = serde_json::to_string(&data)?;
|
||||||
|
|
||||||
u2f.data = new_data_str;
|
u2f.data = new_data_str;
|
||||||
@@ -362,18 +365,36 @@ pub async fn get_webauthn_registrations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
|
pub async fn generate_webauthn_login(
|
||||||
|
user_id: &UserId,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
// Load saved credentials
|
// Load saved credentials
|
||||||
let creds: Vec<Credential> =
|
let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||||
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
|
||||||
|
|
||||||
if creds.is_empty() {
|
if creds.is_empty() {
|
||||||
err!("No Webauthn devices registered")
|
err!("No Webauthn devices registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a challenge based on the credentials
|
// Generate a challenge based on the credentials
|
||||||
let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build();
|
let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?;
|
||||||
let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?;
|
|
||||||
|
// Modify to discourage user verification
|
||||||
|
let mut state = serde_json::to_value(&state)?;
|
||||||
|
|
||||||
|
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
|
||||||
|
let app_id = format!("{}/app-id.json", &CONFIG.domain());
|
||||||
|
state["ast"]["appid"] = Value::String(app_id.clone());
|
||||||
|
response
|
||||||
|
.public_key
|
||||||
|
.extensions
|
||||||
|
.get_or_insert(RequestAuthenticationExtensions {
|
||||||
|
appid: None,
|
||||||
|
uvm: None,
|
||||||
|
hmac_get_secret: None,
|
||||||
|
})
|
||||||
|
.appid = Some(app_id);
|
||||||
|
|
||||||
// Save the challenge state for later validation
|
// Save the challenge state for later validation
|
||||||
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
||||||
@@ -384,11 +405,16 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> Jso
|
|||||||
Ok(Json(serde_json::to_value(response.public_key)?))
|
Ok(Json(serde_json::to_value(response.public_key)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
|
pub async fn validate_webauthn_login(
|
||||||
|
user_id: &UserId,
|
||||||
|
response: &str,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: AuthenticationState = serde_json::from_str(&tf.data)?;
|
let state: SecurityKeyAuthentication = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(conn).await?;
|
tf.delete(conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -405,13 +431,11 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mu
|
|||||||
|
|
||||||
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
||||||
|
|
||||||
// If the credential we received is migrated from U2F, enable the U2F compatibility
|
let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?;
|
||||||
//let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0);
|
|
||||||
let (cred_id, auth_data) = WebauthnConfig::load().authenticate_credential(&rsp, &state)?;
|
|
||||||
|
|
||||||
for reg in &mut registrations {
|
for reg in &mut registrations {
|
||||||
if ®.credential.cred_id == cred_id {
|
if reg.credential.cred_id() == authentication_result.cred_id() && authentication_result.needs_update() {
|
||||||
reg.credential.counter = auth_data.counter;
|
reg.credential.update_credential(&authentication_result);
|
||||||
|
|
||||||
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||||
.save(conn)
|
.save(conn)
|
||||||
|
@@ -9,6 +9,7 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{
|
core::{
|
||||||
@@ -48,6 +49,7 @@ async fn login(
|
|||||||
data: Form<ConnectData>,
|
data: Form<ConnectData>,
|
||||||
client_header: ClientHeaders,
|
client_header: ClientHeaders,
|
||||||
client_version: Option<ClientVersion>,
|
client_version: Option<ClientVersion>,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: ConnectData = data.into_inner();
|
let data: ConnectData = data.into_inner();
|
||||||
@@ -70,7 +72,7 @@ async fn login(
|
|||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
|
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await
|
||||||
}
|
}
|
||||||
"client_credentials" => {
|
"client_credentials" => {
|
||||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
@@ -91,7 +93,7 @@ async fn login(
|
|||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
|
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await
|
||||||
}
|
}
|
||||||
"authorization_code" => err!("SSO sign-in is not available"),
|
"authorization_code" => err!("SSO sign-in is not available"),
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
@@ -169,6 +171,7 @@ async fn _sso_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
||||||
|
|
||||||
@@ -267,7 +270,7 @@ async fn _sso_login(
|
|||||||
}
|
}
|
||||||
Some((mut user, sso_user)) => {
|
Some((mut user, sso_user)) => {
|
||||||
let mut device = get_device(&data, conn, &user).await?;
|
let mut device = get_device(&data, conn, &user).await?;
|
||||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?;
|
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?;
|
||||||
|
|
||||||
if user.private_key.is_none() {
|
if user.private_key.is_none() {
|
||||||
// User was invited a stub was created
|
// User was invited a stub was created
|
||||||
@@ -322,6 +325,7 @@ async fn _password_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
||||||
@@ -431,7 +435,7 @@ async fn _password_login(
|
|||||||
|
|
||||||
let mut device = get_device(&data, conn, &user).await?;
|
let mut device = get_device(&data, conn, &user).await?;
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?;
|
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?;
|
||||||
|
|
||||||
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
|
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
|
||||||
|
|
||||||
@@ -664,6 +668,7 @@ async fn twofactor_auth(
|
|||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||||
@@ -683,7 +688,7 @@ async fn twofactor_auth(
|
|||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => {
|
None => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?,
|
||||||
"2FA token not provided"
|
"2FA token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -700,7 +705,9 @@ async fn twofactor_auth(
|
|||||||
Some(TwoFactorType::Authenticator) => {
|
Some(TwoFactorType::Authenticator) => {
|
||||||
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
|
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
|
||||||
}
|
}
|
||||||
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
|
Some(TwoFactorType::Webauthn) => {
|
||||||
|
webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await?
|
||||||
|
}
|
||||||
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
||||||
Some(TwoFactorType::Duo) => {
|
Some(TwoFactorType::Duo) => {
|
||||||
match CONFIG.duo_use_iframe() {
|
match CONFIG.duo_use_iframe() {
|
||||||
@@ -732,7 +739,7 @@ async fn twofactor_auth(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?,
|
||||||
"2FA Remember token not provided"
|
"2FA Remember token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -766,6 +773,7 @@ async fn _json_err_twofactor(
|
|||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
|
webauthn: Webauthn2FaConfig<'_>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Value> {
|
) -> ApiResult<Value> {
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
@@ -785,7 +793,7 @@ async fn _json_err_twofactor(
|
|||||||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||||
|
|
||||||
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
||||||
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
|
let request = webauthn::generate_webauthn_login(user_id, webauthn, conn).await?;
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use super::UserId;
|
use super::UserId;
|
||||||
|
use crate::api::core::two_factor::webauthn::WebauthnRegistration;
|
||||||
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
||||||
|
use serde_json::Value;
|
||||||
|
use webauthn_rs::prelude::{Credential, ParsedAttestation};
|
||||||
|
use webauthn_rs_core::proto::CredentialV3;
|
||||||
|
use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
@@ -160,7 +163,8 @@ impl TwoFactor {
|
|||||||
|
|
||||||
use crate::api::core::two_factor::webauthn::U2FRegistration;
|
use crate::api::core::two_factor::webauthn::U2FRegistration;
|
||||||
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
|
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
|
||||||
use webauthn_rs::proto::*;
|
use webauthn_rs::prelude::{COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve};
|
||||||
|
use webauthn_rs_proto::{COSEAlgorithm, UserVerificationPolicy};
|
||||||
|
|
||||||
for mut u2f in u2f_factors {
|
for mut u2f in u2f_factors {
|
||||||
let mut regs: Vec<U2FRegistration> = serde_json::from_str(&u2f.data)?;
|
let mut regs: Vec<U2FRegistration> = serde_json::from_str(&u2f.data)?;
|
||||||
@@ -184,8 +188,8 @@ impl TwoFactor {
|
|||||||
type_: COSEAlgorithm::ES256,
|
type_: COSEAlgorithm::ES256,
|
||||||
key: COSEKeyType::EC_EC2(COSEEC2Key {
|
key: COSEKeyType::EC_EC2(COSEEC2Key {
|
||||||
curve: ECDSACurve::SECP256R1,
|
curve: ECDSACurve::SECP256R1,
|
||||||
x,
|
x: x.into(),
|
||||||
y,
|
y: y.into(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,11 +199,19 @@ impl TwoFactor {
|
|||||||
name: reg.name.clone(),
|
name: reg.name.clone(),
|
||||||
credential: Credential {
|
credential: Credential {
|
||||||
counter: reg.counter,
|
counter: reg.counter,
|
||||||
verified: false,
|
user_verified: false,
|
||||||
cred: key,
|
cred: key,
|
||||||
cred_id: reg.reg.key_handle.clone(),
|
cred_id: reg.reg.key_handle.clone().into(),
|
||||||
registration_policy: UserVerificationPolicy::Discouraged,
|
registration_policy: UserVerificationPolicy::Discouraged_DO_NOT_USE,
|
||||||
},
|
|
||||||
|
transports: None,
|
||||||
|
backup_eligible: false,
|
||||||
|
backup_state: false,
|
||||||
|
extensions: RegisteredExtensions::none(),
|
||||||
|
attestation: ParsedAttestation::default(),
|
||||||
|
attestation_format: AttestationFormat::None,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
webauthn_regs.push(new_reg);
|
webauthn_regs.push(new_reg);
|
||||||
@@ -217,7 +229,52 @@ impl TwoFactor {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn migrate_credential_to_passkey(conn: &mut DbConn) -> EmptyResult {
|
||||||
|
let webauthn_factors = db_run! { conn: {
|
||||||
|
twofactor::table
|
||||||
|
.filter(twofactor::atype.eq(TwoFactorType::Webauthn as i32))
|
||||||
|
.load::<TwoFactorDb>(conn)
|
||||||
|
.expect("Error loading twofactor")
|
||||||
|
.from_db()
|
||||||
|
}};
|
||||||
|
|
||||||
|
for webauthn_factor in webauthn_factors {
|
||||||
|
// assume that a failure to parse into the old struct, means that it was already converted
|
||||||
|
// alternatively this could also be checked via an extra field in the db
|
||||||
|
let Ok(regs) = serde_json::from_str::<Vec<WebauthnRegistrationV3>>(&webauthn_factor.data) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let regs = regs.into_iter().map(|r| r.into()).collect::<Vec<WebauthnRegistration>>();
|
||||||
|
|
||||||
|
TwoFactor::new(webauthn_factor.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®s)?)
|
||||||
|
.save(conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct TwoFactorId(String);
|
pub struct TwoFactorId(String);
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct WebauthnRegistrationV3 {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub migrated: bool,
|
||||||
|
pub credential: CredentialV3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WebauthnRegistrationV3> for WebauthnRegistration {
|
||||||
|
fn from(value: WebauthnRegistrationV3) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
migrated: value.migrated,
|
||||||
|
credential: Credential::from(value.credential).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -54,7 +54,7 @@ use rocket::error::Error as RocketErr;
|
|||||||
use serde_json::{Error as SerdeErr, Value};
|
use serde_json::{Error as SerdeErr, Value};
|
||||||
use std::io::Error as IoErr;
|
use std::io::Error as IoErr;
|
||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use webauthn_rs::error::WebauthnError as WebauthnErr;
|
use webauthn_rs::prelude::WebauthnError as WebauthnErr;
|
||||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@@ -61,6 +61,7 @@ mod sso_client;
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
||||||
|
use crate::api::core::two_factor::webauthn::WEBAUTHN_2FA_CONFIG;
|
||||||
use crate::api::purge_auth_requests;
|
use crate::api::purge_auth_requests;
|
||||||
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
||||||
pub use config::{PathType, CONFIG};
|
pub use config::{PathType, CONFIG};
|
||||||
@@ -88,6 +89,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
let pool = create_db_pool().await;
|
let pool = create_db_pool().await;
|
||||||
schedule_jobs(pool.clone());
|
schedule_jobs(pool.clone());
|
||||||
db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
|
db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
|
||||||
|
db::models::TwoFactor::migrate_credential_to_passkey(&mut pool.get().await.unwrap()).await.unwrap();
|
||||||
|
|
||||||
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
|
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
|
||||||
launch_rocket(pool, extra_debug).await // Blocks until program termination.
|
launch_rocket(pool, extra_debug).await // Blocks until program termination.
|
||||||
@@ -599,6 +601,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||||||
.manage(pool)
|
.manage(pool)
|
||||||
.manage(Arc::clone(&WS_USERS))
|
.manage(Arc::clone(&WS_USERS))
|
||||||
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||||
|
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
|
||||||
.attach(util::AppHeaders())
|
.attach(util::AppHeaders())
|
||||||
.attach(util::Cors())
|
.attach(util::Cors())
|
||||||
.attach(util::BetterLogging(extra_debug))
|
.attach(util::BetterLogging(extra_debug))
|
||||||
|
Reference in New Issue
Block a user