diff --git a/.gitignore b/.gitignore
index b02d500994..a2958f7454 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,3 +65,5 @@ app-playground-data/*
.astro
.claude
+
+container_data/
diff --git a/Cargo.lock b/Cargo.lock
index 10b4e70c11..4f2e95a2d6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -514,6 +514,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 = "async-broadcast"
version = "0.7.2"
@@ -1005,6 +1044,17 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "bindgen"
version = "0.72.1"
@@ -1871,7 +1921,7 @@ dependencies = [
"bitflags 2.9.4",
"core-foundation 0.10.1",
"core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -2245,6 +2295,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"
@@ -2962,6 +3026,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -2969,7 +3042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
]
[[package]]
@@ -2983,6 +3056,12 @@ dependencies = [
"syn 2.0.106",
]
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@@ -4673,6 +4752,7 @@ dependencies = [
"urlencoding",
"uuid 1.18.1",
"validator",
+ "webauthn-rs",
"webp",
"woothee",
"yaserde",
@@ -5816,6 +5896,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"
@@ -5844,12 +5933,50 @@ dependencies = [
"pathdiff",
]
+[[package]]
+name = "openssl"
+version = "0.10.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
+dependencies = [
+ "bitflags 2.9.4",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+[[package]]
+name = "openssl-sys"
+version = "0.9.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -5997,6 +6124,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"
@@ -7403,6 +7536,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"
@@ -7941,6 +8083,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_cbor_2"
+version = "0.12.0-dev"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55"
+dependencies = [
+ "half 1.8.3",
+ "serde",
+]
+
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -8313,7 +8465,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
- "foreign-types",
+ "foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
@@ -10628,6 +10780,74 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webauthn-attestation-ca"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384e43534efe4e8f56c4eb1615a27e24d2ff29281385c843cf9f16ac1077dbdc"
+dependencies = [
+ "base64urlsafedata",
+ "openssl",
+ "openssl-sys",
+ "serde",
+ "tracing",
+ "uuid 1.18.1",
+]
+
+[[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 1.18.1",
+ "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",
+ "openssl",
+ "openssl-sys",
+ "rand 0.8.5",
+ "rand_chacha 0.3.1",
+ "serde",
+ "serde_cbor_2",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tracing",
+ "url",
+ "uuid 1.18.1",
+ "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]]
name = "webkit2gtk"
version = "2.0.1"
@@ -11432,6 +11652,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 11de214419..d0d22a63f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -187,6 +187,7 @@ url = "2.5.7"
urlencoding = "2.1.3"
uuid = "1.18.1"
validator = "0.20.0"
+webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation"] }
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/docs/public/openapi.yaml b/apps/docs/public/openapi.yaml
index 3a4376f15c..86dc704698 100644
--- a/apps/docs/public/openapi.yaml
+++ b/apps/docs/public/openapi.yaml
@@ -1290,6 +1290,10 @@ components:
type: boolean
description: Whether you have TOTP two-factor authentication connected to your account (only displayed if requesting your own account)
nullable: true
+ has_webauthn:
+ type: boolean
+ description: Whether you have Webauthn authentication connected to your account (only displayed if requesting your own account)
+ nullable: true
github_id:
deprecated: true
type: integer
diff --git a/apps/frontend/src/pages/settings/account.vue b/apps/frontend/src/pages/settings/account.vue
index e33d5c3807..bdca0e63ca 100644
--- a/apps/frontend/src/pages/settings/account.vue
+++ b/apps/frontend/src/pages/settings/account.vue
@@ -1,425 +1,524 @@
- Your account information is not displayed publicly. The code entered is incorrect!
- Two-factor authentication keeps your account secure by requiring access to a second
- device in order to sign in.
-
- If the QR code does not scan, you can manually enter the secret:
- {{ twoFactorSecret }}
- The code entered is incorrect!
- Download and save these back-up codes in a safe place. You can use these in-place of a
- 2FA code if you ever lose access to your device! You should protect these codes like
- your password.
- Backup codes can only be used once.
- Request a copy of all your personal data you have uploaded to Modrinth. This may take
- several minutes to complete.
-
- Once you delete your account, there is no going back. Deleting your account will remove all
- attached data, excluding projects, from our servers.
- Your account information is not displayed publicly.
+ The code entered is incorrect!
+
+ Two-factor authentication keeps your account secure by requiring
+ access to a second device in order to sign in.
+
+ If the QR code does not scan, you can manually enter the secret:
+ {{ twoFactorSecret }}
+
+ The code entered is incorrect!
+
+ Download and save these back-up codes in a safe place. You can use
+ these in-place of a 2FA code if you ever lose access to your
+ device! You should protect these codes like your password.
+ Backup codes can only be used once.
+ Request a copy of all your personal data you have uploaded to Modrinth.
+ This may take several minutes to complete.
+
+ Once you delete your account, there is no going back. Deleting your
+ account will remove all attached data, excluding projects, from our
+ servers.
+
-
-
-
-
-
-
-
-
-
-
-
-
- Scan the QR code with Authy,
-
- Microsoft Authenticator, or any other 2FA app to begin.
-
-
-
- Account security
-
- Data export
- Delete account
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Scan the QR code with Authy,
+
+ Microsoft Authenticator, or any other 2FA app to begin.
+
+
+
+ Account security
+
+ Data export
+ Delete account
+