From ec5b96527aec971b89123133ba2a6fbd44ceb198 Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Tue, 24 Sep 2024 04:55:07 -0400 Subject: [PATCH] identity: implemented google id flow (#127) --- Cargo.lock | 692 ++++++++++++++++++ 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 ++++ crates/header-parsing/Cargo.toml | 13 + crates/header-parsing/src/lib.rs | 98 +++ 9 files changed, 1117 insertions(+), 16 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 914036f..f0820b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-trait" version = "0.1.83" @@ -113,6 +119,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -174,6 +186,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower 0.5.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -234,6 +280,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -349,6 +401,33 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.14" @@ -436,6 +515,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[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", + "unicode-xid", +] + [[package]] name = "did-chain" version = "0.0.0" @@ -511,6 +620,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -588,6 +706,21 @@ 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", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -686,8 +819,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -696,6 +831,25 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[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" @@ -715,6 +869,13 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "header-parsing" +version = "0.0.0" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.5.0" @@ -800,6 +961,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" + [[package]] name = "httparse" version = "1.9.4" @@ -821,6 +988,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -829,6 +997,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", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[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]] @@ -838,28 +1041,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] name = "identity_server" version = "0.0.0" dependencies = [ + "arc-swap", "axum", + "axum-extra", + "axum-macros", "base64 0.21.7", "clap", "color-eyre", + "derive_more", "did-simple", + "header-parsing", "hex-literal", "http-body-util", "jose-jwk", + "jsonwebtoken", "rand", + "reqwest", "serde", "serde_json", "sqlx", @@ -898,6 +1111,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -952,6 +1171,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +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", + "serde", + "serde_json", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1043,6 +1284,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1070,6 +1321,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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 = "nom" version = "7.1.3" @@ -1107,6 +1375,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1152,6 +1426,50 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "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", +] + +[[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 = "overload" version = "0.1.1" @@ -1273,6 +1591,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1291,6 +1615,54 @@ dependencies = [ "unicode-ident", ] +[[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", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[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", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -1383,6 +1755,54 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[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", + "rustls", + "rustls-pemfile", + "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", + "windows-registry", +] + [[package]] name = "ring" version = "0.17.8" @@ -1424,6 +1844,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[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.4.1" @@ -1499,12 +1925,44 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.23" @@ -1924,6 +2382,30 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "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 = "tempfile" @@ -1968,6 +2450,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2012,6 +2525,27 @@ dependencies = [ "syn", ] +[[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", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.16" @@ -2023,6 +2557,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.4.13" @@ -2062,10 +2609,18 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags", "bytes", + "futures-util", "http", "http-body", "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2155,12 +2710,27 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2188,6 +2758,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -2245,6 +2821,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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.11.0+wasi-snapshot-preview1" @@ -2257,6 +2842,83 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.6" @@ -2298,6 +2960,36 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "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", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 5ff5fc4..f38cf5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,19 +15,24 @@ edition = "2021" rust-version = "1.78.0" [workspace.dependencies] +arc-swap = "1.7.1" axum = "0.7.5" +axum-extra = "0.9.3" +axum-macros = "0.4.1" base64 = "0.21.7" clap = { version = "4.4.11", features = ["derive"] } color-eyre = "0.6" did-simple.path = "crates/did-simple" eyre = "0.6" +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 } rand = "0.8.5" +reqwest = { version = "0.12.7", features = ["rustls-tls"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.114" -slotmap = "1.0.7" thiserror = "1.0.64" tokio = { version = "1.35.1", default-features = false } tower = "0.4.13" @@ -37,15 +42,8 @@ tracing-subscriber = "0.3.18" uuid = "1.7.0" [workspace.dependencies.derive_more] -version = "0.99" +version = "1.0.0" default-features = false -features = [ - "add", - "deref", - "deref_mut", - "mul", - "from", -] [profile.dev] # Enable a small amount of optimization in debug mode diff --git a/apps/identity_server/Cargo.toml b/apps/identity_server/Cargo.toml index cbccad2..50ddfef 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/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"); + } + } +}