mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Implement federated login; #123
This commit is contained in:
338
Cargo.lock
generated
338
Cargo.lock
generated
@@ -137,7 +137,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"parse-size",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -619,6 +619,12 @@ version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -739,6 +745,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 = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -968,7 +983,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1231,8 +1246,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]]
|
||||
@@ -1249,13 +1274,37 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[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 2.0.117",
|
||||
]
|
||||
|
||||
[[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 2.0.117",
|
||||
]
|
||||
|
||||
[[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 2.0.117",
|
||||
]
|
||||
@@ -1308,7 +1357,7 @@ version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -1408,6 +1457,12 @@ 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"
|
||||
@@ -1890,7 +1945,7 @@ dependencies = [
|
||||
"pest_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -2287,6 +2342,17 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||
|
||||
[[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"
|
||||
@@ -2350,6 +2416,15 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[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.14.0"
|
||||
@@ -2401,7 +2476,7 @@ dependencies = [
|
||||
"jni-sys",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -2947,6 +3022,26 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[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 1.4.0",
|
||||
"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 = "obj"
|
||||
version = "0.10.2"
|
||||
@@ -2970,7 +3065,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oj_convert"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -3009,9 +3104,11 @@ dependencies = [
|
||||
"log",
|
||||
"oj_rc_core",
|
||||
"oj_serdes",
|
||||
"openidconnect",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3060,7 +3157,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"jsonwebtoken",
|
||||
"libfj",
|
||||
"log",
|
||||
@@ -3068,15 +3165,18 @@ dependencies = [
|
||||
"oj_rc_database",
|
||||
"oj_rc_factory",
|
||||
"oj_serdes",
|
||||
"openidconnect",
|
||||
"polariton",
|
||||
"polariton_server",
|
||||
"rand 0.10.1",
|
||||
"reqwest 0.13.3",
|
||||
"reqwest-websocket",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3084,7 +3184,7 @@ name = "oj_rc_database"
|
||||
version = "1.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"libfj",
|
||||
"oj_rc_factory",
|
||||
"sea-orm",
|
||||
@@ -3328,6 +3428,7 @@ dependencies = [
|
||||
"oj_rc_core",
|
||||
"oj_rc_factory",
|
||||
"oj_rc_plugins",
|
||||
"oj_serdes",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -3335,9 +3436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oj_serdes"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd23d2f1a6d461c2e2ef1a1a12c27fd8af4e68cdee91c32ae93aeecbb84bf929"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@@ -3361,12 +3460,52 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[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 1.4.0",
|
||||
"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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||
dependencies = [
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
@@ -3826,7 +3965,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.3",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3848,7 +3987,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -4026,6 +4165,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 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@@ -4135,6 +4294,7 @@ dependencies = [
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@@ -4159,7 +4319,7 @@ dependencies = [
|
||||
"reqwest 0.13.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -4403,6 +4563,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"
|
||||
@@ -4446,7 +4630,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum 0.26.3",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -4508,7 +4692,7 @@ dependencies = [
|
||||
"bigdecimal",
|
||||
"chrono",
|
||||
"inherent",
|
||||
"ordered-float",
|
||||
"ordered-float 4.6.0",
|
||||
"rust_decimal",
|
||||
"sea-query-derive",
|
||||
"serde_json",
|
||||
@@ -4538,12 +4722,12 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4639,6 +4823,16 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||
dependencies = [
|
||||
"ordered-float 2.10.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
@@ -4672,6 +4866,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_plain"
|
||||
version = "1.0.2"
|
||||
@@ -4693,6 +4898,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 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -4808,7 +5045,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits 0.2.19",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
]
|
||||
|
||||
@@ -4899,7 +5136,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hashbrown 0.15.5",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@@ -4910,7 +5147,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -4998,7 +5235,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
"uuid",
|
||||
@@ -5041,7 +5278,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
"uuid",
|
||||
@@ -5068,7 +5305,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -5198,13 +5435,33 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[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 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5369,7 +5626,7 @@ version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
@@ -5380,7 +5637,7 @@ version = "0.25.11+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.2",
|
||||
@@ -5506,7 +5763,7 @@ dependencies = [
|
||||
"log",
|
||||
"rand 0.9.4",
|
||||
"sha1 0.10.6",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
@@ -5617,8 +5874,15 @@ dependencies = [
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
@@ -5788,7 +6052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
@@ -5801,7 +6065,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"semver",
|
||||
]
|
||||
|
||||
@@ -6226,7 +6490,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"prettyplease",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
@@ -6257,7 +6521,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -6276,7 +6540,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -6424,7 +6688,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"getrandom 0.4.2",
|
||||
"hmac 0.13.0",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
|
||||
@@ -28,6 +28,8 @@ resolver = "3"
|
||||
[workspace.dependencies]
|
||||
actix-web = { version = "4", default-features = false, features = [ "macros", "compress-brotli", "compress-gzip", "compress-zstd", "ws"] }
|
||||
actix-files = "0.6"
|
||||
openidconnect = { version = "4.0", default-features = false, features = [ "reqwest" ] }
|
||||
urlencoding = "2.1"
|
||||
#libfj = { version = "0.10", path = "../libfj" }
|
||||
libfj = { version = "0.10" }
|
||||
log = "0.4"
|
||||
@@ -47,7 +49,7 @@ rand = { version = "0.10", features = [ "thread_rng" ] }
|
||||
num-quaternion = "1.0"
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
#oj_serdes = { version = "0.3.0", path = "../oj_core/serdes" }
|
||||
oj_serdes = "0.3.0"
|
||||
oj_convert = { version = "0.3.0", path = "../oj_core/convert" }
|
||||
oj_serdes = { version = "0.4.0", path = "../oj_core/serdes" }
|
||||
#oj_serdes = "0.3.0"
|
||||
oj_convert = { version = "0.4.0", path = "../oj_core/convert" }
|
||||
#oj_convert = "0.3.0"
|
||||
|
||||
@@ -17,6 +17,8 @@ tokio = { version = "1.43", features = [ "rt-multi-thread" ] }
|
||||
futures.workspace = true
|
||||
clap.workspace = true
|
||||
oj_rc_core = { version = "*", path = "../rc_core" }
|
||||
openidconnect.workspace = true
|
||||
urlencoding.workspace = true
|
||||
libfj.workspace = true
|
||||
git-version.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
mod cli;
|
||||
mod robocraft;
|
||||
mod oauth;
|
||||
|
||||
use actix_web::{App, HttpServer, Responder};
|
||||
|
||||
@@ -56,7 +57,7 @@ async fn main() -> std::io::Result<()> {
|
||||
App::new()
|
||||
.wrap_fn(|req, srv| {
|
||||
use actix_web::dev::Service;
|
||||
log::trace!("Request {} {}", req.method(), req.path());
|
||||
log::debug!("Request {} {}", req.method(), req.path());
|
||||
srv.call(req)
|
||||
})
|
||||
.app_data(cli_args2.clone())
|
||||
@@ -73,12 +74,18 @@ async fn main() -> std::io::Result<()> {
|
||||
.service(robocraft::email::email_password_auth)
|
||||
.service(robocraft::steam::steam_auth)
|
||||
.service(robocraft::username::user_password_auth)
|
||||
.service(robocraft::displayname::displaye_password_auth)
|
||||
.service(robocraft::intercom::services_ws)
|
||||
.service(robocraft::intercom::service_msg)
|
||||
.service(robocraft::intercom::lobby_state_ws)
|
||||
.service(robocraft::intercom::lobby_state_msg)
|
||||
.service(robocraft::intercom::status_get)
|
||||
.service(robocraft::intercom::status_set)
|
||||
.service(oauth::openid_config::get_openid_configuration)
|
||||
.service(oauth::jwks::get_oauth_jwks)
|
||||
.service(oauth::auth::post_oauth_auth)
|
||||
.service(oauth::auth::get_oauth_auth)
|
||||
.service(oauth::token::post_oauth_token)
|
||||
})
|
||||
.bind((cli_args.ip, cli_args.port))?
|
||||
.run()
|
||||
|
||||
38
rc_auth/src/oauth/auth.rs
Normal file
38
rc_auth/src/oauth/auth.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use actix_web::{get, post, web::{Data, Form, Query, Redirect}};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use oj_rc_core::persist::user::FederatedAuthenticator;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct AuthQuery {
|
||||
pub response_type: Option<String>,
|
||||
pub client_id: String,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub scope: String,
|
||||
pub state: String,
|
||||
pub code_challenge: String,
|
||||
pub code_challenge_method: String,
|
||||
}
|
||||
|
||||
#[post("/authenticate/oauth2/auth")]
|
||||
pub async fn post_oauth_auth(body: Form<oj_rc_core::persist::user::federation::FederatedAuthenticationPayload>, query: Query<AuthQuery>, config: Data<crate::robocraft::RcConfig>) -> impl actix_web::Responder {
|
||||
let access_token = match config.account_provider.remote_auth(&body, &query.code_challenge).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
log::error!("Failed to OAuth authenticate {} from {}: {}", body.display_name, body.domain_source, e.message);
|
||||
return Redirect::to("/")
|
||||
.temporary()
|
||||
}
|
||||
};
|
||||
let redirect_root = query.redirect_uri.as_ref().map(|x| x.to_owned()).unwrap_or_else(|| "/authenticate/oauth2/auth".to_owned());
|
||||
let redirect_url = format!("{}?code={}&state={}", redirect_root, access_token, query.state);
|
||||
#[cfg(debug_assertions)]
|
||||
log::debug!("Redirecting to {}", redirect_url);
|
||||
Redirect::to(redirect_url)
|
||||
.temporary()
|
||||
}
|
||||
|
||||
#[get("/authenticate/oauth2/auth")]
|
||||
pub async fn get_oauth_auth() -> &'static str {
|
||||
"This is unimplemented and should not be used for standard OAuth flows"
|
||||
}
|
||||
9
rc_auth/src/oauth/jwks.rs
Normal file
9
rc_auth/src/oauth/jwks.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use actix_web::{get, web::Json};
|
||||
use openidconnect::core::{CoreJsonWebKeySet, /*CoreJsonWebKey*/};
|
||||
|
||||
#[get("/authenticate/oauth2/jwks")]
|
||||
pub async fn get_oauth_jwks() -> Json<CoreJsonWebKeySet> {
|
||||
Json(CoreJsonWebKeySet::new(vec![
|
||||
// TODO ???
|
||||
]))
|
||||
}
|
||||
4
rc_auth/src/oauth/mod.rs
Normal file
4
rc_auth/src/oauth/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod openid_config;
|
||||
pub mod jwks;
|
||||
pub mod auth;
|
||||
pub mod token;
|
||||
65
rc_auth/src/oauth/openid_config.rs
Normal file
65
rc_auth/src/oauth/openid_config.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use actix_web::{get, web::{Data, Json}};
|
||||
use oj_rc_core::persist::user::federation::DiscoveryMetadata;
|
||||
|
||||
const OAUTH_AUTH_URL: &'static str = "authenticate/oauth2/auth";
|
||||
const OAUTH_JWKS_URL: &'static str = "authenticate/oauth2/jwks";
|
||||
const OAUTH_TOKEN_URL: &'static str = "authenticate/oauth2/token";
|
||||
|
||||
#[get("/.well-known/openid-configuration")]
|
||||
pub async fn get_openid_configuration(server_config: Data<oj_rc_core::persist::config::ServerConfig>) -> Json<DiscoveryMetadata> {
|
||||
let fallback_url = "http://127.0.0.1/fallback";
|
||||
let auth_url = format!("{}/{}", server_config.auth_url, OAUTH_AUTH_URL);
|
||||
let jwks_url = format!("{}/{}", server_config.auth_url, OAUTH_JWKS_URL); // TODO
|
||||
let token_url = format!("{}/{}", server_config.auth_url, OAUTH_TOKEN_URL);
|
||||
let meta = DiscoveryMetadata::new(
|
||||
openidconnect::IssuerUrl::new(server_config.auth_url.clone())
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to parse issuer url {}: {}", server_config.auth_url, e);
|
||||
openidconnect::IssuerUrl::new(fallback_url.to_owned()).unwrap()
|
||||
}),
|
||||
openidconnect::AuthUrl::new(auth_url.clone())
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to parse auth url {}: {}", auth_url, e);
|
||||
openidconnect::AuthUrl::new(fallback_url.to_owned()).unwrap()
|
||||
}),
|
||||
openidconnect::JsonWebKeySetUrl::new(jwks_url.clone())
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to parse jwks url {}: {}", jwks_url, e);
|
||||
openidconnect::JsonWebKeySetUrl::new(fallback_url.to_owned()).unwrap()
|
||||
}),
|
||||
vec![
|
||||
openidconnect::ResponseTypes::new(vec![openidconnect::core::CoreResponseType::Code]),
|
||||
openidconnect::ResponseTypes::new(vec![openidconnect::core::CoreResponseType::IdToken]),
|
||||
],
|
||||
vec![
|
||||
openidconnect::core::CoreSubjectIdentifierType::Public,
|
||||
],
|
||||
vec![
|
||||
//openidconnect::core::CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
|
||||
openidconnect::core::CoreJwsSigningAlgorithm::HmacSha256,
|
||||
],
|
||||
openidconnect::EmptyAdditionalProviderMetadata::default(),
|
||||
)
|
||||
.set_token_endpoint(Some(openidconnect::TokenUrl::new(token_url)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to parse token url {}: {}", server_config.auth_url, e);
|
||||
openidconnect::TokenUrl::new(fallback_url.to_owned()).unwrap()
|
||||
})
|
||||
))
|
||||
.set_scopes_supported(Some(vec![
|
||||
openidconnect::Scope::new("openid".to_owned()),
|
||||
openidconnect::Scope::new("read".to_owned()),
|
||||
openidconnect::Scope::new("write".to_owned()),
|
||||
openidconnect::Scope::new("federate".to_owned()),
|
||||
]))
|
||||
.set_claims_supported(Some(vec![
|
||||
openidconnect::core::CoreClaimName::new("aud".to_owned()),
|
||||
openidconnect::core::CoreClaimName::new("exp".to_owned()),
|
||||
openidconnect::core::CoreClaimName::new("iat".to_owned()),
|
||||
openidconnect::core::CoreClaimName::new("iss".to_owned()),
|
||||
openidconnect::core::CoreClaimName::new("sub".to_owned()),
|
||||
openidconnect::core::CoreClaimName::new("name".to_owned()),
|
||||
openidconnect::core::CoreClaimName::new("preferred_username".to_owned()),
|
||||
]));
|
||||
Json(meta)
|
||||
}
|
||||
40
rc_auth/src/oauth/token.rs
Normal file
40
rc_auth/src/oauth/token.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_web::{post, web::{Data, Form, Json}};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use oj_rc_core::persist::user::{FederatedAuthenticator, federation::TokenResponsePayload};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct TokenQuery {
|
||||
pub code: String,
|
||||
pub client_id: String,
|
||||
pub code_verifier: String,
|
||||
}
|
||||
|
||||
#[post("/authenticate/oauth2/token")]
|
||||
pub async fn post_oauth_token(body: Form<TokenQuery>, config: Data<crate::robocraft::RcConfig>) -> Json<TokenResponsePayload> {
|
||||
// TODO make errors compliant with OAuth2 spec
|
||||
match config.account_provider.remote_token(&body.code, &body.code_verifier).await {
|
||||
Ok(login_info) => {
|
||||
let mut token_resp = TokenResponsePayload::new(
|
||||
openidconnect::AccessToken::new(login_info.response.token.clone()),
|
||||
openidconnect::core::CoreTokenType::Bearer,
|
||||
openidconnect::core::CoreIdTokenFields::new(
|
||||
Some(openidconnect::IdToken::from_str(&login_info.response.token).expect("Bad token")),
|
||||
openidconnect::EmptyExtraTokenFields {},
|
||||
),
|
||||
);
|
||||
token_resp.set_refresh_token(Some(openidconnect::RefreshToken::new(login_info.response.refresh_token)));
|
||||
Json(token_resp)
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed OAuth2 token auth: {}", e.message);
|
||||
Json(TokenResponsePayload::new(
|
||||
openidconnect::AccessToken::new(String::default()),
|
||||
openidconnect::core::CoreTokenType::Bearer,
|
||||
openidconnect::core::CoreIdTokenFields::new(None, openidconnect::EmptyExtraTokenFields {}),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
32
rc_auth/src/robocraft/displayname.rs
Normal file
32
rc_auth/src/robocraft/displayname.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use oj_rc_core::persist::user::FederatedAuthenticator;
|
||||
use actix_web::{post, web::{Data, Json}};
|
||||
|
||||
#[post("/authenticate/displayname/game")]
|
||||
pub async fn displaye_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: Data<super::RcConfig>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, super::ErrorTy> {
|
||||
if body.display_name.is_none() {
|
||||
return Err(super::ErrorTy::from_err(oj_rc_core::persist::user::AuthError {
|
||||
message: "Missing display_name".to_owned(),
|
||||
code: oj_rc_core::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
}));
|
||||
}
|
||||
let display_name = body.display_name.clone().unwrap();
|
||||
if let Some((display_name, domain)) = display_name.split_once('#') {
|
||||
log::info!("Authenticating {} user {} for domain {}", body.target, display_name, domain);
|
||||
let user_info = oj_rc_core::persist::user::FederatedAuthInfo {
|
||||
display_name: display_name.to_owned(),
|
||||
password: body.password.clone(),
|
||||
domain: domain.to_owned(),
|
||||
};
|
||||
let response = config.account_provider.local_login(user_info).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to authenticate {} user {}#{}: {}", body.target, display_name, domain, e.message);
|
||||
super::ErrorTy::from_err(e)
|
||||
})?;
|
||||
Ok(Json(response.response))
|
||||
} else {
|
||||
Err(super::ErrorTy::from_err(oj_rc_core::persist::user::AuthError {
|
||||
message: "Missing display_name domain".to_owned(),
|
||||
code: oj_rc_core::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ use actix_web::{rt, web::{Payload, Data, Path, Json}, Error, HttpRequest, HttpRe
|
||||
|
||||
#[get("/intercom/.oj_services/{name}")]
|
||||
pub async fn services_ws(req: HttpRequest, stream: Payload, auth: Data<super::IntercomAuth>, reg: Data<super::Users>, name: Path<String>) -> Result<HttpResponse, Error> {
|
||||
auth.validate(&req, &format!(".oj_services/{}", name))?;
|
||||
log::debug!("intercom/.oj_services name is {}", name);
|
||||
auth.validate(&req, &format!(".oj_services/{}", urlencoding::encode(&*name)))?;
|
||||
let (res, mut session, _stream) = actix_ws::handle(&req, stream)?;
|
||||
|
||||
/*let mut stream = stream
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod registration;
|
||||
pub mod steam;
|
||||
pub mod username;
|
||||
pub mod intercom;
|
||||
pub mod displayname;
|
||||
|
||||
pub struct RcConfig {
|
||||
//pub data: std::path::PathBuf,
|
||||
|
||||
@@ -27,10 +27,14 @@ indexmap = { version = "2.0", features = ["serde"] }
|
||||
libfj.workspace = true
|
||||
jsonwebtoken = { version = "10", features = [ "rust_crypto" ] }
|
||||
argon2 = { version = "0.5", features = [ "std" ] }
|
||||
# federated auth
|
||||
openidconnect.workspace = true
|
||||
ring = "0.17"
|
||||
urlencoding.workspace = true
|
||||
|
||||
# intercom
|
||||
sha2 = "0.11"
|
||||
reqwest = { version = "0.13", default-features = false, features = [ "rustls", "charset", "json" ] }
|
||||
reqwest = { version = "0.13", default-features = false, features = [ "rustls", "charset", "json", "form" ] }
|
||||
reqwest-websocket = { version = "0.6", default-features = false, features = [ "json" ] }
|
||||
oj_serdes.workspace = true
|
||||
|
||||
|
||||
@@ -7,7 +7,14 @@ pub struct Token {
|
||||
pub federate: bool,
|
||||
pub auth_time: i64,
|
||||
pub qualified_name: String,
|
||||
pub source_domain: String,
|
||||
pub login_method: LoginMethod,
|
||||
pub iss: String,
|
||||
pub exp: i64,
|
||||
pub iat: i64,
|
||||
pub sub: String,
|
||||
pub aud: String,
|
||||
pub fedi_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
@@ -16,4 +23,5 @@ pub enum LoginMethod {
|
||||
DisplayName,
|
||||
Username,
|
||||
Email,
|
||||
OAuth,
|
||||
}
|
||||
|
||||
@@ -327,6 +327,7 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
|
||||
auth_url: self.settings.server.auth_url.trim_end_matches('/').to_owned(),
|
||||
intercom_url: self.settings.server.intercom_url.trim_end_matches('/').to_owned(),
|
||||
factory_url: self.settings.server.factory_url.trim_end_matches('/').to_owned(),
|
||||
society_url: self.settings.server.society_url.trim_end_matches('/').to_owned(),
|
||||
minimum_version: self.settings.server.min_version as i32,
|
||||
dos_protect: self.settings.server.dos_protection,
|
||||
maintenance_message: self.settings.server.maintenance_message.clone(),
|
||||
|
||||
@@ -105,6 +105,7 @@ pub struct ServerConfig {
|
||||
pub auth_url: String,
|
||||
pub intercom_url: String,
|
||||
pub factory_url: String,
|
||||
pub society_url: String,
|
||||
pub minimum_version: i32,
|
||||
pub dos_protect: bool,
|
||||
pub maintenance_message: Option<String>,
|
||||
|
||||
@@ -23,11 +23,16 @@ fn default_aliases() -> std::collections::HashMap<String, String> {
|
||||
alias_map.insert("rc.ngram.ca".to_owned(), "society.rc.ngram.ca".to_owned());
|
||||
alias_map.insert("robocraft.online".to_owned(), "society.robocraft.online".to_owned());
|
||||
alias_map.insert("robocraftgame.co.uk".to_owned(), "society.robocraftgame.co.uk".to_owned());
|
||||
alias_map.insert("127.0.0.1".to_owned(), "127.0.0.1:8002".to_owned());
|
||||
alias_map
|
||||
}
|
||||
|
||||
fn default_defederated() -> Vec<String> {
|
||||
vec![
|
||||
"robocraftgame.com".to_owned(),
|
||||
#[cfg(debug_assertions)]
|
||||
{ "127.0.0.1:8002".to_owned() },
|
||||
#[cfg(debug_assertions)]
|
||||
{ "127.0.0.1".to_owned() },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ pub struct ServerSettings {
|
||||
pub intercom_url: String,
|
||||
#[serde(default = "default_factory_url")]
|
||||
pub factory_url: String,
|
||||
#[serde(default = "default_society_url")]
|
||||
pub society_url: String,
|
||||
#[serde(default = "default_feedback_url")]
|
||||
pub feedback_url: String,
|
||||
#[serde(default = "default_support_url")]
|
||||
@@ -159,6 +161,7 @@ fn default_server_conf() -> ServerSettings {
|
||||
auth_url: default_auth_root_url(),
|
||||
intercom_url: default_intercom_root_url(),
|
||||
factory_url: default_factory_url(),
|
||||
society_url: default_society_url(),
|
||||
feedback_url: default_feedback_url(),
|
||||
support_url: default_support_url(),
|
||||
wiki_url: default_wiki_url(),
|
||||
@@ -188,6 +191,10 @@ fn default_factory_url() -> String {
|
||||
"http://127.0.0.1:8012".to_owned()
|
||||
}
|
||||
|
||||
fn default_society_url() -> String {
|
||||
"http://127.0.0.1:8002".to_owned()
|
||||
}
|
||||
|
||||
fn default_feedback_url() -> String {
|
||||
"https://mstdn.ca/@ngram".to_owned()
|
||||
}
|
||||
|
||||
@@ -10,15 +10,14 @@ pub struct AccountProvider {
|
||||
fake_players: std::sync::Arc<Vec<crate::persist::config::FakePlayer>>,
|
||||
filler_players: std::sync::Arc<Vec<crate::persist::config::FakePlayer>>,
|
||||
auto_signups: bool,
|
||||
domain: std::sync::Arc<String>,
|
||||
pub(super) domain: std::sync::Arc<String>,
|
||||
cdn: std::sync::Arc<String>,
|
||||
auth: std::sync::Arc<String>,
|
||||
pub(super) auth: std::sync::Arc<String>,
|
||||
pub(super) intercom: std::sync::Arc<String>,
|
||||
pub(super) intercom_http_client: std::sync::Arc<reqwest::Client>,
|
||||
pub(super) secret: std::sync::Arc<Vec<u8>>,
|
||||
db: std::sync::Arc<oj_rc_database::Database>,
|
||||
#[allow(dead_code)]
|
||||
federation: Option<crate::persist::config::Federation>,
|
||||
pub(super) db: std::sync::Arc<oj_rc_database::Database>,
|
||||
pub(super) federation: Option<crate::persist::config::Federation>,
|
||||
}
|
||||
|
||||
impl AccountProvider {
|
||||
@@ -42,7 +41,12 @@ impl AccountProvider {
|
||||
cdn: std::sync::Arc::new(server_settings.cdn_url),
|
||||
auth: std::sync::Arc::new(server_settings.auth_url),
|
||||
intercom: std::sync::Arc::new(server_settings.intercom_url),
|
||||
intercom_http_client: std::sync::Arc::new(reqwest::Client::new()),
|
||||
intercom_http_client: std::sync::Arc::new(
|
||||
reqwest::ClientBuilder::new()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.expect("HTTP client did not init")
|
||||
),
|
||||
secret: std::sync::Arc::new(secret),
|
||||
db: std::sync::Arc::new(db),
|
||||
federation: federation_conf,
|
||||
@@ -90,6 +94,7 @@ impl AccountProvider {
|
||||
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
|
||||
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.set_required_spec_claims::<&str>(&[]);
|
||||
validation.aud = Some(vec![ self.domain.to_string() ].into_iter().collect());
|
||||
let token_data = jsonwebtoken::decode::<crate::auth::Token>(&token, &secret, &validation).map_err(|e| super::AuthError {
|
||||
message: e.to_string(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
@@ -134,62 +139,10 @@ impl AccountProvider {
|
||||
secret: self.secret.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
|
||||
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
|
||||
Ok(Box::new(self.auth_internal(&token.token).await?))
|
||||
}
|
||||
|
||||
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
|
||||
let user_info = if let Some(user_info) = self.db.user_by_display_name(user).await.map_err(|e| super::AuthError {
|
||||
message: e.to_string(),
|
||||
code: crate::data::error_codes::AuthErrorCode::Unknown,
|
||||
})? {
|
||||
user_info
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: "User not found".to_owned(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError {
|
||||
message: e.to_string(),
|
||||
code: crate::data::error_codes::AuthErrorCode::Unknown,
|
||||
})? {
|
||||
user_perms
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: "User permissions not found".to_owned(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
Ok(Box::new(UserData {
|
||||
account: user_info,
|
||||
perms: user_perms,
|
||||
cubes: self.cubes.clone(),
|
||||
garage_upgrades: self.garage_upgrades.clone(),
|
||||
fake_players: self.fake_players.clone(),
|
||||
filler_players: self.filler_players.clone(),
|
||||
cdn: self.cdn.clone(),
|
||||
auth: self.auth.clone(),
|
||||
intercom: self.intercom.clone(),
|
||||
http_client: std::sync::Arc::new(reqwest::Client::new()),
|
||||
db: self.db.clone(),
|
||||
secret: self.secret.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn web_authenticate(&self, token: String) -> Result<Box<dyn super::WebUser>, super::AuthError> {
|
||||
Ok(Box::new(self.auth_internal(&token).await?))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::UserAuthenticator for AccountProvider {
|
||||
async fn login(&self, info: super::UserAuthInfo) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
pub(super) async fn login_internal(&self, info: super::UserAuthInfo, audience: Option<String>) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
//let new_root = self.root.join(&info.payload.public_id);
|
||||
let is_fedi = audience.is_some();
|
||||
let is_new_user;
|
||||
let user_opt = match &info {
|
||||
super::UserAuthInfo::Steam { id } => self.db.user_by_steam_id(*id).await,
|
||||
@@ -204,7 +157,7 @@ impl super::UserAuthenticator for AccountProvider {
|
||||
user_info
|
||||
} else {
|
||||
is_new_user = true;
|
||||
if self.auto_signups {
|
||||
if self.auto_signups && !is_fedi {
|
||||
log::info!("New user {}", info.display_id());
|
||||
let auto_name = match &info {
|
||||
super::UserAuthInfo::Steam { id } => id.to_string(),
|
||||
@@ -306,22 +259,31 @@ impl super::UserAuthenticator for AccountProvider {
|
||||
super::UserAuthInfo::Username { .. } => crate::auth::LoginMethod::Username,
|
||||
};
|
||||
|
||||
let pub_id = if is_fedi { format!("{}#{}", user_info.public_id, self.domain) } else { user_info.public_id.clone() };
|
||||
let client_details = libfj::robocraft::TokenPayload {
|
||||
public_id: user_info.public_id.clone(),
|
||||
display_name: user_info.display_name.clone(),
|
||||
robocraft_name: user_info.display_name,
|
||||
public_id: pub_id.clone(),
|
||||
display_name: if is_fedi { format!("{}#{}", user_info.display_name, self.domain) } else { user_info.display_name.clone() },
|
||||
robocraft_name: if is_fedi { format!("{}#{}", user_info.public_id, self.domain) } else { user_info.public_id.clone() },
|
||||
email_address: user_info.email,
|
||||
email_verified: true,
|
||||
flags: vec![
|
||||
"federated=false".to_owned(),
|
||||
if is_fedi { "federated=true".to_owned() } else { "federated=false".to_owned() },
|
||||
],
|
||||
};
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let payload = crate::auth::Token {
|
||||
client_details,
|
||||
federate: false,
|
||||
auth_time: chrono::Utc::now().timestamp(),
|
||||
qualified_name: format!("{}@{}", user_info.public_id, self.domain),
|
||||
login_method,
|
||||
federate: is_fedi,
|
||||
auth_time: now,
|
||||
qualified_name: format!("{}#{}", user_info.public_id, self.domain),
|
||||
source_domain: self.domain.to_string(),
|
||||
login_method: if is_fedi { crate::auth::LoginMethod::OAuth } else { login_method },
|
||||
iss: self.auth.to_string(),
|
||||
exp: now + 86400, // 1 day
|
||||
iat: now,
|
||||
sub: pub_id.clone(),
|
||||
aud: audience.unwrap_or_else(|| self.domain.to_string()),
|
||||
fedi_token: None,
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
log::debug!("Token payload\n{}", serde_json::to_string_pretty(&payload).unwrap());
|
||||
@@ -341,6 +303,63 @@ impl super::UserAuthenticator for AccountProvider {
|
||||
is_new: is_new_user,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
|
||||
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
|
||||
Ok(Box::new(self.auth_internal(&token.token).await?))
|
||||
}
|
||||
|
||||
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
|
||||
let user_info = if let Some(user_info) = self.db.user_by_display_name(user).await.map_err(|e| super::AuthError {
|
||||
message: e.to_string(),
|
||||
code: crate::data::error_codes::AuthErrorCode::Unknown,
|
||||
})? {
|
||||
user_info
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: "User not found".to_owned(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError {
|
||||
message: e.to_string(),
|
||||
code: crate::data::error_codes::AuthErrorCode::Unknown,
|
||||
})? {
|
||||
user_perms
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: "User permissions not found".to_owned(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
Ok(Box::new(UserData {
|
||||
account: user_info,
|
||||
perms: user_perms,
|
||||
cubes: self.cubes.clone(),
|
||||
garage_upgrades: self.garage_upgrades.clone(),
|
||||
fake_players: self.fake_players.clone(),
|
||||
filler_players: self.filler_players.clone(),
|
||||
cdn: self.cdn.clone(),
|
||||
auth: self.auth.clone(),
|
||||
intercom: self.intercom.clone(),
|
||||
http_client: std::sync::Arc::new(reqwest::Client::new()),
|
||||
db: self.db.clone(),
|
||||
secret: self.secret.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn web_authenticate(&self, token: String) -> Result<Box<dyn super::WebUser>, super::AuthError> {
|
||||
Ok(Box::new(self.auth_internal(&token).await?))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::UserAuthenticator for AccountProvider {
|
||||
async fn login(&self, info: super::UserAuthInfo) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
self.login_internal(info, None).await
|
||||
}
|
||||
|
||||
async fn user_exists(&self, user: super::UserId) -> Result<bool, String> {
|
||||
Ok(match user {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
use openidconnect::{OAuth2TokenResponse, TokenResponse};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
const SOCIETY_URLS_API_ENDPOINT: &'static str = "api/v1/services.json";
|
||||
const ACCESS_CODE_AAD: &'static [u8] = b"oj-access-code";
|
||||
|
||||
pub type DiscoveryMetadata = openidconnect::core::CoreProviderMetadata;
|
||||
pub type TokenResponsePayload = openidconnect::core::CoreTokenResponse;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Federation {
|
||||
pub enabled: bool,
|
||||
@@ -14,3 +21,557 @@ impl std::default::Default for Federation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct FederatedAuthenticationPayload {
|
||||
pub display_name: String,
|
||||
pub password: String,
|
||||
pub domain_source: String,
|
||||
pub domain_target: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct AccessCode {
|
||||
aud: String,
|
||||
exp: i64,
|
||||
iat: i64,
|
||||
iss: String,
|
||||
sub: String,
|
||||
secured: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct SecuredCodes {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
code_challenge: String,
|
||||
}
|
||||
|
||||
struct NonceProvider {
|
||||
issuer: std::sync::Arc<String>,
|
||||
secret: std::sync::Arc<Vec<u8>>,
|
||||
generated_time: i64,
|
||||
fuse: bool,
|
||||
}
|
||||
|
||||
impl NonceProvider {
|
||||
fn reset(&mut self, iat: i64) {
|
||||
self.generated_time = iat;
|
||||
self.fuse = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl ring::aead::NonceSequence for NonceProvider {
|
||||
// realistically this shouldn't ever be called again
|
||||
fn advance(&mut self) -> Result<ring::aead::Nonce, ring::error::Unspecified> {
|
||||
if self.fuse {
|
||||
Err(ring::error::Unspecified)
|
||||
} else {
|
||||
use sha2::Digest;
|
||||
self.fuse = true;
|
||||
let hash = sha2::Sha512::new()
|
||||
.chain_update(self.issuer.as_bytes())
|
||||
.chain_update(self.secret.as_slice())
|
||||
.chain_update(&self.generated_time.to_ne_bytes())
|
||||
.finalize();
|
||||
let mut nonce = Vec::from(hash.as_slice());
|
||||
nonce.truncate(12);
|
||||
Ok(ring::aead::Nonce::try_assume_unique_for_key(&nonce).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::AccountProvider {
|
||||
fn nonce_provider(&self) -> NonceProvider {
|
||||
NonceProvider {
|
||||
issuer: self.auth.clone(),
|
||||
secret: self.secret.clone(),
|
||||
generated_time: 0,
|
||||
fuse: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_defederated_from(&self, domain: &str, fedi_conf: &crate::persist::config::Federation) -> bool {
|
||||
for defederated in fedi_conf.defederated.iter() {
|
||||
if domain.ends_with(defederated) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn local_login_impl(&self, auth_info: super::FederatedAuthInfo, federation: &Option<crate::persist::config::Federation>) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
if auth_info.display_name.is_empty() {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation login with empty username"),
|
||||
code: crate::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
});
|
||||
}
|
||||
if let Some(fedi_conf) = federation {
|
||||
if auth_info.domain.is_empty() {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation login with empty domain for logging in {}", auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::PasswordInvalidated,
|
||||
});
|
||||
}
|
||||
let sanitised_domain = auth_info.domain.trim().to_lowercase();
|
||||
let target_domain = if let Some(alias) = fedi_conf.aliases.get(&sanitised_domain) {
|
||||
alias.to_owned()
|
||||
} else {
|
||||
sanitised_domain
|
||||
};
|
||||
if self.is_defederated_from(&target_domain, fedi_conf) {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation with {} for logging in {}", auth_info.domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::PasswordInvalidated,
|
||||
});
|
||||
}
|
||||
let is_localhost = target_domain == "localhost"
|
||||
|| target_domain == "::1"
|
||||
|| target_domain == "0:0:0:0:0:0:0:1"
|
||||
|| target_domain.starts_with("127.0.0.")
|
||||
|| target_domain.starts_with("localhost:");
|
||||
// TODO contact other domain
|
||||
let social_urls_api = if is_localhost {
|
||||
format!("http://{}/{}", target_domain, SOCIETY_URLS_API_ENDPOINT)
|
||||
} else {
|
||||
format!("https://{}/{}", target_domain, SOCIETY_URLS_API_ENDPOINT)
|
||||
};
|
||||
let urls: oj_serdes::society::ServiceDomains = self.intercom_http_client.get(&social_urls_api).send().await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to get {}: {}", social_urls_api, e);
|
||||
super::AuthError {
|
||||
message: format!("Failed to get {} for logging in {}", social_urls_api, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
}
|
||||
})?
|
||||
.json().await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to deserialize {}: {}", social_urls_api, e);
|
||||
super::AuthError {
|
||||
message: format!("Failed to deserialize {} for logging in {}", social_urls_api, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
}
|
||||
})?;
|
||||
if self.is_defederated_from(&urls.root, fedi_conf) {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation with root {} for logging in {}", urls.root, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::PasswordInvalidated,
|
||||
});
|
||||
}
|
||||
let sani_soc = urls.society.trim_start_matches("http://").trim_start_matches("https://").trim_matches('/').to_lowercase();
|
||||
if sani_soc != target_domain {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Bad society federation with {} for logging in {}", target_domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
}
|
||||
let issuer_url = openidconnect::IssuerUrl::new(urls.auth.clone())
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to parse issuer url {} for logging in {}: {}", urls.auth, auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
// openidconnect relies on an old dependency
|
||||
let oauth_http_client = openidconnect::reqwest::ClientBuilder::new()
|
||||
// Following redirects opens the client up to SSRF vulnerabilities.
|
||||
.redirect(openidconnect::reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.expect("Client should build");
|
||||
// access openid discovery endpoint to self-configure
|
||||
let provider_metadata = DiscoveryMetadata::discover_async(issuer_url, &oauth_http_client).await
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to discover OAuth on {} for logging in {}: {}", urls.auth, auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
let redirect_url_s = format!("http://{}/federation/redirect", self.domain);
|
||||
let redirect_url = openidconnect::RedirectUrl::new(redirect_url_s.clone())
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to parse redirect url {} for logging in {}: {}", redirect_url_s, auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
// do oauth exchange
|
||||
let oauth_client = openidconnect::core::CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
openidconnect::ClientId::new(self.domain.to_string()),
|
||||
None, // no client secret
|
||||
).set_redirect_uri(redirect_url);
|
||||
let (pkce_challenge, pkce_verifier) = openidconnect::PkceCodeChallenge::new_random_sha256();
|
||||
let (auth_url, csrf_token, _nonce) = oauth_client
|
||||
.authorize_url(
|
||||
openidconnect::core::CoreAuthenticationFlow::AuthorizationCode,
|
||||
openidconnect::CsrfToken::new_random,
|
||||
openidconnect::Nonce::new_random,
|
||||
)
|
||||
.add_scope(openidconnect::Scope::new("read".to_string()))
|
||||
.add_scope(openidconnect::Scope::new("federate".to_string()))
|
||||
.set_pkce_challenge(pkce_challenge)
|
||||
.url();
|
||||
// bypass browser because we don't need to ask permission and there's no mechanism to open a browser
|
||||
let remote_login = FederatedAuthenticationPayload {
|
||||
display_name: auth_info.display_name.clone(),
|
||||
password: auth_info.password.clone(),
|
||||
domain_source: self.domain.to_string(),
|
||||
domain_target: auth_info.domain.clone(),
|
||||
};
|
||||
let auth_resp = oauth_http_client.post(auth_url)
|
||||
.form(&remote_login)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to authenticate federated login in for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
let auth_redirected_url = if let Some(loc_header) = auth_resp.headers().get("location") {
|
||||
let url = loc_header.to_str().map_err(|e| super::AuthError {
|
||||
message: format!("Failed to stringify location header for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
})?;
|
||||
reqwest::Url::parse(url).map_err(|e| super::AuthError {
|
||||
message: format!("Failed to parse location URL for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
})?
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Bad OAuth2 auth response from {} for logging in {} (missing location header)", target_domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
//let auth_redirected_url = auth_resp.url();
|
||||
log::debug!("OAuth auth response URL: {}", auth_redirected_url);
|
||||
let mut query_map: std::collections::HashMap<_, _> = auth_redirected_url.query_pairs().collect();
|
||||
let auth_code = if let Some(auth_code) = query_map.remove("code") {
|
||||
openidconnect::AuthorizationCode::new(auth_code.to_string())
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Bad OAuth2 auth response from {} for logging in {} (missing code)", target_domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
if let Some(state) = query_map.remove("state") {
|
||||
if state != csrf_token.into_secret() {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Bad OAuth2 auth response from {} for logging in {} (invalid state)", target_domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Bad OAuth2 auth response from {} for logging in {} (missing state)", target_domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
};
|
||||
let token_resp: TokenResponsePayload = oauth_client.exchange_code(auth_code)
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to exchange code for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?
|
||||
.set_pkce_verifier(pkce_verifier)
|
||||
.request_async(&oauth_http_client).await
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to exchange code for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
let _id_token = token_resp.id_token()
|
||||
.ok_or_else(|| super::AuthError {
|
||||
message: format!("OAuth2 Token response missing ID token for {}", auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
// Don't verify since it's not using the openidconnect::core's signing algorithm
|
||||
/*let id_token_verifier = oauth_client.id_token_verifier();
|
||||
let claims = id_token.claims(&id_token_verifier, &nonce)
|
||||
.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to verify OAuth2 claims for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||
let actual_access_token_hash = openidconnect::AccessTokenHash::from_token(
|
||||
token_resp.access_token(),
|
||||
id_token.signing_alg().map_err(|e| super::AuthError {
|
||||
message: format!("Failed to verify OAuth2 signing algorithm for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?,
|
||||
id_token.signing_key(&id_token_verifier).map_err(|e| super::AuthError {
|
||||
message: format!("Failed to verify OAuth2 signing key for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?,
|
||||
).map_err(|e| super::AuthError {
|
||||
message: format!("Failed to verify OAuth2 access token for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
if actual_access_token_hash != *expected_access_token_hash {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Failed to verify OAuth2 access token for {} (no match)", auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
});
|
||||
}
|
||||
}*/
|
||||
let refresh_token = token_resp.refresh_token()
|
||||
.ok_or_else(|| super::AuthError {
|
||||
message: format!("OAuth2 Token response missing refresh token for {}", auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?
|
||||
.secret()
|
||||
.to_owned();
|
||||
let remote_token = token_resp.access_token().secret();
|
||||
#[cfg(debug_assertions)]
|
||||
log::debug!("User {} authenticated to {} with access token {}", auth_info.display_name, target_domain, remote_token);
|
||||
// create/update federated user entry in DB
|
||||
self.update_local_database(&auth_info, &urls).await.map_err(|e| super::AuthError {
|
||||
message: format!("Failed to update DB entries for {}: {}", auth_info.display_name, e),
|
||||
code: crate::data::error_codes::AuthErrorCode::Unknown,
|
||||
})?;
|
||||
// return local success token
|
||||
let local_token = self.localify_token(remote_token)?;
|
||||
Ok(super::UserLoginInfo {
|
||||
response: libfj::robocraft::AuthenticationResponseInfo {
|
||||
token: local_token,
|
||||
refresh_token: refresh_token,
|
||||
refresh_token_expiry: "0".to_string(), // TODO (seems like this isn't actually considered by the client)
|
||||
},
|
||||
is_new: false,
|
||||
})
|
||||
} else {
|
||||
Err(super::AuthError {
|
||||
message: format!("Refusing federation with {} for logging in {} (federation not configured)", auth_info.domain, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::PasswordInvalidated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn localify_token(&self, remote_token: &str) -> Result<String, super::AuthError> {
|
||||
let remote_token_data = jsonwebtoken::dangerous::insecure_decode::<crate::auth::Token>(remote_token)
|
||||
.map_err(|e| super::AuthError {
|
||||
message: e.to_string(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
let local_token_data = crate::auth::Token {
|
||||
iss: self.auth.to_string(),
|
||||
fedi_token: Some(remote_token.to_owned()),
|
||||
..remote_token_data.claims
|
||||
};
|
||||
let header = jsonwebtoken::Header {
|
||||
typ: Some("JWT".to_string()),
|
||||
alg: jsonwebtoken::Algorithm::HS256,
|
||||
..Default::default()
|
||||
};
|
||||
let secret = jsonwebtoken::EncodingKey::from_secret(&self.secret);
|
||||
let token = jsonwebtoken::encode(&header, &local_token_data, &secret)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to encode fedi JWT: {}", e);
|
||||
libfj::robocraft::DEFAULT_TOKEN.to_owned()
|
||||
});
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn update_local_database(&self, auth_info: &super::FederatedAuthInfo, services_info: &oj_serdes::society::ServiceDomains) -> Result<(), oj_rc_database::sea_orm::DbErr> {
|
||||
use oj_rc_database::sea_orm::IntoActiveModel;
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let fedi_id = if let Some(existing_fedi) = self.db.federation_by_domain(&services_info.root).await? {
|
||||
let mut active = existing_fedi.into_active_model();
|
||||
active.last_used_time = oj_rc_database::sea_orm::ActiveValue::Set(now);
|
||||
active.auth = oj_rc_database::sea_orm::ActiveValue::Set(services_info.auth.clone());
|
||||
active.cdn = oj_rc_database::sea_orm::ActiveValue::Set(services_info.cdn.clone());
|
||||
active.factory = oj_rc_database::sea_orm::ActiveValue::Set(services_info.factory.clone());
|
||||
active.society = oj_rc_database::sea_orm::ActiveValue::Set(services_info.society.clone());
|
||||
self.db.update_federation(active).await?.id
|
||||
} else {
|
||||
let new_entity = oj_rc_database::schema::federation::ActiveModel {
|
||||
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
|
||||
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
|
||||
last_used_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
|
||||
domain: oj_rc_database::sea_orm::ActiveValue::Set(services_info.root.clone()),
|
||||
auth: oj_rc_database::sea_orm::ActiveValue::Set(services_info.auth.clone()),
|
||||
cdn: oj_rc_database::sea_orm::ActiveValue::Set(services_info.cdn.clone()),
|
||||
factory: oj_rc_database::sea_orm::ActiveValue::Set(services_info.factory.clone()),
|
||||
society: oj_rc_database::sea_orm::ActiveValue::Set(services_info.society.clone()),
|
||||
};
|
||||
self.db.insert_federation(new_entity).await?.id
|
||||
};
|
||||
let qualified_name = format!("{}#{}", auth_info.display_name, services_info.root);
|
||||
if let Some(existing_user) = self.db.user_by_display_name_and_federation(qualified_name.clone(), fedi_id).await? {
|
||||
log::info!("Using existing federated user with id {} for {} from {}", existing_user.id, auth_info.display_name, auth_info.domain);
|
||||
} else {
|
||||
let new_id = super::initial_data::register_new_federated_user(auth_info, fedi_id, &qualified_name, self.db.as_ref()).await?;
|
||||
log::info!("Created federated user with id {} for {} from {}", new_id, auth_info.display_name, auth_info.domain);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remote_auth_impl(&self, auth_info: &FederatedAuthenticationPayload, challenge: &str, federation: &Option<crate::persist::config::Federation>) -> Result<String, super::AuthError> {
|
||||
if auth_info.display_name.is_empty() {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation login with empty username"),
|
||||
code: crate::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
});
|
||||
}
|
||||
if let Some(fedi_conf) = federation {
|
||||
if auth_info.domain_source.is_empty() {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation login with empty domain_source"),
|
||||
code: crate::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
});
|
||||
}
|
||||
if auth_info.domain_target != *self.domain {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation login with not my domain_target"),
|
||||
code: crate::data::error_codes::AuthErrorCode::InvalidDisplayName,
|
||||
});
|
||||
}
|
||||
if self.is_defederated_from(&auth_info.domain_source, fedi_conf) {
|
||||
return Err(super::AuthError {
|
||||
message: format!("Refusing federation with {} for logging in {}", auth_info.domain_source, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::PasswordInvalidated,
|
||||
});
|
||||
}
|
||||
let user_info = super::UserAuthInfo::Username {
|
||||
username: auth_info.display_name.clone(),
|
||||
password: auth_info.password.clone(),
|
||||
};
|
||||
let login_info = self.login_internal(user_info, Some(auth_info.domain_source.clone())).await?;
|
||||
Ok(Self::generate_access_code(
|
||||
&*self.auth,
|
||||
challenge,
|
||||
&auth_info.display_name,
|
||||
&login_info.response.token,
|
||||
&login_info.response.refresh_token,
|
||||
&*self.secret,
|
||||
self.nonce_provider(),
|
||||
))
|
||||
} else {
|
||||
Err(super::AuthError {
|
||||
message: format!("Refusing federation with {} for logging in {} (federation not configured)", auth_info.domain_source, auth_info.display_name),
|
||||
code: crate::data::error_codes::AuthErrorCode::PasswordInvalidated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn remote_token_impl(&self, access_code: &str, verifier: &str) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
let (_code, tokens) = Self::read_access_code(access_code, &self.auth, &self.secret, self.nonce_provider())
|
||||
.map_err(|_| super::AuthError {
|
||||
message: "Failed to read access code".to_owned(),
|
||||
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
|
||||
})?;
|
||||
if !Self::validate_pkce(&tokens.code_challenge, verifier) {
|
||||
return Err(super::AuthError {
|
||||
message: "Failed to validate PKCE".to_owned(),
|
||||
code: crate::data::error_codes::AuthErrorCode::AccountUnconfirmed,
|
||||
});
|
||||
}
|
||||
Ok(super::UserLoginInfo {
|
||||
response: libfj::robocraft::AuthenticationResponseInfo {
|
||||
token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
refresh_token_expiry: "0".to_owned(), // TODO
|
||||
},
|
||||
is_new: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_key(secret: &[u8], issuer: &str) -> Vec<u8> {
|
||||
if secret.len() < 32 {
|
||||
let mut temp_key = Vec::from(secret);
|
||||
for b in issuer.bytes() {
|
||||
temp_key.push(b);
|
||||
if temp_key.len() >= 32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if temp_key.len() < 32 {
|
||||
for _ in temp_key.len()..32 {
|
||||
temp_key.push(0);
|
||||
}
|
||||
}
|
||||
temp_key
|
||||
} else if secret.len() > 32 {
|
||||
let mut temp_key = Vec::from(secret);
|
||||
temp_key.truncate(32);
|
||||
temp_key
|
||||
} else {
|
||||
Vec::from(secret)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_access_code(issuer: &str, challenge: &str, display_name: &str, access_token: &str, refresh_token: &str, secret: &[u8], mut noncer: NonceProvider) -> String {
|
||||
use ring::aead::BoundKey;
|
||||
use base64::Engine;
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
noncer.reset(now);
|
||||
let secure_data = SecuredCodes {
|
||||
access_token: access_token.to_owned(),
|
||||
refresh_token: refresh_token.to_owned(),
|
||||
code_challenge: challenge.to_owned(),
|
||||
};
|
||||
let secure_data_str = serde_json::to_string(&secure_data).unwrap();
|
||||
let key = Self::build_key(secret, issuer);
|
||||
let enc_key = ring::aead::UnboundKey::new(&ring::aead::AES_256_GCM, &key).unwrap();
|
||||
let mut seal_key = ring::aead::SealingKey::new(enc_key, noncer);
|
||||
let mut enc_data = Vec::from(secure_data_str.as_bytes());
|
||||
seal_key.seal_in_place_append_tag(ring::aead::Aad::from(ACCESS_CODE_AAD), &mut enc_data).unwrap();
|
||||
let secured_b64 = base64::engine::general_purpose::STANDARD.encode(enc_data);
|
||||
let token_data = AccessCode {
|
||||
aud: issuer.to_owned(),
|
||||
exp: now + 60, // 60s
|
||||
iat: now,
|
||||
iss: issuer.to_owned(),
|
||||
sub: display_name.to_owned(),
|
||||
secured: secured_b64,
|
||||
};
|
||||
let data_str = serde_json::to_string(&token_data).unwrap();
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data_str)
|
||||
}
|
||||
|
||||
fn read_access_code(code: &str, issuer: &str, secret: &[u8], mut noncer: NonceProvider) -> Result<(AccessCode, SecuredCodes), ()> {
|
||||
use ring::aead::BoundKey;
|
||||
use base64::Engine;
|
||||
let json_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(code)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to decode Base64 access code: {}", e);
|
||||
})?;
|
||||
let access_code: AccessCode = serde_json::from_slice(&json_bytes)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to decode JSON access code: {}", e);
|
||||
})?;
|
||||
let mut enc_data = base64::engine::general_purpose::STANDARD.decode(&access_code.secured)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to decode Base64 secure code: {}", e);
|
||||
})?;
|
||||
noncer.reset(access_code.iat);
|
||||
let key = Self::build_key(secret, issuer);
|
||||
let enc_key = ring::aead::UnboundKey::new(&ring::aead::AES_256_GCM, &key).unwrap();
|
||||
let mut opening_key = ring::aead::OpeningKey::new(enc_key, noncer);
|
||||
let plaintext = opening_key.open_in_place(ring::aead::Aad::from(ACCESS_CODE_AAD), &mut enc_data)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to decrypt secure code: {}", e);
|
||||
})?;
|
||||
let secure_data: SecuredCodes = serde_json::from_slice(&plaintext)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to decode JSON secure code: {}", e);
|
||||
})?;
|
||||
Ok((access_code, secure_data))
|
||||
}
|
||||
|
||||
fn validate_pkce(challenge: &str, verifier: &str) -> bool {
|
||||
use sha2::Digest;
|
||||
use base64::Engine;
|
||||
let hash = sha2::Sha256::new()
|
||||
.chain_update(verifier.as_bytes())
|
||||
.finalize();
|
||||
let expected = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash);
|
||||
expected == challenge
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::FederatedAuthenticator for super::AccountProvider {
|
||||
async fn local_login(&self, info: super::FederatedAuthInfo) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
self.local_login_impl(info, &self.federation).await
|
||||
}
|
||||
|
||||
async fn remote_auth(&self, info: &FederatedAuthenticationPayload, challenge: &str) -> Result<String, super::AuthError> {
|
||||
self.remote_auth_impl(info, challenge, &self.federation).await
|
||||
}
|
||||
|
||||
async fn remote_token(&self, access_code: &str, verifier: &str) -> Result<super::UserLoginInfo, super::AuthError> {
|
||||
self.remote_token_impl(access_code, verifier).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,14 @@ pub async fn register_new_user(info: &super::RegistrationInfo, db: &oj_rc_databa
|
||||
Ok(user_data.id)
|
||||
}
|
||||
|
||||
pub async fn register_new_federated_user(info: &super::FederatedAuthInfo, fedi_id: i32, qualified_name: &str, db: &oj_rc_database::Database) -> Result<i32, oj_rc_database::sea_orm::DbErr> {
|
||||
let user_data = db.insert_user(default_fedi_user_data(info, fedi_id, qualified_name)).await?;
|
||||
db.insert_perms(default_user_perms(user_data.id)).await?;
|
||||
db.insert_user_aux(default_user_aux_data(user_data.id)).await?;
|
||||
db.insert_garages(default_garage_slots(user_data.id)).await?;
|
||||
Ok(user_data.id)
|
||||
}
|
||||
|
||||
fn default_user_data(info: &super::RegistrationInfo) -> oj_rc_database::schema::user::ActiveModel {
|
||||
let password = {
|
||||
use argon2::password_hash::PasswordHasher;
|
||||
@@ -61,6 +69,32 @@ fn default_user_data(info: &super::RegistrationInfo) -> oj_rc_database::schema::
|
||||
password: oj_rc_database::sea_orm::ActiveValue::Set(password),
|
||||
email: oj_rc_database::sea_orm::ActiveValue::Set(info.email.clone().unwrap_or_else(|| "".to_owned())),
|
||||
steam_id: oj_rc_database::sea_orm::ActiveValue::Set(steam_id),
|
||||
federation_id: oj_rc_database::sea_orm::ActiveValue::Set(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_fedi_user_data(info: &super::FederatedAuthInfo, fedi_id: i32, qualified_name: &str) -> oj_rc_database::schema::user::ActiveModel {
|
||||
let password = {
|
||||
use argon2::password_hash::PasswordHasher;
|
||||
let argon2_algo = argon2::Argon2::default();
|
||||
let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
|
||||
match argon2_algo.hash_password(info.password.as_bytes(), &salt) {
|
||||
Err(e) => {
|
||||
log::error!("Failed to hash password for user {}: {}", info.display_name, e);
|
||||
"".to_owned()
|
||||
},
|
||||
Ok(password) => password.to_string(),
|
||||
}
|
||||
};
|
||||
oj_rc_database::schema::user::ActiveModel {
|
||||
id: Default::default(),
|
||||
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
|
||||
public_id: oj_rc_database::sea_orm::ActiveValue::Set(qualified_name.to_owned()),
|
||||
display_name: oj_rc_database::sea_orm::ActiveValue::Set(qualified_name.to_owned()),
|
||||
password: oj_rc_database::sea_orm::ActiveValue::Set(password),
|
||||
email: oj_rc_database::sea_orm::ActiveValue::Set(format!("{}@{}", info.display_name, info.domain)),
|
||||
steam_id: oj_rc_database::sea_orm::ActiveValue::Set(None),
|
||||
federation_id: oj_rc_database::sea_orm::ActiveValue::Set(Some(fedi_id)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ use serde::{Serialize, Deserialize};
|
||||
impl super::account_json::UserData {
|
||||
async fn listen_on_websocket<D: serde::de::DeserializeOwned>(&self, server_name: &str) -> Result<super::IntercomListener<D>, reqwest_websocket::Error> {
|
||||
use reqwest_websocket::Upgrade;
|
||||
let token = generate_token(format!("{}/{}", server_name, self.account.public_id).as_bytes(), &self.secret);
|
||||
let url_encoded_pub_id = urlencoding::encode(&self.account.public_id);
|
||||
let token = generate_token(format!("{}/{}", server_name, url_encoded_pub_id).as_bytes(), &self.secret);
|
||||
let auth_header_val = format!("Internal {}", token);
|
||||
let url = format!("{}/intercom/{}/{}", self.intercom, server_name, self.account.public_id);
|
||||
let url = format!("{}/intercom/{}/{}", self.intercom, server_name, url_encoded_pub_id);
|
||||
log::debug!("Listening on websocket {}", url);
|
||||
let websocket = self.http_client.get(url)
|
||||
.header("Authorization", auth_header_val)
|
||||
@@ -21,7 +22,7 @@ impl super::account_json::UserData {
|
||||
}
|
||||
|
||||
async fn post_to_intercom<D: serde::Serialize>(&self, data: &D, server_name: &str, operation: &str) -> Result<(), reqwest::Error> {
|
||||
let path = format!("{}/{}/{}", server_name, self.account.public_id, operation);
|
||||
let path = format!("{}/{}/{}", server_name, urlencoding::encode(&self.account.public_id), operation);
|
||||
let token = generate_token(path.as_bytes(), &self.secret);
|
||||
let auth_header_val = format!("Internal {}", token);
|
||||
let url = format!("{}/intercom/{}", self.auth, path);
|
||||
@@ -55,9 +56,10 @@ impl super::account_json::UserData {
|
||||
impl super::IntercomUser for super::account_json::UserData {
|
||||
async fn save_custom_avatar(&self, image: Vec<u8>) -> Result<(), polariton_server::operations::SimpleOpError> {
|
||||
// seems to always be jpg
|
||||
let token = generate_token(self.account.public_id.as_bytes(), &self.secret);
|
||||
let url_encoded_pub_id = urlencoding::encode(&self.account.public_id);
|
||||
let token = generate_token(url_encoded_pub_id.as_bytes(), &self.secret);
|
||||
let auth_header_val = format!("Internal {}", token);
|
||||
let url = format!("{}/customavatar/Live/{}", self.cdn, self.account.public_id);
|
||||
let url = format!("{}/customavatar/Live/{}", self.cdn, url_encoded_pub_id);
|
||||
if let Err(e) = self.http_client.post(url)
|
||||
.header("Authorization", auth_header_val)
|
||||
.body(image)
|
||||
|
||||
@@ -11,7 +11,7 @@ mod inventory;
|
||||
pub use inventory::{UnlockedParts, UnlockOverride};
|
||||
|
||||
mod traits;
|
||||
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser, GarageWebInfo, GarageWebStats, SanctionWebStats, AccountWebStats, SocialWebStats};
|
||||
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, FederatedAuthInfo, UserLoginInfo, UserAuthenticator, FederatedAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser, GarageWebInfo, GarageWebStats, SanctionWebStats, AccountWebStats, SocialWebStats};
|
||||
|
||||
pub mod intercom;
|
||||
pub use intercom::generate_token as generate_intercom_token;
|
||||
@@ -28,7 +28,7 @@ mod team;
|
||||
pub use team::{TeamChooser, StandardTeamChooser};
|
||||
mod web;
|
||||
|
||||
mod federation;
|
||||
pub mod federation;
|
||||
pub use federation::Federation;
|
||||
|
||||
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";
|
||||
|
||||
@@ -22,7 +22,7 @@ pub enum UserAuthInfo {
|
||||
Email {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
impl UserAuthInfo {
|
||||
@@ -35,6 +35,12 @@ impl UserAuthInfo {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FederatedAuthInfo {
|
||||
pub display_name: String,
|
||||
pub password: String,
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
pub enum UserId {
|
||||
SteamId(u64),
|
||||
Email(String),
|
||||
@@ -68,13 +74,20 @@ pub trait UserProvider<C> {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait UserAuthenticator {
|
||||
pub trait UserAuthenticator: FederatedAuthenticator {
|
||||
async fn login(&self, info: UserAuthInfo) -> Result<UserLoginInfo, AuthError>;
|
||||
async fn user_exists(&self, user: UserId) -> Result<bool, String>;
|
||||
async fn register(&self, info: RegistrationInfo) -> Result<i32, String>;
|
||||
async fn verify(&self, token: String) -> Result<crate::auth::Token, AuthError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait FederatedAuthenticator {
|
||||
async fn local_login(&self, info: FederatedAuthInfo) -> Result<UserLoginInfo, AuthError>;
|
||||
async fn remote_auth(&self, info: &super::federation::FederatedAuthenticationPayload, challenge: &str) -> Result<String, AuthError>;
|
||||
async fn remote_token(&self, access_token: &str, verifier: &str) -> Result<UserLoginInfo, AuthError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait User<C>: ChatUser + SocialUser + SocialUserC<C> + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser + FactoryUser {
|
||||
async fn unlocked_parts(&self) -> Vec<u32>;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260705_000001_create_federation_table"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
// Define how to apply this migration: Create the Federation table.
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(crate::schema::federation::Entity)
|
||||
.col(
|
||||
ColumnDef::new(crate::schema::federation::Column::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::CreationTime).big_integer().not_null())
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::LastUsedTime).big_integer().not_null())
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::Domain).string().not_null())
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::Auth).string().not_null())
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::Cdn).string().not_null())
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::Factory).string().not_null())
|
||||
.col(ColumnDef::new(crate::schema::federation::Column::Society).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Define how to rollback this migration: Drop the Federation table.
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(crate::schema::federation::Entity).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260705_000002_add_user_federation"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
// Define how to apply this migration: Add user federation id column
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// SQLite doesn't support multiple alter options in one operation
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(crate::schema::user::Entity)
|
||||
.add_column(ColumnDef::new(crate::schema::user::Column::FederationId).integer().null())
|
||||
.to_owned()
|
||||
)
|
||||
.await?;
|
||||
if manager.get_connection().get_database_backend() != sea_orm::DbBackend::Sqlite {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(crate::schema::user::Entity)
|
||||
.add_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk-user_federation_id")
|
||||
.from(crate::schema::user::Entity, crate::schema::user::Column::FederationId)
|
||||
.to(crate::schema::federation::Entity, crate::schema::federation::Column::Id)
|
||||
.get_foreign_key(),
|
||||
)
|
||||
.to_owned()
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
// SQLite doesn't support altering foreign keys of an existing table
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Define how to rollback this migration: Drop the added column
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// SQLite doesn't support multiple alter options in one operation
|
||||
if manager.get_connection().get_database_backend() != sea_orm::DbBackend::Sqlite {
|
||||
// SQLite doesn't support altering foreign keys of an existing table
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(crate::schema::user::Entity)
|
||||
.drop_foreign_key("fk-user_federation_id")
|
||||
.to_owned()
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(crate::schema::user::Entity)
|
||||
.drop_column(crate::schema::user::Column::FederationId)
|
||||
.to_owned()
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ mod m20260215_000001_create_friend_table;
|
||||
mod m20260221_000001_create_clan_table;
|
||||
mod m20260221_000002_create_clan_member_table;
|
||||
mod m20260317_000001_add_game_overrides;
|
||||
mod m20260705_000001_create_federation_table;
|
||||
mod m20260705_000002_add_user_federation;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -45,6 +47,8 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260221_000001_create_clan_table::Migration),
|
||||
Box::new(m20260221_000002_create_clan_member_table::Migration),
|
||||
Box::new(m20260317_000001_add_game_overrides::Migration),
|
||||
Box::new(m20260705_000001_create_federation_table::Migration),
|
||||
Box::new(m20260705_000002_add_user_federation::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
29
rc_database/src/schema/federation.rs
Normal file
29
rc_database/src/schema/federation.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "federations")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub creation_time: i64, // seconds since unix epoch
|
||||
pub last_used_time: i64, // seconds since unix epoch
|
||||
pub domain: String, // root domain
|
||||
pub auth: String,
|
||||
pub cdn: String,
|
||||
pub factory: String,
|
||||
pub society: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::user::Entity")]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -15,6 +15,7 @@ pub mod factory;
|
||||
pub mod friend;
|
||||
pub mod clan;
|
||||
pub mod clan_member;
|
||||
pub mod federation;
|
||||
|
||||
pub fn parse_int_csv(s: &str) -> Vec<u32> {
|
||||
s.split(',').filter_map(|i_as_s| {
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Model {
|
||||
pub password: String,
|
||||
pub email: String,
|
||||
pub steam_id: Option<String>, // u64
|
||||
pub federation_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -29,6 +30,12 @@ pub enum Relation {
|
||||
FactoryUploads,
|
||||
#[sea_orm(has_many = "super::friend::Entity")]
|
||||
Friends, // this will probably join the wrong column (i.e. in the wrong direction)
|
||||
#[sea_orm(
|
||||
belongs_to = "super::federation::Entity",
|
||||
from = "Column::FederationId",
|
||||
to = "super::federation::Column::Id"
|
||||
)]
|
||||
Federation,
|
||||
}
|
||||
|
||||
impl Related<super::permissions::Entity> for Entity {
|
||||
@@ -73,4 +80,10 @@ impl Related<super::friend::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::federation::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Federation.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -39,6 +39,17 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn user_by_display_name_and_federation(&self, public_id: String, federation_id: i32) -> Result<Option<crate::schema::user::Model>, sea_orm::DbErr> {
|
||||
crate::schema::user::Entity::find()
|
||||
.filter(sea_orm::sea_query::Expr::expr(
|
||||
sea_orm::sea_query::Func::lower(crate::schema::user::Column::DisplayName.into_expr())
|
||||
).eq(public_id.to_lowercase())
|
||||
)
|
||||
.filter(crate::schema::user::Column::FederationId.eq(Some(federation_id)))
|
||||
.one(self.orm.as_ref())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn user_by_public_id(&self, public_id: String) -> Result<Option<crate::schema::user::Model>, sea_orm::DbErr> {
|
||||
crate::schema::user::Entity::find()
|
||||
.filter(crate::schema::user::Column::PublicId.eq(public_id))
|
||||
@@ -919,6 +930,31 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn federation_by_id(&self, id: i32) -> Result<Option<crate::schema::federation::Model>, sea_orm::DbErr> {
|
||||
crate::schema::federation::Entity::find_by_id(id)
|
||||
.one(self.orm.as_ref())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn federation_by_domain(&self, domain: &str) -> Result<Option<crate::schema::federation::Model>, sea_orm::DbErr> {
|
||||
crate::schema::federation::Entity::find()
|
||||
.filter(crate::schema::federation::Column::Id.eq(domain))
|
||||
.one(self.orm.as_ref())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert_federation(&self, entity: crate::schema::federation::ActiveModel) -> Result<crate::schema::federation::Model, sea_orm::DbErr> {
|
||||
#[cfg(debug_assertions)]
|
||||
assert!(matches!(entity.id, sea_orm::ActiveValue::NotSet));
|
||||
entity.insert(self.orm.as_ref()).await
|
||||
}
|
||||
|
||||
pub async fn update_federation(&self, entity: crate::schema::federation::ActiveModel) -> Result<crate::schema::federation::Model, sea_orm::DbErr> {
|
||||
crate::schema::federation::Entity::update(entity)
|
||||
.exec(self.orm.as_ref())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn metrics(&self) -> super::DatabaseMetrics {
|
||||
self.metrics.lock().unwrap().snapshot()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ oj_rc_core = { version = "*", path = "../rc_core" }
|
||||
oj_rc_plugins = { version = "*", path = "../rc_plugins" }
|
||||
oj_rc_factory = { version = "*", path = "../rc_factory" }
|
||||
oj_convert.workspace = true
|
||||
oj_serdes.workspace = true
|
||||
libfj.workspace = true
|
||||
git-version.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod garage;
|
||||
pub mod config;
|
||||
pub mod urls;
|
||||
|
||||
pub fn init(config: &dyn oj_rc_core::ConfigProvider<()>) {
|
||||
config::init(config);
|
||||
urls::init(config);
|
||||
}
|
||||
|
||||
25
rc_society/src/api/urls.rs
Normal file
25
rc_society/src/api/urls.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use actix_web::{HttpResponse, Responder, get, http::header::ContentType};
|
||||
|
||||
static JSON_DATA: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
|
||||
pub(super) fn init(config: &dyn oj_rc_core::ConfigProvider<()>) {
|
||||
JSON_DATA.get_or_init(|| {
|
||||
let urls = config.server_config();
|
||||
let data = oj_serdes::society::ServiceDomains {
|
||||
root: urls.domain,
|
||||
auth: urls.auth_url,
|
||||
cdn: urls.cdn_url,
|
||||
factory: urls.factory_url,
|
||||
society: urls.society_url,
|
||||
};
|
||||
serde_json::to_string(&data).expect("ServiceDomains did not serialize")
|
||||
});
|
||||
}
|
||||
|
||||
#[get("/api/v1/services.json")]
|
||||
pub async fn get() -> impl Responder {
|
||||
let data = JSON_DATA.get().expect("ServiceDomains JSON init failure").to_owned();
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::json())
|
||||
.body(data)
|
||||
}
|
||||
@@ -104,6 +104,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.service(web::user_federation::post_off)
|
||||
.service(web::user_federation::post_on)
|
||||
.service(api::config::get)
|
||||
.service(api::urls::get)
|
||||
})
|
||||
.bind((cli_args.ip, cli_args.port))?
|
||||
.run()
|
||||
|
||||
Reference in New Issue
Block a user