diff --git a/Cargo.lock b/Cargo.lock index 3b9ac1231..f6e1f5bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,6 +421,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -529,6 +535,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "built" version = "0.8.1" @@ -611,7 +626,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -684,7 +699,12 @@ version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ + "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", + "windows-link", ] [[package]] @@ -886,7 +906,7 @@ dependencies = [ "ctor", "dtor", "futures", - "hmac", + "hmac 0.13.0", "http", "http-body-util", "hyper", @@ -953,6 +973,7 @@ dependencies = [ "maplit", "nix", "num-traits", + "openidconnect", "parking_lot", "rand 0.10.2", "rand_core 0.6.4", @@ -967,7 +988,7 @@ dependencies = [ "sha2 0.11.0", "smallstr", "smallvec", - "thiserror", + "thiserror 2.0.18", "tikv-jemalloc-ctl", "tikv-jemalloc-sys", "tikv-jemallocator", @@ -1078,6 +1099,7 @@ dependencies = [ "loole", "lru-cache", "nonzero_ext", + "openidconnect", "rand 0.10.2", "recaptcha-verify", "regex", @@ -1119,6 +1141,7 @@ dependencies = [ "futures", "lettre", "memory-serve", + "openidconnect", "rand 0.10.2", "recaptcha-verify", "reqwest 0.12.28", @@ -1126,7 +1149,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror", + "thiserror 2.0.18", "tower-http 0.7.0", "tower-sec-fetch", "tower-sessions", @@ -1405,6 +1428,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1482,8 +1517,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1500,13 +1545,37 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] @@ -1540,6 +1609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1581,6 +1651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common 0.1.7", "subtle", ] @@ -1641,6 +1712,26 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -1674,6 +1765,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" version = "0.4.1" @@ -1758,6 +1870,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1967,6 +2089,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2061,6 +2184,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.15" @@ -2073,7 +2207,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2114,6 +2248,12 @@ version = "0.1.2+12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "647deb1583b14d160f85f3ff626f20b6edd366e3852c9843b06077388f794cb6" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.5" @@ -2212,7 +2352,7 @@ dependencies = [ "ipnet", "jni", "rand 0.10.2", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -2234,7 +2374,7 @@ dependencies = [ "rand 0.10.2", "ring", "serde", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "url", @@ -2262,11 +2402,29 @@ dependencies = [ "serde", "smallvec", "system-configuration", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac" version = "0.13.0" @@ -2406,6 +2564,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -2443,6 +2602,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -2586,6 +2769,17 @@ dependencies = [ "quick-error", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -2640,6 +2834,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2685,7 +2888,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.18", "walkdir", "windows-link", ] @@ -2799,6 +3002,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -2862,6 +3068,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libz-sys" version = "1.1.29" @@ -3218,6 +3430,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -3271,6 +3499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3283,6 +3512,26 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + [[package]] name = "objc2" version = "0.6.4" @@ -3461,6 +3710,37 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac 0.12.1", + "http", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.6", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2 0.10.9", + "subtle", + "thiserror 1.0.69", + "url", +] + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3477,7 +3757,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -3507,7 +3787,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest 0.13.4", - "thiserror", + "thiserror 2.0.18", "tokio", "tonic", "tonic-types", @@ -3539,11 +3819,20 @@ dependencies = [ "percent-encoding", "portable-atomic", "rand 0.9.4", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "os_info" version = "3.15.0" @@ -3560,6 +3849,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -3631,6 +3944,15 @@ dependencies = [ "syn", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3644,7 +3966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -3711,6 +4033,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3797,6 +4130,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -3927,7 +4269,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3949,7 +4291,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4002,6 +4344,8 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -4011,7 +4355,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -4026,6 +4370,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -4080,6 +4434,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.4" @@ -4128,6 +4502,7 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -4144,6 +4519,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -4198,6 +4574,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -4212,6 +4598,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "ruma" version = "0.15.1" @@ -4260,7 +4666,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror", + "thiserror 2.0.18", "url", "web-time", ] @@ -4277,7 +4683,7 @@ dependencies = [ "form_urlencoded", "getrandom 0.4.2", "http", - "indexmap", + "indexmap 2.14.0", "js_int", "konst", "percent-encoding", @@ -4288,7 +4694,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -4304,7 +4710,7 @@ version = "0.33.0" source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" dependencies = [ "as_variant", - "indexmap", + "indexmap 2.14.0", "js_int", "js_option", "pulldown-cmark", @@ -4312,7 +4718,7 @@ dependencies = [ "ruma-macros", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tracing", "web-time", "wildmatch", @@ -4338,7 +4744,7 @@ dependencies = [ "ruma-signatures", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -4348,7 +4754,7 @@ version = "0.12.1" source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" dependencies = [ "js_int", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -4392,7 +4798,7 @@ dependencies = [ "ruma-common", "serde_json", "sha2 0.10.9", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -4406,7 +4812,7 @@ dependencies = [ "ruma-signatures", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -4575,7 +4981,7 @@ dependencies = [ "futures-util", "pin-project", "thingbuf", - "thiserror", + "thiserror 2.0.18", "unicode-segmentation", ] @@ -4612,6 +5018,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4627,6 +5057,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -4787,7 +5231,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4822,6 +5266,16 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4849,7 +5303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0346d7a342ab90f405cfc08f25d15075f944f42fcabbc5eac923829fa6d228" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.14.0", "itoa", "serde_core", "zmij", @@ -4879,6 +5333,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_regex" version = "1.2.0" @@ -4919,6 +5382,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5034,6 +5529,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -5100,6 +5596,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spinning_top" version = "0.3.0" @@ -5249,7 +5751,7 @@ dependencies = [ "lazy-regex", "minimad", "serde", - "thiserror", + "thiserror 2.0.18", "unicode-width 0.1.14", ] @@ -5263,13 +5765,33 @@ dependencies = [ "pin-project", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -5467,7 +5989,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", @@ -5500,7 +6022,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5514,7 +6036,7 @@ version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.3", @@ -5600,7 +6122,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5730,7 +6252,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tracing", @@ -5979,7 +6501,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling", + "darling 0.20.11", "once_cell", "proc-macro-error2", "proc-macro2", @@ -6120,7 +6642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -6146,7 +6668,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -6191,6 +6713,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.12" @@ -6240,6 +6771,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -6483,7 +7049,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -6514,7 +7080,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -6533,7 +7099,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index 09e2f93cb..fe948046c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -403,6 +403,9 @@ default-features = false version = "0.11.0" default-features = false +[workspace.dependencies.openidconnect] +version = "4.0.1" + # optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring [workspace.dependencies.opentelemetry] version = "0.32.0" diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 28d1022cf..8db23efa9 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -2028,3 +2028,41 @@ # legacy authentication will be unable to log in. # #compatibility_mode = "hybrid" + +# This item is undocumented. Please contribute documentation for it. +# +#oidc = +# Uncommenting this section will enable Continuwuity's support for +# authenticating users using an OpenID Connect-compatible identity provider. +# This is referred to as "delegated authentication". +# +# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if +# the `global.oauth.compatibility_mode` setting is set to `exclusive`. +# Matrix clients which do not support OAuth login (also referred to as "next-gen auth") will NOT be able +# to log in while delegated authentication is active. +#[global.oauth.oidc] + +# The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to +# automatically fetch the identity provider's metadata from this URL. +# Generally you should set this to the base domain your identity provider +# runs on. +# +#discovery_url = + +# The OAuth client ID for Continuwuity to use when communicating with the +# identity provider. +# +#client_id = + +# The OAuth client secret for Continuwuity to use when communicating with +# the identity provider. +# +#client_secret = + +# Whether the user should be prompted to choose a localpart +# when signing in for the first time. If this is `false`, Continuwuity +# will attempt to use the value of the `preferred_username` claim +# returned from the IDP as the user's localpart, and authentication will +# fail if this claim is missing or is not a valid localpart. +# +#prompt_for_localpart = false diff --git a/src/admin/admin.rs b/src/admin/admin.rs index 2bebc7523..793bf1f85 100644 --- a/src/admin/admin.rs +++ b/src/admin/admin.rs @@ -1,5 +1,5 @@ use clap::Parser; -use conduwuit::Result; +use conduwuit::{Err, Result}; use crate::{ appservice::{self, AppserviceCommand}, @@ -8,6 +8,7 @@ debug::{self, DebugCommand}, federation::{self, FederationCommand}, media::{self, MediaCommand}, + oidc::{self, OidcCommand}, query::{self, QueryCommand}, room::{self, RoomCommand}, server::{self, ServerCommand}, @@ -30,6 +31,9 @@ pub enum AdminCommand { /// Commands for managing registration tokens Token(TokenCommand), + #[command(subcommand)] + Oidc(OidcCommand), + #[command(subcommand)] /// Commands for managing rooms Rooms(RoomCommand), @@ -80,6 +84,16 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res context.bail_restricted()?; token::process(command, context).await }, + | Oidc(command) => { + // OIDC commands are all restricted + context.bail_restricted()?; + + if !context.services.oidc.enabled() { + return Err!("OIDC is not configured"); + } + + oidc::process(command, context).await + }, | Rooms(command) => room::process(command, context).await, | Federation(command) => federation::process(command, context).await, | Server(command) => server::process(command, context).await, diff --git a/src/admin/mod.rs b/src/admin/mod.rs index 1bc33691c..fd73c3f15 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod debug; pub(crate) mod federation; pub(crate) mod media; +pub(crate) mod oidc; pub(crate) mod query; pub(crate) mod room; pub(crate) mod server; diff --git a/src/admin/oidc/commands.rs b/src/admin/oidc/commands.rs new file mode 100644 index 000000000..c5f9d049d --- /dev/null +++ b/src/admin/oidc/commands.rs @@ -0,0 +1,17 @@ +use conduwuit::Result; + +use crate::utils::parse_active_local_user_id; + +impl crate::Context<'_> { + pub(super) async fn oidc_link(&self, user_id: String, subject: String) -> Result { + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + + self.services.oidc.link_user(&user_id, &subject).await; + + self.write_str("Account linked successfully").await?; + + Ok(()) + } + + pub(super) async fn oidc_unlink(&self, _user_id: String) -> Result { todo!() } +} diff --git a/src/admin/oidc/mod.rs b/src/admin/oidc/mod.rs new file mode 100644 index 000000000..94baab797 --- /dev/null +++ b/src/admin/oidc/mod.rs @@ -0,0 +1,21 @@ +mod commands; + +use clap::Subcommand; +use conduwuit::Result; +use conduwuit_macros::admin_command_dispatch; + +#[admin_command_dispatch] +#[derive(Debug, Subcommand)] +pub enum OidcCommand { + /// Link a user ID to the given subject claim. + #[clap(name = "link")] + OidcLink { + user_id: String, + subject: String, + }, + + #[clap(name = "unlink")] + OidcUnlink { + user_id: String, + }, +} diff --git a/src/admin/token/commands.rs b/src/admin/token/commands.rs index 0e5ae6132..21882bc32 100644 --- a/src/admin/token/commands.rs +++ b/src/admin/token/commands.rs @@ -44,7 +44,7 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result { .services .config .oauth - .compatibility_mode + .compatibility_mode() .oauth_available() { self.write_str(&format!( diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index a9ffce7ff..a76e3fe92 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -59,7 +59,7 @@ pub(super) async fn create_user(&self, username: String, password: Option Router { .layer(middleware::from_fn_with_state( state, async |State(state): State, request: Request, next: Next| -> Response { - if state.config.oauth.compatibility_mode.oauth_available() { + if state.config.oauth.compatibility_mode().oauth_available() { next.run(request).await } else { (StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response() diff --git a/src/api/client/oauth/server_metadata.rs b/src/api/client/oauth/server_metadata.rs index 60d287960..eaf4d2a7a 100644 --- a/src/api/client/oauth/server_metadata.rs +++ b/src/api/client/oauth/server_metadata.rs @@ -21,7 +21,7 @@ pub(crate) async fn get_authorization_server_metadata_route( State(services): State, _body: Ruma, ) -> Result { - if !services.config.oauth.compatibility_mode.oauth_available() { + if !services.config.oauth.compatibility_mode().oauth_available() { return Err!(Request(Unrecognized("OAuth is unavailable on this server"))); } diff --git a/src/api/client/session.rs b/src/api/client/session.rs index c7af45fe5..fa04b4242 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -43,7 +43,7 @@ pub(crate) async fn get_login_types_route( ClientIp(client): ClientIp, _body: Ruma, ) -> Result { - if !services.config.oauth.compatibility_mode.uiaa_available() { + if !services.config.oauth.compatibility_mode().uiaa_available() { return Err!(Request(Unrecognized( "User-interactive authentication is not available on this server." ))); @@ -120,7 +120,7 @@ pub(crate) async fn login_route( ClientIp(client): ClientIp, body: Ruma, ) -> Result { - if !services.config.oauth.compatibility_mode.uiaa_available() { + if !services.config.oauth.compatibility_mode().uiaa_available() { return match body.login_info { | LoginInfo::ApplicationService(_) => { Err!(Request(AppserviceLoginUnsupported( diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 39eded4cf..b67598aea 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -118,6 +118,7 @@ url.workspace = true parking_lot.workspace = true lock_api.workspace = true hyper-util.workspace = true +openidconnect.workspace = true [target.'cfg(unix)'.dependencies] nix.workspace = true diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 4e81c32bd..6c458c98c 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -17,6 +17,7 @@ use figment::providers::{Env, Format, Toml}; pub use figment::{Figment, value::Value as FigmentValue}; use lettre::message::Mailbox; +use openidconnect::{ClientId, ClientSecret}; use regex::RegexSet; use ruma::{ OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId, @@ -2419,10 +2420,65 @@ pub struct OauthConfig { /// legacy authentication will be unable to log in. /// /// default: "hybrid" - pub compatibility_mode: OAuthMode, + compatibility_mode: OAuthMode, + + pub oidc: Option, } -#[derive(Clone, Debug, Default, Deserialize)] +impl OauthConfig { + #[must_use] + pub fn compatibility_mode(&self) -> OAuthMode { + if self.oidc.is_some() { + OAuthMode::Exclusive + } else { + self.compatibility_mode + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[config_example_generator( + filename = "conduwuit-example.toml", + section = "global.oauth.oidc", + optional = "true", + header = "\ +# Uncommenting this section will enable Continuwuity's support for +# authenticating users using an OpenID Connect-compatible identity provider. +# This is referred to as \"delegated authentication\". +# +# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if +# the `global.oauth.compatibility_mode` setting is set to `exclusive`. +# Matrix clients which do not support OAuth login (also referred to as \"next-gen auth\") will \ + NOT be able +# to log in while delegated authentication is active." +)] +pub struct OidcConfig { + /// The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to + /// automatically fetch the identity provider's metadata from this URL. + /// Generally you should set this to the base domain your identity provider + /// runs on. + pub discovery_url: Url, + + /// The OAuth client ID for Continuwuity to use when communicating with the + /// identity provider. + pub client_id: ClientId, + + /// The OAuth client secret for Continuwuity to use when communicating with + /// the identity provider. + pub client_secret: ClientSecret, + + /// Whether the user should be prompted to choose a localpart + /// when signing in for the first time. If this is `false`, Continuwuity + /// will attempt to use the value of the `preferred_username` claim + /// returned from the IDP as the user's localpart, and authentication will + /// fail if this claim is missing or is not a valid localpart. + /// + /// default: false + #[serde(default)] + pub prompt_for_localpart: bool, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OAuthMode { Disabled, diff --git a/src/database/maps.rs b/src/database/maps.rs index dbaa71a78..e47c54835 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -124,6 +124,10 @@ pub(super) fn open_list(db: &Arc, maps: &[Descriptor]) -> Result { name: "onetimekeyid_onetimekeys", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "openidsubject_localpart", + ..descriptor::RANDOM_SMALL + }, Descriptor { name: "fallbackkeyid_fallbackkey", ..descriptor::RANDOM_SMALL diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index e302ff6c9..db51692dc 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -120,6 +120,7 @@ reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features yansi.workspace = true lettre.workspace = true serde_urlencoded.workspace = true +openidconnect.workspace = true [target.'cfg(all(unix, target_os = "linux"))'.dependencies] sd-notify.workspace = true diff --git a/src/service/mod.rs b/src/service/mod.rs index 8471891a2..5488ff178 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -28,6 +28,7 @@ pub mod media; pub mod moderation; pub mod oauth; +pub mod oidc; pub mod presence; pub mod pusher; pub mod registration_tokens; diff --git a/src/service/oauth/grant.rs b/src/service/oauth/grant.rs index d1dd8efc1..0a027f637 100644 --- a/src/service/oauth/grant.rs +++ b/src/service/oauth/grant.rs @@ -160,7 +160,7 @@ pub enum ErrorCode { InvalidClientMetadata, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct AuthorizationCodeResponse { pub state: String, pub code: String, diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs new file mode 100644 index 000000000..8b52c74a3 --- /dev/null +++ b/src/service/oidc/mod.rs @@ -0,0 +1,283 @@ +use std::{sync::Arc, time::Duration}; + +use async_trait::async_trait; +use conduwuit::{Result, config::OidcConfig, err, error, info}; +use database::{Deserialized, Map}; +use openidconnect::{ + AuthorizationCode, CsrfToken, EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl, + Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse, + core::{CoreAuthenticationFlow, CoreClient, CoreIdTokenClaims, CoreProviderMetadata}, + reqwest, +}; +use ruma::{OwnedUserId, UserId}; +use serde::{Deserialize, Serialize}; +use tokio::sync::SetOnce; +use url::Url; + +use crate::{ + Dep, config, globals, + oauth::grant::AuthorizationCodeResponse, + users::{self, AccountStatus}, +}; + +pub struct Service { + services: Services, + db: Data, + client: Option, +} + +struct Data { + openidsubject_localpart: Arc, +} +struct Services { + config: Dep, + globals: Dep, + users: Dep, +} + +struct OidcClient { + config: OidcConfig, + machine: SetOnce< + CoreClient< + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointMaybeSet, + EndpointMaybeSet, + >, + >, + client: reqwest::Client, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PendingSession { + pkce_verifier: PkceCodeVerifier, + nonce: Nonce, + csrf_token: CsrfToken, +} + +pub enum SessionCompletionStatus { + Complete(OwnedUserId), + NeedsLocalpart, + InvalidLocalpart(String), +} + +#[async_trait] +impl crate::Service for Service { + fn build(args: crate::Args<'_>) -> Result> { + Ok(Arc::new(Self { + services: Services { + config: args.depend::("config"), + globals: args.depend::("globals"), + users: args.depend::("users"), + }, + db: Data { + openidsubject_localpart: args.db["openidsubject_localpart"].clone(), + }, + client: args.server.config.oauth.oidc.as_ref().map(|config| OidcClient { + config: config.clone(), + machine: SetOnce::new(), + // This isn't in the client service because it has to use the `reqwest` shipped by `openidconnect` + client: reqwest::ClientBuilder::new() + .connect_timeout(Duration::from_secs(args.server.config.request_conn_timeout)) + .read_timeout(Duration::from_secs(args.server.config.request_timeout)) + .timeout(Duration::from_secs(args.server.config.request_total_timeout)) + .pool_idle_timeout(Duration::from_secs(args.server.config.request_idle_timeout)) + .pool_max_idle_per_host(args.server.config.request_idle_per_host.into()) + .user_agent(conduwuit::user_agent()) + .redirect(reqwest::redirect::Policy::none()) + .danger_accept_invalid_certs(args.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure) + .build() + .expect("client should build") + }), + })) + } + + async fn worker(self: Arc) -> Result { + if let Some(OidcClient { config, machine, client }) = &self.client { + let redirect_url = self + .services + .config + .get_client_domain() + .join(&format!("{}/oidc/complete", conduwuit::ROUTE_PREFIX)) + .expect("redirect url should be valid"); + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::from_url(config.discovery_url.clone()), + client, + ) + .await + .map_err(|err| err!("Failed to discover OIDC provider metadata: {err}"))?; + + machine + .set( + CoreClient::from_provider_metadata( + provider_metadata, + config.client_id.clone(), + Some(config.client_secret.clone()), + ) + .set_redirect_uri(RedirectUrl::from_url(redirect_url)), + ) + .expect("machine should be empty"); + } + + Ok(()) + } + + fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } +} + +impl Service { + const SERVER_MISCONFIGURED: &str = + "Identity server is misconfigured. Contact your homeserver's administrator."; + + pub fn enabled(&self) -> bool { self.client.is_some() } + + pub async fn begin_session(&self) -> (PendingSession, Url) { + let OidcClient { machine, .. } = self.client.as_ref().expect("oidc should be configured"); + let machine = machine.wait().await; + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + let (auth_url, csrf_token, nonce) = machine + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .set_pkce_challenge(pkce_challenge) + .url(); + + (PendingSession { pkce_verifier, nonce, csrf_token }, auth_url) + } + + pub async fn exchange_code( + &self, + session: PendingSession, + response: AuthorizationCodeResponse, + ) -> Result { + let Some(OidcClient { machine, client, .. }) = self.client.as_ref() else { + return Err("Delegated authentication is not enabled on this server."); + }; + + let machine = machine.wait().await; + + if session.csrf_token.into_secret() != response.state { + return Err("State mismatch."); + } + + let token_response = machine + .exchange_code(AuthorizationCode::new(response.code)) + .expect("machine should be configured correctly") + .set_pkce_verifier(session.pkce_verifier) + .request_async(client) + .await + .map_err(|err| { + error!("Failed to exchange OIDC authorization code: {err}"); + "Code exchange failed." + })?; + + let Some(id_token) = token_response.id_token() else { + error!("Identity server did not return an id token"); + return Err(Self::SERVER_MISCONFIGURED); + }; + + let claims = id_token + .claims(&machine.id_token_verifier(), &session.nonce) + .map_err(|err| { + error!("Failed to verify id token claims: {err}"); + Self::SERVER_MISCONFIGURED + })? + .to_owned(); + + info!(subject = claims.subject().as_str(), "Authenticated subject"); + + Ok(claims) + } + + pub async fn complete_session( + &self, + claims: &CoreIdTokenClaims, + supplied_username: Option, + ) -> Result { + let Some(OidcClient { config, .. }) = self.client.as_ref() else { + return Err("Delegated authentication is not enabled on this server."); + }; + + let subject = claims.subject().as_str(); + + let user_id = if let Ok(localpart) = self + .db + .openidsubject_localpart + .get(subject) + .await + .deserialized::() + { + UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name())) + .expect("saved localpart should be valid") + } else if config.prompt_for_localpart { + if let Some(supplied_username) = supplied_username { + match self + .services + .users + .determine_registration_user_id(Some(supplied_username), None, None) + .await + { + | Ok(user_id) => user_id, + | Err(err) => + return Ok(SessionCompletionStatus::InvalidLocalpart(err.message())), + } + } else { + return Ok(SessionCompletionStatus::NeedsLocalpart); + } + } else if let Some(preferred_username) = claims.preferred_username() { + self.services + .users + .determine_registration_user_id(Some(preferred_username.to_string()), None, None) + .await + .map_err(|err| { + error!("Preferred username claim is not a valid localpart: {err}"); + "Your preferred username is not a valid Matrix user ID localpart. Contact \ + your homeserver's administrator." + })? + } else { + error!("No preferred username claim was present"); + return Err(Self::SERVER_MISCONFIGURED); + }; + + info!(?subject, ?user_id, "User {user_id} successfully authorized with OIDC"); + + match self.services.users.status(&user_id).await { + | AccountStatus::Active => { + // Do nothing, an account already exists + }, + | AccountStatus::NotFound => { + // Create a new shadow user + self.services + .users + .create_local_account(&user_id, None, None) + .await + .map_err(|err| { + error!("Failed to create a shadow user for {user_id}: {err}"); + Self::SERVER_MISCONFIGURED + })?; + + self.link_user(&user_id, subject).await; + + info!(?subject, ?user_id, "Shadow user created for {user_id}"); + }, + | AccountStatus::Deactivated => { + return Err("Your account has been deactivated."); + }, + } + + Ok(SessionCompletionStatus::Complete(user_id)) + } + + pub async fn link_user(&self, user_id: &UserId, subject: &str) { + self.db + .openidsubject_localpart + .insert(subject, user_id.localpart()); + } +} diff --git a/src/service/services.rs b/src/service/services.rs index 0cfec43bd..92d4a899e 100644 --- a/src/service/services.rs +++ b/src/service/services.rs @@ -11,8 +11,8 @@ account_data, admin, announcements, antispam, appservice, client, config, emergency, federation, firstrun, globals, key_backups, mailer, manager::Manager, - media, moderation, oauth, presence, pusher, registration_tokens, resolver, rooms, sending, - server_keys, + media, moderation, oauth, oidc, presence, pusher, registration_tokens, resolver, rooms, + sending, server_keys, service::{self, Args, Map, Service}, sync, threepid, transactions, uiaa, users, }; @@ -28,6 +28,7 @@ pub struct Services { pub key_backups: Arc, pub media: Arc, pub oauth: Arc, + pub oidc: Arc, pub mailer: Arc, pub presence: Arc, pub pusher: Arc, @@ -85,6 +86,7 @@ macro_rules! build { key_backups: build!(key_backups::Service), media: build!(media::Service), oauth: build!(oauth::Service), + oidc: build!(oidc::Service), mailer: build!(mailer::Service), presence: build!(presence::Service), pusher: build!(pusher::Service), diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index 2bb70c152..57665f5d6 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -314,7 +314,7 @@ async fn create_session( .services .config .oauth - .compatibility_mode + .compatibility_mode() .uiaa_available() { return Err!(Request(Unrecognized( diff --git a/src/service/users/account.rs b/src/service/users/account.rs index f4ed1f564..a3d7e2b16 100644 --- a/src/service/users/account.rs +++ b/src/service/users/account.rs @@ -38,8 +38,10 @@ pub enum AccountStatus { } impl AccountStatus { + #[must_use] pub fn is_found(&self) -> bool { !matches!(self, Self::NotFound) } + #[must_use] pub fn is_active(&self) -> bool { matches!(self, Self::Active) } pub fn ensure_active(&self) -> Result<()> { @@ -125,15 +127,19 @@ pub async fn create_shadow_account(&self, user_id: &UserId) -> Result<()> { Ok(()) } - /// Create a new account for a local human or bot user. + /// Create a new account for a local human or bot user. If `password` is + /// None, the account will be a shadow account. pub async fn create_local_account( &self, user_id: &UserId, - password: HashedPassword, + password: Option, email: Option
, ) -> Result<()> { self.create_shadow_account(user_id).await?; - self.convert_to_local_account(user_id, password).await?; + + if let Some(password) = password { + self.convert_to_local_account(user_id, password).await?; + } // Set an initial display name { diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml index d70335ebf..516ff1712 100644 --- a/src/web/Cargo.toml +++ b/src/web/Cargo.toml @@ -49,6 +49,7 @@ url.workspace = true recaptcha-verify = { version = "0.2.0", default-features = false } reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated form_urlencoded = "1.2.2" +openidconnect.workspace = true [build-dependencies] memory-serve = "2.1.0" diff --git a/src/web/mod.rs b/src/web/mod.rs index 633b64191..847ca80fe 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -133,6 +133,7 @@ pub fn build(services: &Services) -> Router { .nest("/account/", account::build()) .merge(debug::build()) .nest("/oauth2/", oauth::build()) + .nest("/oidc/", oidc::build()) .merge(resources::build()) .merge(threepid::build()) .fallback(async || WebError::NotFound), diff --git a/src/web/pages/account/login.rs b/src/web/pages/account/login.rs index 52ef6cd69..ca66bae2d 100644 --- a/src/web/pages/account/login.rs +++ b/src/web/pages/account/login.rs @@ -21,6 +21,7 @@ GET_POST, Result, TemplateContext, account::register::{TrustedFlowStatus, UntrustedFlowStatus, registration_flow_status}, components::UserCard, + oidc::{OIDC_SESSION_ID_KEY, OidcSession, OidcSessionState}, }, response, session::{LoginQuery, LoginTarget, User, UserSession}, @@ -68,6 +69,24 @@ async fn route_login( ) -> Result { let user_id = user.into_session().map(|session| session.user_id); + if services.oidc.enabled() { + if user_id.is_some() && !reauthenticate { + return response!(Redirect::to(&next.unwrap_or_default().target_path())); + } + + let (session, redirect_url) = services.oidc.begin_session().await; + + session_store + .insert(OIDC_SESSION_ID_KEY, OidcSession { + next: next.unwrap_or_default(), + state: OidcSessionState::CodeExchange { expected_user: user_id, session }, + }) + .await + .expect("should be able to serialize OIDC session"); + + return response!(Redirect::to(redirect_url.as_str())); + } + let body = match &user_id { | None => { let (trusted_flow_status, untrusted_flow_status) = diff --git a/src/web/pages/account/register.rs b/src/web/pages/account/register.rs index 9dffbf4ed..0e23d3359 100644 --- a/src/web/pages/account/register.rs +++ b/src/web/pages/account/register.rs @@ -520,7 +520,7 @@ async fn complete_registration( ) -> Result { services .users - .create_local_account(&user_id, password_hash, email) + .create_local_account(&user_id, Some(password_hash), email) .await?; if let Some(registration_token) = registration_token { diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs index e2e7ff8fe..c09e405d3 100644 --- a/src/web/pages/mod.rs +++ b/src/web/pages/mod.rs @@ -17,6 +17,7 @@ pub(super) mod debug; pub(super) mod index; pub(super) mod oauth; +pub(super) mod oidc; pub(super) mod resources; pub(super) mod threepid; diff --git a/src/web/pages/oidc/complete.rs b/src/web/pages/oidc/complete.rs new file mode 100644 index 000000000..fbe75b8ec --- /dev/null +++ b/src/web/pages/oidc/complete.rs @@ -0,0 +1,126 @@ +use std::time::SystemTime; + +use axum::{ + Extension, Router, + extract::{Query, State}, + response::Redirect, + routing::on, +}; +use conduwuit_service::{oauth::grant::AuthorizationCodeResponse, oidc::SessionCompletionStatus}; +use futures::FutureExt; +use ruma::OwnedServerName; +use serde::{Deserialize, de::IgnoredAny}; +use tower_sessions::Session; + +use crate::{ + WebError, + extract::{Expect, PostForm}, + pages::{ + GET_POST, Result, TemplateContext, + oidc::{OIDC_SESSION_ID_KEY, OidcSession, OidcSessionState}, + }, + response, + session::{User, UserSession}, + template, +}; + +pub(crate) fn build() -> Router { + Router::new().route("/", on(GET_POST, route_complete)) +} + +template! { + struct OidcComplete use "oidc_complete.html.j2" { + server_name: OwnedServerName, + username_error: Option + } +} + +#[derive(Deserialize)] +struct LoginForm { + username: String, +} + +async fn route_complete( + State(services): State, + Extension(context): Extension, + Expect(Query(query)): Expect>, + session_store: Session, + user: User, + PostForm(form): PostForm, +) -> Result { + let user_id = user.into_session().map(|session| session.user_id); + + let Some(session) = session_store + .get::(OIDC_SESSION_ID_KEY) + .await + .expect("should be able to deserialize oidc session") + else { + return response!(WebError::BadRequest( + "No OIDC session found. What are you doing here?".to_owned() + )); + }; + + let session_completion_status = match session.state { + | OidcSessionState::CodeExchange { expected_user, session: pending_session } => { + if let (Some(user_id), Some(expected_user)) = (&user_id, &expected_user) + && user_id != expected_user + { + return response!(WebError::BadRequest( + "Identity mismatch. You may have switched accounts at your identity \ + provider. Please log out and back in to continue." + .to_owned() + )); + } + + let claims = services + .oidc + .exchange_code(pending_session, query) + .boxed() + .await + .map_err(|err| WebError::BadRequest(err.to_owned()))?; + + session_store + .insert(OIDC_SESSION_ID_KEY, OidcSession { + next: session.next.clone(), + state: OidcSessionState::Authorized { claims: Box::new(claims.clone()) }, + }) + .await + .expect("Should be able to serialize oidc session"); + + services.oidc.complete_session(&claims, None).await + }, + | OidcSessionState::Authorized { claims } => + services + .oidc + .complete_session(&claims, form.map(|form| form.username)) + .await, + } + .map_err(|err| WebError::BadRequest(err.to_owned()))?; + + match session_completion_status { + | SessionCompletionStatus::Complete(user_id) => { + let _ = session_store + .remove::(OIDC_SESSION_ID_KEY) + .await; + + let user_session = UserSession { user_id, last_login: SystemTime::now() }; + + session_store + .insert(User::KEY, user_session) + .await + .expect("should be able to serialize user session"); + + response!(Redirect::to(&session.next.target_path())) + }, + | SessionCompletionStatus::NeedsLocalpart => { + response!(OidcComplete::new(context, services.globals.server_name().to_owned(), None)) + }, + | SessionCompletionStatus::InvalidLocalpart(error) => { + response!(OidcComplete::new( + context, + services.globals.server_name().to_owned(), + Some(error) + )) + }, + } +} diff --git a/src/web/pages/oidc/mod.rs b/src/web/pages/oidc/mod.rs new file mode 100644 index 000000000..a53a97930 --- /dev/null +++ b/src/web/pages/oidc/mod.rs @@ -0,0 +1,35 @@ +use axum::Router; +use conduwuit_service::oidc; +use openidconnect::core::CoreIdTokenClaims; +use ruma::OwnedUserId; +use serde::{Deserialize, Serialize}; + +use crate::session::LoginTarget; + +mod complete; + +pub(crate) const OIDC_SESSION_ID_KEY: &str = "oidc_session"; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct OidcSession { + pub next: LoginTarget, + pub state: OidcSessionState, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum OidcSessionState { + CodeExchange { + expected_user: Option, + session: oidc::PendingSession, + }, + Authorized { + claims: Box, + }, +} + +pub(crate) fn build() -> Router { + #[allow(clippy::wildcard_imports)] + use self::*; + + Router::new().nest("/complete", complete::build()) +} diff --git a/src/web/pages/resources/login.css b/src/web/pages/resources/login.css index ab5123885..d36810727 100644 --- a/src/web/pages/resources/login.css +++ b/src/web/pages/resources/login.css @@ -33,6 +33,7 @@ padding: 0.5em; margin-bottom: 0.5em; line-height: 1; + align-items: baseline; border-radius: var(--border-radius-sm); border: 2px solid var(--secondary); diff --git a/src/web/pages/templates/oidc_complete.html.j2 b/src/web/pages/templates/oidc_complete.html.j2 new file mode 100644 index 000000000..e5f322bd1 --- /dev/null +++ b/src/web/pages/templates/oidc_complete.html.j2 @@ -0,0 +1,39 @@ +{% extends "_layout.html.j2" %} +{% import "_components/form.html.j2" as form %} + +{%- block head -%} + +{%- endblock -%} + +{%- block title -%} +Link your account +{%- endblock -%} + +{%- block content -%} +
+

+ Link your account + + Matrix logo + +

+
+

To finish linking your account to Matrix, choose a username.

+

+ + + @ + + :{{ server_name }} + + {% if let Some(username_error) = username_error %} + + {{ username_error }} + + {% endif %} + Your username cannot be changed after you create your account. +

+ +
+
+{% endblock %}