diff --git a/Cargo.lock b/Cargo.lock
index 1abbf338b6..45b9d07ec1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -477,7 +477,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"password-hash",
]
@@ -494,7 +494,7 @@ dependencies = [
"serde_json",
"thiserror 2.0.17",
"utoipa",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -530,6 +530,45 @@ dependencies = [
"zbus",
]
+[[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 2.0.106",
+ "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 2.0.106",
+]
+
[[package]]
name = "astral-tokio-tar"
version = "0.5.6"
@@ -1000,7 +1039,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -1369,6 +1408,17 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+[[package]]
+name = "base64urlsafedata"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
+dependencies = [
+ "base64 0.21.7",
+ "pastey",
+ "serde",
+]
+
[[package]]
name = "bindgen"
version = "0.72.1"
@@ -1378,7 +1428,7 @@ dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
- "itertools 0.12.1",
+ "itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
@@ -1831,7 +1881,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -1856,6 +1906,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+[[package]]
+name = "chacha20"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "chardetng"
version = "0.1.17"
@@ -2004,7 +2065,7 @@ dependencies = [
"time",
"tokio",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -2332,6 +2393,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc"
version = "3.3.0"
@@ -2706,7 +2776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -2726,6 +2796,20 @@ dependencies = [
"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]]
name = "deranged"
version = "0.5.4"
@@ -2927,7 +3011,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
- "libloading 0.7.4",
+ "libloading 0.8.8",
]
[[package]]
@@ -3283,7 +3367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -3894,11 +3978,25 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "r-efi",
+ "r-efi 5.3.0",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasip2",
+ "wasip3",
+]
+
[[package]]
name = "gif"
version = "0.13.3"
@@ -4600,7 +4698,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"system-configuration",
"tokio",
"tower-service",
@@ -4743,6 +4841,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -4997,7 +5101,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.2",
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -5331,8 +5435,10 @@ dependencies = [
"utoipa",
"utoipa-actix-web",
"utoipa-scalar",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"validator",
+ "webauthn-rs",
+ "webauthn-rs-proto",
"webp",
"woothee",
"yaserde",
@@ -5365,6 +5471,12 @@ dependencies = [
"spin 0.9.8",
]
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
[[package]]
name = "lebe"
version = "0.5.3"
@@ -5463,7 +5575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.53.5",
]
[[package]]
@@ -5766,7 +5878,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"tokio",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"wasm-bindgen-futures",
"web-sys",
"yaup",
@@ -5906,7 +6018,7 @@ dependencies = [
"rustc_version",
"smallvec",
"tagptr",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -5957,7 +6069,7 @@ dependencies = [
"serde_with",
"strum",
"utoipa",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -6592,6 +6704,15 @@ dependencies = [
"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]]
name = "once_cell"
version = "1.21.3"
@@ -6622,15 +6743,14 @@ dependencies = [
[[package]]
name = "openssl"
-version = "0.10.73"
+version = "0.10.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
"foreign-types 0.3.2",
"libc",
- "once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -6654,9 +6774,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
-version = "0.9.109"
+version = "0.9.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
"cc",
"libc",
@@ -6960,6 +7080,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
[[package]]
name = "path-util"
version = "0.0.0"
@@ -7725,7 +7851,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.32",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -7762,9 +7888,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -7788,6 +7914,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
[[package]]
name = "r2d2"
version = "0.8.10"
@@ -7840,6 +7972,17 @@ dependencies = [
"rand_core 0.9.3",
]
+[[package]]
+name = "rand"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.2",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -7897,6 +8040,12 @@ dependencies = [
"getrandom 0.3.3",
]
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -8288,7 +8437,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -8427,6 +8576,15 @@ dependencies = [
"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]]
name = "rustix"
version = "0.38.44"
@@ -8437,7 +8595,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -8450,7 +8608,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -8644,7 +8802,7 @@ dependencies = [
"serde",
"serde_json",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -8919,7 +9077,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -8976,6 +9134,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_cbor_2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
+dependencies = [
+ "half 2.7.0",
+ "serde",
+]
+
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -9222,7 +9390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -9239,7 +9407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -9495,7 +9663,7 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"webpki-roots 0.26.11",
]
@@ -9578,7 +9746,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.17",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"whoami",
]
@@ -9618,7 +9786,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.17",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"whoami",
]
@@ -9645,7 +9813,7 @@ dependencies = [
"thiserror 2.0.17",
"tracing",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -9678,7 +9846,6 @@ dependencies = [
"cfg-if",
"libc",
"psm",
- "windows-sys 0.52.0",
"windows-sys 0.59.0",
]
@@ -10111,7 +10278,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"walkdir",
]
@@ -10419,7 +10586,7 @@ dependencies = [
"toml 0.9.8",
"url",
"urlpattern",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"walkdir",
]
@@ -10443,7 +10610,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.2",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -10574,7 +10741,7 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"whoami",
"windows",
"windows-core 0.61.2",
@@ -10619,7 +10786,7 @@ dependencies = [
"tracing-error",
"url",
"urlencoding",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"webview2-com",
"windows-core 0.61.2",
]
@@ -11107,9 +11274,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -11127,14 +11294,14 @@ dependencies = [
"mutually_exclusive_features",
"pin-project",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
name = "tracing-attributes"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -11143,9 +11310,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.34"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -11541,7 +11708,7 @@ dependencies = [
"regex",
"syn 2.0.106",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -11567,14 +11734,14 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.18.1"
+version = "1.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
dependencies = [
- "getrandom 0.3.3",
+ "getrandom 0.4.2",
"js-sys",
- "rand 0.9.2",
- "serde",
+ "rand 0.10.1",
+ "serde_core",
"wasm-bindgen",
]
@@ -11749,7 +11916,16 @@ version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
- "wit-bindgen",
+ "wit-bindgen 0.46.0",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
]
[[package]]
@@ -11817,6 +11993,28 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.11.4",
+ "wasm-encoder",
+ "wasmparser",
+]
+
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -11843,6 +12041,18 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.9.4",
+ "hashbrown 0.15.5",
+ "indexmap 2.11.4",
+ "semver",
+]
+
[[package]]
name = "wayland-backend"
version = "0.3.11"
@@ -11923,6 +12133,74 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webauthn-attestation-ca"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
+dependencies = [
+ "base64urlsafedata",
+ "openssl",
+ "openssl-sys",
+ "serde",
+ "tracing",
+ "uuid 1.23.3",
+]
+
+[[package]]
+name = "webauthn-rs"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
+dependencies = [
+ "base64urlsafedata",
+ "serde",
+ "tracing",
+ "url",
+ "uuid 1.23.3",
+ "webauthn-rs-core",
+]
+
+[[package]]
+name = "webauthn-rs-core"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
+dependencies = [
+ "base64 0.21.7",
+ "base64urlsafedata",
+ "der-parser",
+ "hex",
+ "nom 7.1.3",
+ "openssl",
+ "openssl-sys",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "serde",
+ "serde_cbor_2",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tracing",
+ "url",
+ "uuid 1.23.3",
+ "webauthn-attestation-ca",
+ "webauthn-rs-proto",
+ "x509-parser",
+]
+
+[[package]]
+name = "webauthn-rs-proto"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
+dependencies = [
+ "base64 0.21.7",
+ "base64urlsafedata",
+ "serde",
+ "serde_json",
+ "url",
+]
+
[[package]]
name = "webkit2gtk"
version = "2.0.2"
@@ -12097,7 +12375,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -12636,6 +12914,94 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap 2.11.4",
+ "prettyplease",
+ "syn 2.0.106",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.9.4",
+ "indexmap 2.11.4",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.11.4",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
[[package]]
name = "woothee"
version = "0.13.0"
@@ -12727,6 +13093,23 @@ dependencies = [
"pkg-config",
]
+[[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]]
name = "xattr"
version = "1.6.1"
diff --git a/Cargo.toml b/Cargo.toml
index af805f52e8..18cba66337 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -216,6 +216,8 @@ utoipa-actix-web = { version = "0.1.2" }
utoipa-scalar = { version = "0.3.0", default-features = false }
uuid = "1.18.1"
validator = "0.20.0"
+webauthn-rs = "0.5.5"
+webauthn-rs-proto = "0.5.5"
webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
whoami = "1.6.1"
diff --git a/apps/frontend/src/components/ui/auth/PasskeySettings.vue b/apps/frontend/src/components/ui/auth/PasskeySettings.vue
new file mode 100644
index 0000000000..79fe5771a1
--- /dev/null
+++ b/apps/frontend/src/components/ui/auth/PasskeySettings.vue
@@ -0,0 +1,422 @@
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.managePasskeyModalLoading) }}
+
+
+
+ {{ formatMessage(messages.managePasskeyModalNoPasskeys) }}
+
+
+
+
+ {{ passkey.name }}
+
+
+
+ {{
+ formatMessage(messages.managePasskeyModalAdded, {
+ ago: formatRelativeTime(passkey.created_at),
+ })
+ }}
+
+ ⋅
+
+ {{
+ formatMessage(messages.managePasskeyModalLastUsed, {
+ ago: formatRelativeTime(passkey.last_used),
+ })
+ }}
+
+
+ {{ formatMessage(messages.managePasskeyModalNeverUsed) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/frontend/src/components/ui/auth/SignIn.vue b/apps/frontend/src/components/ui/auth/SignIn.vue
index ffb3414653..43f2d47a2f 100644
--- a/apps/frontend/src/components/ui/auth/SignIn.vue
+++ b/apps/frontend/src/components/ui/auth/SignIn.vue
@@ -153,6 +153,25 @@
+
+
+
+ {{ formatMessage(messages.continueWithPasskey) }}
+
+ {{ formatMessage(messages.lastSignInLabel) }}
+
+
+
@@ -235,6 +254,7 @@ import {
MicrosoftColorIcon,
RightArrowIcon,
SteamColorIcon,
+ UserKeyIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
@@ -248,7 +268,7 @@ import {
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
} from '@/composables/auth.ts'
-type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft'
+type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft' | 'passkey'
interface AuthGlobals {
captcha_enabled?: boolean
@@ -263,6 +283,7 @@ interface Props {
globals?: AuthGlobals | null
onPasswordSignIn?: () => void
onTwoFactorSignIn?: () => void
+ onPasskeySignIn?: () => void
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
}
@@ -274,6 +295,7 @@ const {
globals = null,
onPasswordSignIn = () => {},
onTwoFactorSignIn = () => {},
+ onPasskeySignIn = () => {},
onSetCaptchaRef = undefined,
} = defineProps()
@@ -343,6 +365,10 @@ const messages = defineMessages({
id: 'auth.sign-in.last-sign-in',
defaultMessage: 'Last sign in',
},
+ continueWithPasskey: {
+ id: 'auth.sign-in.continue-with-passkey',
+ defaultMessage: 'Continue with passkey',
+ },
})
diff --git a/apps/frontend/src/helpers/passkey.ts b/apps/frontend/src/helpers/passkey.ts
new file mode 100644
index 0000000000..746e6638aa
--- /dev/null
+++ b/apps/frontend/src/helpers/passkey.ts
@@ -0,0 +1,95 @@
+function ensurePasskeySupported() {
+ const supported =
+ typeof window !== 'undefined' &&
+ typeof window.PublicKeyCredential !== 'undefined' &&
+ typeof navigator !== 'undefined' &&
+ !!navigator.credentials
+ if (!supported) {
+ throw new Error('Passkeys are not supported by this browser.')
+ }
+}
+
+function base64urlToBuffer(base64url: string) {
+ return Uint8Array.from(atob(base64url.replace(/-/g, '+').replace(/_/g, '/')), (char) =>
+ char.charCodeAt(0),
+ )
+}
+
+function bufferToBase64url(buffer: ArrayBuffer) {
+ const bytes = new Uint8Array(buffer)
+ let str = ''
+ for (let i = 0; i < bytes.length; i++) {
+ str += String.fromCharCode(bytes[i])
+ }
+ return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
+}
+
+/**
+ * Creates a passkey credential using the browser's WebAuthn API.
+ *
+ * @param options The public key options for creating the passkey credential, provided by the server.
+ */
+export async function createPasskeyCredential(options: any) {
+ ensurePasskeySupported()
+
+ const publicKey = {
+ ...options,
+ challenge: base64urlToBuffer(options.challenge),
+ user: {
+ ...options.user,
+ id: base64urlToBuffer(options.user.id),
+ },
+ excludeCredentials: options.excludeCredentials?.map((cred: any) => ({
+ ...cred,
+ id: base64urlToBuffer(cred.id),
+ })),
+ }
+
+ const credential = (await navigator.credentials.create({ publicKey })) as PublicKeyCredential
+ const response = credential.response as AuthenticatorAttestationResponse
+
+ return {
+ id: credential.id,
+ rawId: bufferToBase64url(credential.rawId),
+ type: credential.type,
+ response: {
+ clientDataJSON: bufferToBase64url(response.clientDataJSON),
+ attestationObject: bufferToBase64url(response.attestationObject),
+ },
+ extensions: credential.getClientExtensionResults(),
+ }
+}
+
+/**
+ * Authenticates a user using a passkey credential.
+ *
+ * @param options The public key options for authenticating the passkey credential, provided by the server.
+ */
+export async function getPasskeyCredential(options: any) {
+ ensurePasskeySupported()
+
+ const publicKey = {
+ ...options,
+ challenge: base64urlToBuffer(options.challenge),
+ allowCredentials: options.allowCredentials?.map((cred: any) => ({
+ ...cred,
+ id: base64urlToBuffer(cred.id),
+ })),
+ }
+
+ const credential = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential
+ const response = credential.response as AuthenticatorAssertionResponse
+
+ return {
+ id: credential.id,
+ rawId: bufferToBase64url(credential.rawId),
+ type: credential.type,
+ response: {
+ clientDataJSON: bufferToBase64url(response.clientDataJSON),
+ authenticatorData: bufferToBase64url(response.authenticatorData),
+ signature: bufferToBase64url(response.signature),
+ userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
+ },
+ extensions: credential.getClientExtensionResults(),
+ }
+}
diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json
index 7a58e6cd41..99b7e9391f 100644
--- a/apps/frontend/src/locales/en-US/index.json
+++ b/apps/frontend/src/locales/en-US/index.json
@@ -332,6 +332,9 @@
"auth.sign-in.continue-with-email": {
"message": "Continue with Email"
},
+ "auth.sign-in.continue-with-passkey": {
+ "message": "Continue with passkey"
+ },
"auth.sign-in.create-account": {
"message": "Sign up"
},
@@ -3518,6 +3521,48 @@
"settings.account.security.email.title": {
"message": "Email"
},
+ "settings.account.security.passkey.add": {
+ "message": "Add passkey"
+ },
+ "settings.account.security.passkey.add-modal.name.description": {
+ "message": "Make sure to pick something memorable, so you can identify this passkey later."
+ },
+ "settings.account.security.passkey.add-modal.name.label": {
+ "message": "Name"
+ },
+ "settings.account.security.passkey.add-modal.name.placeholder": {
+ "message": "My passkey"
+ },
+ "settings.account.security.passkey.description": {
+ "message": "Manage your registered passkeys, or add a new one."
+ },
+ "settings.account.security.passkey.modal.added": {
+ "message": "Added {ago}"
+ },
+ "settings.account.security.passkey.modal.last-used": {
+ "message": "Last used {ago}"
+ },
+ "settings.account.security.passkey.modal.loading": {
+ "message": "Loading passkeys…"
+ },
+ "settings.account.security.passkey.modal.never-used": {
+ "message": "Never used"
+ },
+ "settings.account.security.passkey.modal.no-passkeys": {
+ "message": "You have no passkeys registered yet."
+ },
+ "settings.account.security.passkey.remove.description": {
+ "message": "This will permanently remove the passkey \"{name}\". You will no longer be able to sign in with it."
+ },
+ "settings.account.security.passkey.remove.title": {
+ "message": "Are you sure you want to remove this passkey?"
+ },
+ "settings.account.security.passkey.rename-modal.header": {
+ "message": "Rename passkey"
+ },
+ "settings.account.security.passkey.title": {
+ "message": "Manage passkeys"
+ },
"settings.account.security.password.action.add": {
"message": "Add password"
},
diff --git a/apps/frontend/src/pages/auth/sign-in.vue b/apps/frontend/src/pages/auth/sign-in.vue
index 03b6371d0e..adb3e4f488 100644
--- a/apps/frontend/src/pages/auth/sign-in.vue
+++ b/apps/frontend/src/pages/auth/sign-in.vue
@@ -11,6 +11,7 @@
:globals="globals"
:on-password-sign-in="beginPasswordSignIn"
:on-two-factor-sign-in="begin2FASignIn"
+ :on-passkey-sign-in="beginPasskeySignin"
:on-set-captcha-ref="setCaptchaRef"
/>
@@ -34,8 +35,9 @@ import {
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
promotePendingSignInOAuthProvider,
} from '@/composables/auth.ts'
+import { getPasskeyCredential } from '@/helpers/passkey.ts'
-type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft'
+type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft' | 'passkey'
interface AuthGlobalsResponse {
captcha_enabled?: boolean
@@ -193,6 +195,30 @@ async function begin2FASignIn() {
stopLoading()
}
+async function beginPasskeySignin() {
+ startLoading()
+ try {
+ const start = await client.labrinth.auth_v2.authenticatePasskeyStart()
+
+ const credential = await getPasskeyCredential(start.options.publicKey)
+
+ const result = await client.labrinth.auth_v2.authenticatePasskeyFinish({
+ flow: start.flow,
+ credential,
+ })
+
+ pendingSignInOAuthProvider.value = 'passkey'
+ await finishSignIn(result.session)
+ } catch (err) {
+ addNotification({
+ title: formatMessage(commonMessages.errorNotificationTitle),
+ text: getErrorMessage(err),
+ type: 'error',
+ })
+ }
+ stopLoading()
+}
+
async function finishSignIn(sessionToken?: string | null) {
if (route.query.launcher) {
let token = sessionToken
diff --git a/apps/frontend/src/pages/settings/account.vue b/apps/frontend/src/pages/settings/account.vue
index 057ed2eef7..940df6b60f 100644
--- a/apps/frontend/src/pages/settings/account.vue
+++ b/apps/frontend/src/pages/settings/account.vue
@@ -434,6 +434,7 @@
+
@@ -507,6 +508,7 @@ import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
import SteamIcon from 'assets/icons/auth/sso-steam.svg'
import QrcodeVue from 'qrcode.vue'
+import PasskeySettings from '~/components/ui/auth/PasskeySettings.vue'
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.ts'
definePageMeta({
diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose
index 23f8398ce9..3faa68bd36 100644
--- a/apps/labrinth/.env.docker-compose
+++ b/apps/labrinth/.env.docker-compose
@@ -166,3 +166,6 @@ MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
SERVER_PING_TIMEOUT=10000
SERVER_PING_RETRIES=3
+
+# Display name for Webauthn Authenticators
+WEBAUTHN_RP_NAME=Modrinth
diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local
index 8f6c29d907..631f5bc326 100644
--- a/apps/labrinth/.env.local
+++ b/apps/labrinth/.env.local
@@ -188,3 +188,6 @@ DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
SERVER_PING_TIMEOUT=10000
SERVER_PING_RETRIES=3
SERVER_PING_MIN_INTERVAL_SEC=1800
+
+# Display name for Webauthn Authenticators
+WEBAUTHN_RP_NAME=Modrinth
diff --git a/apps/labrinth/.sqlx/query-05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b.json b/apps/labrinth/.sqlx/query-05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b.json
new file mode 100644
index 0000000000..106e54f7af
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b.json
@@ -0,0 +1,58 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json\",\n last_used, created_at\n FROM user_passkeys\n WHERE credential_id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "credential_id",
+ "type_info": "Bytea"
+ },
+ {
+ "ordinal": 4,
+ "name": "passkey: sqlx::types::Json",
+ "type_info": "Jsonb"
+ },
+ {
+ "ordinal": 5,
+ "name": "last_used",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Bytea"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false
+ ]
+ },
+ "hash": "05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b"
+}
diff --git a/apps/labrinth/.sqlx/query-29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129.json b/apps/labrinth/.sqlx/query-29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129.json
new file mode 100644
index 0000000000..f42f890f09
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129.json
@@ -0,0 +1,16 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE user_passkeys SET name = $1\n WHERE id = $2 AND user_id = $3\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Varchar",
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129"
+}
diff --git a/apps/labrinth/.sqlx/query-31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b.json b/apps/labrinth/.sqlx/query-31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b.json
new file mode 100644
index 0000000000..b8b7a22428
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b.json
@@ -0,0 +1,28 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM sessions WHERE user_id = $1 RETURNING id, session\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "session",
+ "type_info": "Varchar"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b"
+}
diff --git a/apps/labrinth/.sqlx/query-42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e.json b/apps/labrinth/.sqlx/query-42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e.json
new file mode 100644
index 0000000000..44d2b50c59
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM user_passkeys\n WHERE id = $1 AND user_id = $2\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e"
+}
diff --git a/apps/labrinth/.sqlx/query-538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73.json b/apps/labrinth/.sqlx/query-538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73.json
new file mode 100644
index 0000000000..c990bb2864
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE user_passkeys\n SET passkey = $1, last_used = NOW()\n WHERE id = $2\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Jsonb",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73"
+}
diff --git a/apps/labrinth/.sqlx/query-7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9.json b/apps/labrinth/.sqlx/query-7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9.json
new file mode 100644
index 0000000000..b0e65451e3
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO user_passkeys (\n id, user_id, name, credential_id, passkey, created_at, last_used\n )\n VALUES (\n $1, $2 ,$3, $4, $5, $6, $7\n )\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8",
+ "Varchar",
+ "Bytea",
+ "Jsonb",
+ "Timestamptz",
+ "Timestamptz"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9"
+}
diff --git a/apps/labrinth/.sqlx/query-806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d.json b/apps/labrinth/.sqlx/query-806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d.json
new file mode 100644
index 0000000000..a5eb0420a0
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d.json
@@ -0,0 +1,58 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json\",\n last_used, created_at\n FROM user_passkeys\n WHERE user_id = $1\n ORDER BY created_at DESC\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "credential_id",
+ "type_info": "Bytea"
+ },
+ {
+ "ordinal": 4,
+ "name": "passkey: sqlx::types::Json",
+ "type_info": "Jsonb"
+ },
+ {
+ "ordinal": 5,
+ "name": "last_used",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false
+ ]
+ },
+ "hash": "806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d"
+}
diff --git a/apps/labrinth/.sqlx/query-9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb.json b/apps/labrinth/.sqlx/query-9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb.json
new file mode 100644
index 0000000000..ae5395b6c8
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT EXISTS(SELECT 1 FROM user_passkeys WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb"
+}
diff --git a/apps/labrinth/.sqlx/query-ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d.json b/apps/labrinth/.sqlx/query-ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d.json
new file mode 100644
index 0000000000..f2f563deb2
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM user_passkeys\n WHERE id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d"
+}
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index 0632770a08..7fd5de1c13 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -131,6 +131,8 @@ utoipa-actix-web = { workspace = true }
utoipa-scalar = { workspace = true, features = ["actix-web"] }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
validator = { workspace = true, features = ["derive"] }
+webauthn-rs = { workspace = true, features = ["danger-allow-state-serialisation", "conditional-ui"] }
+webauthn-rs-proto = { workspace = true }
webp = { workspace = true }
woothee = { workspace = true }
yaserde = { workspace = true, features = ["derive"] }
diff --git a/apps/labrinth/migrations/20260610162635_passkeys.sql b/apps/labrinth/migrations/20260610162635_passkeys.sql
new file mode 100644
index 0000000000..9e798b3bcf
--- /dev/null
+++ b/apps/labrinth/migrations/20260610162635_passkeys.sql
@@ -0,0 +1,11 @@
+CREATE TABLE user_passkeys (
+ id BIGINT PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ name VARCHAR(255) NOT NULL,
+ credential_id BYTEA NOT NULL UNIQUE,
+ passkey JSONB NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ last_used TIMESTAMPTZ
+);
+
+CREATE INDEX user_passkeys_user_id ON user_passkeys (user_id);
diff --git a/apps/labrinth/src/database/models/flow_item.rs b/apps/labrinth/src/database/models/flow_item.rs
index a000057f6e..2bc8dd864d 100644
--- a/apps/labrinth/src/database/models/flow_item.rs
+++ b/apps/labrinth/src/database/models/flow_item.rs
@@ -11,6 +11,7 @@ use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::SeedableRng;
use serde::{Deserialize, Serialize};
use url::Url;
+use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration};
const FLOWS_NAMESPACE: &str = "flows";
@@ -58,6 +59,13 @@ pub enum DBFlow {
scopes: Scopes,
original_redirect_uri: Option, // Needed for https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
},
+ RegisterPasskey {
+ user_id: DBUserId,
+ state: PasskeyRegistration,
+ },
+ AuthenticatePasskey {
+ state: DiscoverableAuthentication,
+ },
}
impl DBFlow {
diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs
index 646d5e4cc2..a194ce774c 100644
--- a/apps/labrinth/src/database/models/ids.rs
+++ b/apps/labrinth/src/database/models/ids.rs
@@ -3,7 +3,7 @@ use crate::database::PgTransaction;
use crate::models::ids::{
AffiliateCodeId, ChargeId, CollectionId, FileId, ImageId, NotificationId,
OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId,
- OAuthRedirectUriId, OrganizationId, PatId, PayoutId, ProductId,
+ OAuthRedirectUriId, OrganizationId, PasskeyId, PatId, PayoutId, ProductId,
ProductPriceId, ProjectId, ReportId, SessionId, SharedInstanceId,
SharedInstanceVersionId, TeamId, TeamMemberId, ThreadId, ThreadMessageId,
UserSubscriptionId, VersionId,
@@ -269,6 +269,10 @@ db_id_interface!(
AffiliateCodeId,
generator: generate_affiliate_code_id @ "affiliate_codes",
);
+db_id_interface!(
+ PasskeyId,
+ generator: generate_passkey_id @ "user_passkeys",
+);
id_type!(CategoryId as i32);
id_type!(GameId as i32);
diff --git a/apps/labrinth/src/database/models/mod.rs b/apps/labrinth/src/database/models/mod.rs
index 0db87c5082..2bd93a9aba 100644
--- a/apps/labrinth/src/database/models/mod.rs
+++ b/apps/labrinth/src/database/models/mod.rs
@@ -21,6 +21,7 @@ pub mod oauth_client_authorization_item;
pub mod oauth_client_item;
pub mod oauth_token_item;
pub mod organization_item;
+pub mod passkey_item;
pub mod pat_item;
pub mod payout_item;
pub mod payouts_values_notifications;
@@ -48,6 +49,7 @@ pub use ids::*;
pub use image_item::DBImage;
pub use oauth_client_item::DBOAuthClient;
pub use organization_item::DBOrganization;
+pub use passkey_item::DBPasskey;
pub use project_item::DBProject;
pub use team_item::DBTeam;
pub use team_item::DBTeamMember;
diff --git a/apps/labrinth/src/database/models/passkey_item.rs b/apps/labrinth/src/database/models/passkey_item.rs
new file mode 100644
index 0000000000..063e0724f4
--- /dev/null
+++ b/apps/labrinth/src/database/models/passkey_item.rs
@@ -0,0 +1,191 @@
+use super::ids::*;
+use crate::database::PgTransaction;
+use crate::database::models::DatabaseError;
+use chrono::{DateTime, Utc};
+use futures::TryStreamExt;
+use serde::{Deserialize, Serialize};
+use sqlx::types::Json;
+use webauthn_rs::prelude::Passkey;
+
+#[derive(Deserialize, Serialize, Clone, Debug)]
+pub struct DBPasskey {
+ pub id: DBPasskeyId,
+ pub user_id: DBUserId,
+ pub name: String,
+ pub credential_id: Vec,
+ pub passkey: Passkey,
+ pub created_at: DateTime,
+ pub last_used: Option>,
+}
+
+impl DBPasskey {
+ pub async fn insert(
+ &self,
+ transaction: &mut PgTransaction<'_>,
+ ) -> Result<(), DatabaseError> {
+ sqlx::query!(
+ "
+ INSERT INTO user_passkeys (
+ id, user_id, name, credential_id, passkey, created_at, last_used
+ )
+ VALUES (
+ $1, $2 ,$3, $4, $5, $6, $7
+ )
+ ",
+ self.id as DBPasskeyId,
+ self.user_id as DBUserId,
+ self.name,
+ self.credential_id,
+ Json(&self.passkey) as _,
+ self.created_at,
+ self.last_used,
+ )
+ .execute(&mut *transaction)
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn get_by_credential_id<'a, E>(
+ credential_id: &[u8],
+ exec: E,
+ ) -> Result