From 01c8cedd0f898fc5b0928281b5aa526929fccbef Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Wed, 18 Sep 2024 04:23:33 -0400 Subject: [PATCH] identity: implemented google id flow --- Cargo.lock | 494 ++++++++++++++++-- Cargo.toml | 16 +- apps/identity_server/Cargo.toml | 11 +- .../src/google_jwks_provider.rs | 141 +++++ apps/identity_server/src/lib.rs | 11 + apps/identity_server/src/main.rs | 22 +- apps/identity_server/src/oauth.rs | 129 +++++ apps/networked_physics_demo/client/Cargo.toml | 2 +- apps/social/client/Cargo.toml | 2 +- crates/header-parsing/Cargo.toml | 13 + crates/header-parsing/src/lib.rs | 98 ++++ crates/replicate/server/Cargo.toml | 2 +- 12 files changed, 891 insertions(+), 50 deletions(-) create mode 100644 apps/identity_server/src/google_jwks_provider.rs create mode 100644 apps/identity_server/src/oauth.rs create mode 100644 crates/header-parsing/Cargo.toml create mode 100644 crates/header-parsing/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e7feb07..4d806cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,6 +365,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.7" @@ -853,6 +859,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie 0.18.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.4.1" @@ -1204,7 +1233,7 @@ dependencies = [ "bevy_utils", "downcast-rs", "fixedbitset", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "thiserror", "thread_local", @@ -1462,7 +1491,7 @@ checksum = "eb270c98a96243b29465139ed10bda2f675d00a11904f6588a5f7fc4774119c7" dependencies = [ "proc-macro2", "quote", - "rustc-hash", + "rustc-hash 1.1.0", "syn 2.0.77", "toml_edit 0.21.1", ] @@ -2089,7 +2118,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.77", ] @@ -2521,7 +2550,7 @@ dependencies = [ "cocoa-foundation", "core-foundation", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "libc", "objc", ] @@ -2728,6 +2757,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time 0.3.36", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2753,7 +2793,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -3112,6 +3152,27 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "unicode-xid", +] + [[package]] name = "did-chain" version = "0.0.0" @@ -3751,6 +3812,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -3758,7 +3828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -3772,6 +3842,12 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -4296,6 +4372,25 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -4331,6 +4426,13 @@ dependencies = [ "winapi", ] +[[package]] +name = "header-parsing" +version = "0.0.0" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -4511,7 +4613,7 @@ dependencies = [ "async-channel 1.9.0", "async-std", "base64 0.13.1", - "cookie", + "cookie 0.14.4", "futures-lite 1.13.0", "infer", "pin-project-lite", @@ -4550,6 +4652,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -4558,6 +4661,41 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls 0.23.13", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 0.26.3", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -4567,12 +4705,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2 0.5.7", "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -4619,15 +4762,22 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" name = "identity_server" version = "0.0.0" dependencies = [ + "arc-swap", "axum", + "axum-extra", + "axum-macros", "base64 0.21.7", "clap", "color-eyre", + "derive_more 1.0.0", "did-simple", + "header-parsing", "hex-literal", "http-body-util", "jose-jwk", + "jsonwebtoken", "rand 0.8.5", + "reqwest", "serde", "serde_json", "sqlx", @@ -4763,6 +4913,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + [[package]] name = "is-terminal" version = "0.4.12" @@ -4903,6 +5059,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "ring 0.17.8", + "serde", + "serde_json", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -5069,7 +5238,7 @@ dependencies = [ "chrono", "console_error_panic_hook", "crossbeam-channel", - "derive_more", + "derive_more 0.99.18", "enum_delegate", "enum_dispatch", "getrandom 0.2.15", @@ -5237,7 +5406,7 @@ dependencies = [ "bitflags 2.6.0", "block", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", @@ -5324,7 +5493,7 @@ dependencies = [ "log", "num-traits", "pp-rs", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror", @@ -5345,7 +5514,7 @@ dependencies = [ "once_cell", "regex", "regex-syntax 0.8.4", - "rustc-hash", + "rustc-hash 1.1.0", "thiserror", "tracing", "unicode-ident", @@ -5394,6 +5563,23 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e943b2c21337b7e3ec6678500687cdc741b7639ad457f234693352075c082204" +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.8.0" @@ -5865,12 +6051,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "openxr" version = "0.18.0" @@ -6011,7 +6235,7 @@ dependencies = [ "nalgebra", "num-derive", "num-traits", - "rustc-hash", + "rustc-hash 1.1.0", "simba", "slab", "smallvec", @@ -6070,7 +6294,7 @@ dependencies = [ "bevy_rapier3d", "bytes", "color-eyre", - "derive_more", + "derive_more 1.0.0", "rand 0.8.5", "rand_xoshiro", "replicate-client", @@ -6353,15 +6577,33 @@ checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ "bytes", "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", + "quinn-proto 0.10.6", + "quinn-udp 0.4.1", + "rustc-hash 1.1.0", "rustls 0.21.12", "thiserror", "tokio", "tracing", ] +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto 0.11.8", + "quinn-udp 0.5.5", + "rustc-hash 2.0.0", + "rustls 0.23.13", + "socket2 0.5.7", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "quinn-proto" version = "0.10.6" @@ -6371,7 +6613,7 @@ dependencies = [ "bytes", "rand 0.8.5", "ring 0.16.20", - "rustc-hash", + "rustc-hash 1.1.0", "rustls 0.21.12", "rustls-native-certs", "slab", @@ -6380,6 +6622,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring 0.17.8", + "rustc-hash 2.0.0", + "rustls 0.23.13", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + [[package]] name = "quinn-udp" version = "0.4.1" @@ -6393,6 +6652,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.7", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -6534,7 +6806,7 @@ dependencies = [ "num-traits", "ordered-float", "parry3d", - "rustc-hash", + "rustc-hash 1.1.0", "simba", ] @@ -6723,7 +6995,7 @@ dependencies = [ "clap", "color-eyre", "dashmap", - "derive_more", + "derive_more 1.0.0", "eyre", "futures", "rand 0.8.5", @@ -6740,6 +7012,54 @@ dependencies = [ "wtransport", ] +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn 0.11.5", + "rustls 0.23.13", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.3", + "windows-registry", +] + [[package]] name = "residua-zigzag" version = "0.1.0" @@ -6845,6 +7165,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.2.3" @@ -6933,7 +7259,21 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +dependencies = [ + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -6987,9 +7327,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -7009,7 +7349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" dependencies = [ "byteorder", - "derive_more", + "derive_more 0.99.18", "twox-hash", ] @@ -7407,7 +7747,7 @@ dependencies = [ "clap", "color-eyre", "cpal", - "derive_more", + "derive_more 1.0.0", "egui", "egui-picking", "jni 0.21.1", @@ -7430,7 +7770,7 @@ dependencies = [ "bevy_egui", "bevy_vrm", "color-eyre", - "derive_more", + "derive_more 1.0.0", "egui", "portpicker", "serde", @@ -7443,7 +7783,7 @@ version = "0.0.0" dependencies = [ "bevy", "color-eyre", - "derive_more", + "derive_more 1.0.0", "lightyear", "portpicker", "random-number", @@ -7949,6 +8289,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -7975,6 +8318,27 @@ dependencies = [ "windows 0.52.0", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "taffy" version = "0.3.19" @@ -8162,6 +8526,27 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.13", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-serde" version = "0.9.0" @@ -8376,6 +8761,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.21.1" @@ -8529,7 +8920,7 @@ dependencies = [ "once_cell", "rustls 0.22.4", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.8", "url", "webpki-roots 0.26.3", ] @@ -8608,6 +8999,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -8922,7 +9322,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle 0.6.2", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "web-sys", @@ -8966,7 +9366,7 @@ dependencies = [ "range-alloc", "raw-window-handle 0.6.2", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "wasm-bindgen", @@ -9110,7 +9510,7 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -9122,7 +9522,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement 0.57.0", "windows-interface 0.57.0", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -9170,6 +9570,17 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -9179,6 +9590,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -9458,7 +9888,7 @@ checksum = "3367f124e6272c52b83a8e7c9aaea274782b3f32a8997ed0b33776b540a9a1d2" dependencies = [ "bytes", "pem", - "quinn", + "quinn 0.10.2", "rcgen", "rustls 0.21.12", "rustls-native-certs", diff --git a/Cargo.toml b/Cargo.toml index e1205ee..92bce20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/did-chain", "crates/did-simple", "crates/egui-picking", + "crates/header-parsing", "crates/picking-xr", "crates/replicate/client", "crates/replicate/common", @@ -29,8 +30,11 @@ edition = "2021" rust-version = "1.78.0" [workspace.dependencies] +arc-swap = "1.7.1" async-compat = "0.2.4" axum = "0.7.5" +axum-extra = "0.9.3" +axum-macros = "0.4.1" base64 = "0.21.7" bevy = { version = "0.13", features = ["serialize"] } bevy-inspector-egui = "0.23.4" @@ -54,7 +58,9 @@ egui = "0.26" egui-picking = { path = "crates/egui-picking" } eyre = "0.6" futures = "0.3.30" +header-parsing.path = "crates/header-parsing" hex-literal = "0.4.1" +http = "1.1.0" http-body-util = "0.1.2" jose-jwk = { version = "0.1.2", default-features = false } lightyear = "0.12" @@ -67,6 +73,7 @@ rand_xoshiro = "0.6.0" random-number = "0.1.8" replicate-client.path = "crates/replicate/client" replicate-server.path = "crates/replicate/server" +reqwest = { version = "0.12.7", features = ["rustls-tls"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.114" slotmap = "1.0.7" @@ -85,15 +92,8 @@ uuid = "1.7.0" wtransport = "0.1.13" [workspace.dependencies.derive_more] -version = "0.99" +version = "1.0.0" default-features = false -features = [ - "add", - "deref", - "deref_mut", - "mul", - "from", -] [workspace.dependencies.opus] git = "https://github.com/Schmarni-Dev/opus-rs" diff --git a/apps/identity_server/Cargo.toml b/apps/identity_server/Cargo.toml index afd8c4f..5756b00 100644 --- a/apps/identity_server/Cargo.toml +++ b/apps/identity_server/Cargo.toml @@ -9,19 +9,26 @@ description = "Self-custodial identity using did:web" publish = false [dependencies] -axum.workspace = true +arc-swap.workspace = true +axum = { workspace = true, features = [] } +axum-extra = { workspace = true, features = ["cookie"] } +axum-macros.workspace = true clap = { workspace = true, features = ["derive", "env"] } color-eyre.workspace = true +derive_more = { workspace = true, features = ["debug"] } did-simple.workspace = true +header-parsing.workspace = true http-body-util.workspace = true jose-jwk = { workspace = true, default-features = false } +jsonwebtoken = { version = "9.3.0", default-features = false } rand.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true sqlx = { version = "0.8.0", features = ["runtime-tokio", "tls-rustls", "sqlite", "uuid", "migrate"] } thiserror.workspace = true tokio = { workspace = true, features = ["full"] } -tower-http = { workspace = true, features = ["trace"] } +tower-http = { workspace = true, features = ["trace", "fs"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing.workspace = true uuid = { workspace = true, features = ["std", "v4", "serde"] } diff --git a/apps/identity_server/src/google_jwks_provider.rs b/apps/identity_server/src/google_jwks_provider.rs new file mode 100644 index 0000000..35d8496 --- /dev/null +++ b/apps/identity_server/src/google_jwks_provider.rs @@ -0,0 +1,141 @@ +use std::{sync::Arc, time::Duration}; + +use arc_swap::ArcSwap; +use axum::async_trait; +use color_eyre::{eyre::WrapErr as _, Result, Section}; +use jsonwebtoken::jwk::JwkSet; +use reqwest::Url; +use tracing::info; + +/// Retrieves the latest JWKs for an external service. +/// +/// Example: This can be used to get the JWKs from google, located at +/// +/// +/// This provider exists to support mocking of the external interface, for the purposes +/// of testing. +#[derive(Debug)] +pub struct JwksProvider { + #[cfg(not(test))] + provider: HttpProvider, + #[cfg(test)] + provider: Box, +} + +impl JwksProvider { + pub fn google(client: reqwest::Client) -> Self { + Self { + #[cfg(not(test))] + provider: HttpProvider::google(client), + #[cfg(test)] + provider: Box::new(HttpProvider::google(client)), + } + } + pub async fn get(&self) -> Result> { + self.provider.get().await + } +} + +#[async_trait] +trait JwksProviderT: std::fmt::Debug + Send + Sync + 'static { + /// Gets the latest JWKS for google. + async fn get(&self) -> Result>; +} + +#[derive(Debug, Eq, PartialEq)] +pub struct CachedJwks { + jwks: JwkSet, + expires_at: std::time::Instant, +} + +impl CachedJwks { + /// Creates an empty set of JWKs, which is already expired. + fn new_expired() -> Self { + let now = std::time::Instant::now(); + let expires_at = now.checked_sub(Duration::from_secs(1)).unwrap_or(now); + Self { + jwks: JwkSet { keys: vec![] }, + expires_at, + } + } + + pub fn jwks(&self) -> &JwkSet { + &self.jwks + } + + fn is_expired(&self) -> bool { + self.expires_at <= std::time::Instant::now() + } +} + +/// Uses http to retrieve the JWKs. +#[derive(Debug)] +struct HttpProvider { + url: Url, + client: reqwest::Client, + cached_jwks: ArcSwap, +} + +impl HttpProvider { + /// Creates a provider that requests the JWKS over HTTP from google's url. + pub fn google(client: reqwest::Client) -> Self { + // Creates immediately expired empty keyset + Self { + client, + url: "https://www.googleapis.com/oauth2/v3/certs" + .try_into() + .unwrap(), + cached_jwks: ArcSwap::new(Arc::new(CachedJwks::new_expired())), + } + } +} + +#[async_trait] +impl JwksProviderT for HttpProvider { + /// Usually this is instantly ready with the JWKS, but if the cached value doesn't + /// exist + /// or is out of date, it will await on the new value. + async fn get(&self) -> Result> { + let cached_jwks = self.cached_jwks.load(); + if !cached_jwks.is_expired() { + return Ok(cached_jwks.to_owned()); + } + let response = self + .client + .get(self.url.clone()) + .send() + .await + .wrap_err("failed to initiate get request for certs") + .with_note(|| format!("url was {}", self.url))?; + let expires_at = { + if let Some(duration) = + header_parsing::time_until_max_age(response.headers()) + { + std::time::Instant::now() + duration + } else { + std::time::Instant::now() + } + }; + let serialized_keys = response + .bytes() + .await + .wrap_err("failed to get response body")?; + let jwks: JwkSet = serde_json::from_slice(&serialized_keys) + .wrap_err("unexpected response, expected a JWKS")?; + let cached_jwks = Arc::new(CachedJwks { jwks, expires_at }); + self.cached_jwks.store(Arc::clone(&cached_jwks)); + info!("cached JWKs: {cached_jwks:?}"); + Ok(cached_jwks) + } +} + +/// Always provides the same JWKs. +#[derive(Debug, Clone)] +struct StaticProvider(Arc); + +#[async_trait] +impl JwksProviderT for StaticProvider { + async fn get(&self) -> Result> { + Ok(Arc::clone(&self.0)) + } +} diff --git a/apps/identity_server/src/lib.rs b/apps/identity_server/src/lib.rs index 8c709c5..12c87d8 100644 --- a/apps/identity_server/src/lib.rs +++ b/apps/identity_server/src/lib.rs @@ -1,4 +1,6 @@ +pub mod google_jwks_provider; pub mod jwk; +pub mod oauth; pub mod v1; mod uuid; @@ -28,6 +30,7 @@ impl MigratedDbPool { #[derive(Debug)] pub struct RouterConfig { pub v1: crate::v1::RouterConfig, + pub oauth: crate::oauth::OAuthConfig, } impl RouterConfig { @@ -37,9 +40,17 @@ impl RouterConfig { .build() .await .wrap_err("failed to build v1 router")?; + + let oauth = self + .oauth + .build() + .await + .wrap_err("failed to build oauth router")?; + Ok(axum::Router::new() .route("/", get(root)) .nest("/api/v1", v1) + .nest("/oauth2", oauth) .layer(TraceLayer::new_for_http())) } } diff --git a/apps/identity_server/src/main.rs b/apps/identity_server/src/main.rs index 4bb5a54..b1d7105 100644 --- a/apps/identity_server/src/main.rs +++ b/apps/identity_server/src/main.rs @@ -2,7 +2,7 @@ use std::net::{Ipv6Addr, SocketAddr}; use clap::Parser as _; use color_eyre::eyre::Context as _; -use identity_server::MigratedDbPool; +use identity_server::{google_jwks_provider::JwksProvider, MigratedDbPool}; use std::path::PathBuf; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -13,6 +13,10 @@ struct Cli { port: u16, #[clap(long, env, default_value = "identities.db")] db_path: PathBuf, + /// The Google API OAuth2 Client ID. + /// See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid + #[clap(long, env)] + google_client_id: String, } #[tokio::main] @@ -43,15 +47,23 @@ async fn main() -> color_eyre::Result<()> { .await .wrap_err("failed to migrate db pool")? }; + let reqwest_client = reqwest::Client::new(); let v1_cfg = identity_server::v1::RouterConfig { uuid_provider: Default::default(), db_pool, }; - let router = identity_server::RouterConfig { v1: v1_cfg } - .build() - .await - .wrap_err("failed to build router")?; + let oauth_cfg = identity_server::oauth::OAuthConfig { + google_client_id: cli.google_client_id, + google_jwks_provider: JwksProvider::google(reqwest_client.clone()), + }; + let router = identity_server::RouterConfig { + v1: v1_cfg, + oauth: oauth_cfg, + } + .build() + .await + .wrap_err("failed to build router")?; let listener = tokio::net::TcpListener::bind(SocketAddr::new( Ipv6Addr::UNSPECIFIED.into(), diff --git a/apps/identity_server/src/oauth.rs b/apps/identity_server/src/oauth.rs new file mode 100644 index 0000000..5db8b5b --- /dev/null +++ b/apps/identity_server/src/oauth.rs @@ -0,0 +1,129 @@ +//! Routes for handling oauth with Google. + +use std::sync::Arc; + +use axum::{extract::State, response::IntoResponse, routing::post, Form, Router}; +use axum_extra::extract::cookie::CookieJar; +use color_eyre::eyre::{eyre, OptionExt, WrapErr as _}; +use jsonwebtoken::DecodingKey; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use tracing::{debug, error, info}; + +use crate::google_jwks_provider::JwksProvider; + +#[derive(Debug, Clone)] +struct RouterState { + google_jwt_validation: jsonwebtoken::Validation, + google_jwks_provider: Arc, +} + +#[derive(Debug)] +pub struct OAuthConfig { + pub google_client_id: String, + /// ArcSwap is used, so that another task can continuously refresh the keys. + pub google_jwks_provider: JwksProvider, +} + +impl OAuthConfig { + pub async fn build(self) -> color_eyre::Result { + let google_jwt_validation = { + let mut v = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256); + v.set_issuer(&["https://accounts.google.com", "accounts.google.com"]); + v.set_audience(&[self.google_client_id]); + v + }; + Ok(Router::new() + .route("/google", post(google)) + .with_state(RouterState { + google_jwt_validation, + google_jwks_provider: Arc::new(self.google_jwks_provider), + })) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct GoogleIdForm { + credential: String, + g_csrf_token: String, +} + +#[derive(thiserror::Error, Debug)] +enum GoogleErr { + #[error(transparent)] + Internal(#[from] color_eyre::eyre::Report), +} + +impl IntoResponse for GoogleErr { + fn into_response(self) -> axum::response::Response { + error!("{self:?}"); + match self { + Self::Internal(err) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + } + } + } +} + +/// See +#[derive(Debug, Serialize, Deserialize)] +struct GoogleIdTokenClaims { + /// Unique ID of user's google account. + sub: String, + name: String, + email: String, +} + +#[tracing::instrument(skip_all)] +#[axum_macros::debug_handler] +async fn google( + State(state): State, + jar: CookieJar, + Form(form): Form, +) -> Result<(), GoogleErr> { + // Check for CSRF + let cookie = jar + .get("g_csrf_token") + .ok_or_eyre("missing the double-submit csrf cookie")?; + if form.g_csrf_token != cookie.value() { + return Err(eyre!("double-submit csrf cookie mismatched!").into()); + } + + let google_keys = state + .google_jwks_provider + .get() + .await + .wrap_err("failed to get google's public keys")?; + debug!(?form, "received form"); + let token = &form.credential; + let header = + jsonwebtoken::decode_header(token).wrap_err("could not decode JWT header")?; + + // TODO: Start caching the decoding keys in a HashMap. + let decoding_key = { + let Some(ref token_key_id) = header.kid else { + return Err(eyre!("expected a `kid` field in the jwt header").into()); + }; + let google_key = google_keys + .jwks() + .keys + .iter() + .find(|jwk| jwk.common.key_id.as_ref() == Some(token_key_id)) + .ok_or_eyre( + "the provided credential's key did not match google's reported keys", + )?; + + DecodingKey::from_jwk(google_key) + .wrap_err("failed to create decoding key from jwk")? + }; + + let decoded_jwt = jsonwebtoken::decode::( + &form.credential, + &decoding_key, + &state.google_jwt_validation, + ) + .wrap_err("failed to validate jwt")?; + info!(claims = ?decoded_jwt.claims, "Got ID Token claims"); + // TODO: Do something with the user info that we got + Ok(()) +} diff --git a/apps/networked_physics_demo/client/Cargo.toml b/apps/networked_physics_demo/client/Cargo.toml index e607b13..dadd6b8 100644 --- a/apps/networked_physics_demo/client/Cargo.toml +++ b/apps/networked_physics_demo/client/Cargo.toml @@ -18,7 +18,7 @@ bevy_flycam.workspace = true bevy_rapier3d.workspace = true bytes.workspace = true color-eyre.workspace = true -derive_more.workspace = true +derive_more = { workspace = true, features = ["from"] } rand.workspace = true rand_xoshiro.workspace = true replicate-client.workspace = true diff --git a/apps/social/client/Cargo.toml b/apps/social/client/Cargo.toml index 2c659fb..5e51cec 100644 --- a/apps/social/client/Cargo.toml +++ b/apps/social/client/Cargo.toml @@ -28,7 +28,7 @@ portpicker = "0.1.1" rodio = "0.17.3" social-common.workspace = true social-networking.workspace = true -derive_more.workspace = true +derive_more = { workspace = true, features = ["display"] } picking-xr.workspace = true egui-picking.workspace = true bevy_egui.workspace = true diff --git a/crates/header-parsing/Cargo.toml b/crates/header-parsing/Cargo.toml new file mode 100644 index 0000000..2b253c6 --- /dev/null +++ b/crates/header-parsing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "header-parsing" +version = "0.0.0" +description = "Parsing utilities for http headers" +publish = false + +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +http.workspace = true diff --git a/crates/header-parsing/src/lib.rs b/crates/header-parsing/src/lib.rs new file mode 100644 index 0000000..8804dfe --- /dev/null +++ b/crates/header-parsing/src/lib.rs @@ -0,0 +1,98 @@ +#![forbid(unsafe_code)] + +use http::header::{AGE, CACHE_CONTROL}; +use std::time::Duration; + +/// Parses the `max-age=` value from the [`CACHE_CONTROL`] header. +pub fn parse_max_age(cache_control_value: &http::HeaderValue) -> Option { + let s = cache_control_value.to_str().ok()?; + s.split(',').map(str::trim).find_map(|s| { + s.split_once("max-age=") + .and_then(|(_front, back)| back.parse::().ok()) + }) +} + +// ---- Helpers for parsing Cache-Control header + +/// Extracts the age and max-age in seconds from the [`AGE`] and [`CACHE_CONTROL`] headers. Then +/// subtracts them to find the time until the age specified by `max-age` is reached. +pub fn time_until_max_age(headers: &http::header::HeaderMap) -> Option { + let max_age = headers.get(CACHE_CONTROL).and_then(parse_max_age)?; + let age = headers + .get(AGE) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let remaining_age = max_age.saturating_sub(age); + Some(Duration::from_secs(remaining_age)) +} + +#[cfg(test)] +mod tests { + use super::*; + use http::header::{HeaderMap, HeaderValue}; + + #[test] + fn test_time_until_max_age() { + fn hm(age: &str, max_age: &str) -> HeaderMap { + let mut m = HeaderMap::new(); + m.insert(AGE, HeaderValue::from_str(age).unwrap()); + m.insert( + CACHE_CONTROL, + HeaderValue::from_str(&format!("max-age={max_age}")).unwrap(), + ); + m + } + + let test_cases = [ + (hm("0", "10"), Some(10)), + (hm("10", "0"), Some(0)), + (hm("1.0", "10"), Some(10)), + (hm("0", "10.0"), None), + (HeaderMap::new(), None), + ]; + for (i, (input, output)) in test_cases.into_iter().enumerate() { + let output = output.map(Duration::from_secs); + assert_eq!(time_until_max_age(&input), output, "{i}th case failed"); + } + } + + #[test] + fn test_parse_max_age() { + fn hs(s: &str) -> HeaderValue { + HeaderValue::try_from(s).unwrap() + } + + fn hb(b: &[u8]) -> HeaderValue { + HeaderValue::from_bytes(b).unwrap() + } + + let test_cases = [ + (hs("max-age=420"), Some(420)), + (hs("max-age=420 "), Some(420)), + (hs(" max-age=420"), Some(420)), + (hs(" max-age=420 "), Some(420)), + (hs(", max-age=420"), Some(420)), + (hs(",max-age=420"), Some(420)), + (hs(",max-age=420,"), Some(420)), + (hs(",max-age=420, "), Some(420)), + (hs(",max-age=420, "), Some(420)), + (hs("foo,max-age=420,bar"), Some(420)), + (hs(",foo,max-age=420,bar"), Some(420)), + (hs(",foo,max-age=420,bar,"), Some(420)), + (hs(",foo,max-age=420,bar "), Some(420)), + (hs("foo, max-age=420"), Some(420)), + (hs("Max-Age=420"), None), + (hs("max_age=420"), None), + (hs("max-age=3.20"), None), + (hs("max-age=-3"), None), + (hs("max-age=-3"), None), + (hs("max-age=foo"), None), + (hb(b"\xFF, max-age=420"), None), + ]; + + for (i, (input, output)) in test_cases.into_iter().enumerate() { + assert_eq!(parse_max_age(&input), output, "{i}th test case failed"); + } + } +} diff --git a/crates/replicate/server/Cargo.toml b/crates/replicate/server/Cargo.toml index 846031e..1fd19c8 100644 --- a/crates/replicate/server/Cargo.toml +++ b/crates/replicate/server/Cargo.toml @@ -14,7 +14,7 @@ bytes = { workspace = true, features = ["serde"] } clap.workspace = true color-eyre.workspace = true dashmap = "5.5.3" -derive_more.workspace = true +derive_more = { workspace = true, features = ["deref", "deref_mut"] } eyre.workspace = true futures.workspace = true rand.workspace = true