From 3081fb4af8227f4516245f946118137073c7aaa8 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Thu, 27 Mar 2025 18:39:29 +0000 Subject: [PATCH] feat: add oauth flow base Signed-off-by: Nikolaos Karaolidis --- Cargo.lock | 412 +++++++++++++++++------------ Cargo.toml | 8 +- Containerfile | 2 + manifest.yaml | 8 +- migrations/20250327112746_init.sql | 0 src/config.rs | 36 +-- src/main.rs | 44 +-- src/models/mod.rs | 0 src/routes/auth.rs | 317 ++++++++++++++++++++++ src/routes/health.rs | 12 +- src/routes/mod.rs | 10 +- src/state.rs | 139 ++++++++++ 12 files changed, 763 insertions(+), 225 deletions(-) create mode 100644 migrations/20250327112746_init.sql create mode 100644 src/models/mod.rs create mode 100644 src/routes/auth.rs create mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 16130ba..4651482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,48 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-session" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630" +dependencies = [ + "anyhow", + "async-lock", + "async-trait", + "base64 0.13.1", + "bincode", + "blake3", + "chrono", + "hmac 0.11.0", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.9.9", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -180,6 +222,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -187,7 +251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", @@ -201,6 +265,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -219,6 +289,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -228,6 +307,30 @@ dependencies = [ "serde", ] +[[package]] +name = "blake3" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 0.1.10", + "constant_time_eq", + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -264,6 +367,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -352,6 +461,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -419,16 +534,36 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -519,13 +654,22 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -561,7 +705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -587,7 +731,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.8", "subtle", "zeroize", ] @@ -609,7 +753,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -644,11 +788,17 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "home", "windows-sys 0.48.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.4.0" @@ -660,12 +810,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fastrand" version = "2.3.0" @@ -764,17 +908,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -795,7 +928,6 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -821,7 +953,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -834,7 +966,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "r-efi", @@ -885,6 +1017,30 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.5.0" @@ -903,7 +1059,17 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.0", + "digest 0.9.0", ] [[package]] @@ -912,7 +1078,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1369,8 +1535,8 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if", - "digest", + "cfg-if 1.0.0", + "digest 0.10.7", ] [[package]] @@ -1473,7 +1639,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", "url", ] @@ -1493,6 +1659,12 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openidconnect" version = "4.0.0" @@ -1503,7 +1675,7 @@ dependencies = [ "chrono", "dyn-clone", "ed25519-dalek", - "hmac", + "hmac 0.12.1", "http", "itertools", "log", @@ -1518,7 +1690,7 @@ dependencies = [ "serde_path_to_error", "serde_plain", "serde_with", - "sha2", + "sha2 0.10.8", "subtle", "thiserror 1.0.69", "url", @@ -1542,7 +1714,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -1554,7 +1726,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -1579,7 +1751,7 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", @@ -1601,24 +1773,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1658,49 +1812,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "postgres" -version = "0.19.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e6dfbdd780d3aa3597b6eb430db76bb315fa9bad7fae595bb8def808b8470" -dependencies = [ - "bytes", - "fallible-iterator", - "futures-util", - "log", - "tokio", - "tokio-postgres", -] - -[[package]] -name = "postgres-protocol" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.9.0", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" -dependencies = [ - "bytes", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1921,7 +2032,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -1932,7 +2043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if", + "cfg-if 1.0.0", "getrandom 0.2.15", "libc", "untrusted", @@ -1946,7 +2057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", - "digest", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2196,9 +2307,22 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] [[package]] @@ -2207,9 +2331,9 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2224,16 +2348,10 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -2304,7 +2422,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", @@ -2318,9 +2436,10 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "smallvec", "thiserror 2.0.12", + "time", "tokio", "tokio-stream", "tracing", @@ -2355,7 +2474,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -2378,7 +2497,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -2388,7 +2507,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5", @@ -2399,11 +2518,12 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.8", "smallvec", "sqlx-core", "stringprep", "thiserror 2.0.12", + "time", "tracing", "whoami", ] @@ -2426,7 +2546,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", @@ -2436,11 +2556,12 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "smallvec", "sqlx-core", "stringprep", "thiserror 2.0.12", + "time", "tracing", "whoami", ] @@ -2464,6 +2585,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "time", "tracing", "url", ] @@ -2685,32 +2807,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "tokio-postgres" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.9.0", - "socket2", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-rustls" version = "0.26.2" @@ -2732,19 +2828,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "tower" version = "0.5.2" @@ -2914,14 +2997,14 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" name = "veil" version = "0.1.0" dependencies = [ + "async-session", "axum", + "axum-extra", "clap", "log", "log4rs", "openidconnect", - "postgres", "serde", - "serde_derive", "serde_yaml", "sqlx", "tokio", @@ -2969,7 +3052,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -2995,7 +3078,7 @@ version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "once_cell", "wasm-bindgen", @@ -3071,7 +3154,6 @@ checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", - "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7d94328..7cfc018 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,14 +14,14 @@ lto = true codegen-units = 1 [dependencies] +async-session = "3.0.0" axum = "0.8.1" +axum-extra = { version = "0.10.0", features = ["typed-header"] } clap = { version = "4.5.32", features = ["derive"] } log = "0.4.27" log4rs = "1.3.0" -openidconnect = "4.0.0" -postgres = "0.19.10" +openidconnect = { version = "4.0.0", features = ["reqwest"] } serde = "1.0.219" -serde_derive = "1.0.219" serde_yaml = "0.9.34" -sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio"] } +sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio", "time"] } tokio = { version = "1.44.1", features = ["rt-multi-thread"] } diff --git a/Containerfile b/Containerfile index f33c2a2..462d317 100644 --- a/Containerfile +++ b/Containerfile @@ -11,6 +11,8 @@ RUN cargo fetch RUN rm -rf src COPY src ./src +COPY migrations ./migrations +COPY .sqlx ./.sqlx RUN cargo build --target=x86_64-unknown-linux-musl --release diff --git a/manifest.yaml b/manifest.yaml index 4d2ce52..73db240 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -70,6 +70,9 @@ metadata: name: veil-config data: default.yml: | + server: + host: https://app.veil.local + database: host: postgresql port: 5432 @@ -77,10 +80,11 @@ data: password: veil database: veil - oidc: + oauth: issuer_url: "https://id.veil.local" client_id: "veil" client_secret: "insecure_secret" + insecure: true log4rs.yml: | appenders: stdout: @@ -167,7 +171,7 @@ data: - client_id: "veil" client_secret: "$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng" # The digest of 'insecure_secret'. redirect_uris: - - "https://app.veil.local/oauth2/callback" + - "https://app.veil.local/api/auth/callback" authorization_policy: "one_factor" users.yml: | users: diff --git a/migrations/20250327112746_init.sql b/migrations/20250327112746_init.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/config.rs b/src/config.rs index 246b621..fdeb02c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use clap::Parser; use serde::Deserialize; use std::{ fs, @@ -5,8 +6,9 @@ use std::{ path::PathBuf, }; -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct ServerConfig { + pub host: String, #[serde(default = "default_address")] pub address: std::net::IpAddr, #[serde(default = "default_port")] @@ -23,17 +25,7 @@ const fn default_port() -> u16 { 51821 } -impl Default for ServerConfig { - fn default() -> Self { - Self { - address: default_address(), - port: default_port(), - subpath: String::default(), - } - } -} - -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct DatabaseConfig { pub user: String, pub password: String, @@ -43,18 +35,19 @@ pub struct DatabaseConfig { } #[derive(Debug, Deserialize, Clone)] -pub struct OidcConfig { +pub struct OAuthConfig { pub issuer_url: String, pub client_id: String, pub client_secret: String, + #[serde(default)] + pub insecure: bool, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct Config { - #[serde(default)] pub server: ServerConfig, pub database: DatabaseConfig, - pub oidc: OidcConfig, + pub oauth: OAuthConfig, } impl Config { @@ -64,3 +57,14 @@ impl Config { Ok(config) } } + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None, author)] +pub struct Args { + /// Path to the YAML config file + #[arg(short, long, value_name = "FILE", default_value = "config.yaml")] + pub config: PathBuf, + /// Path to the log4rs config file + #[arg(short, long, value_name = "FILE", default_value = "log4rs.yaml")] + pub log_config: PathBuf, +} diff --git a/src/main.rs b/src/main.rs index 91298d4..0e26843 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,56 +2,38 @@ #![allow(clippy::missing_docs_in_private_items)] mod config; +mod models; mod routes; +mod state; -use axum::{Extension, serve}; +use axum::serve; use clap::Parser; use log::info; use log4rs::config::Deserializers; -use sqlx::postgres::PgPoolOptions; -use std::{net::SocketAddr, path::PathBuf}; +use std::net::SocketAddr; use tokio::net::TcpListener; -use config::Config; - -#[derive(Parser, Debug)] -#[command(version, about, long_about = None, author)] -struct Args { - /// Path to the YAML config file - #[arg(short, long, value_name = "FILE", default_value = "config.yaml")] - config: PathBuf, - /// Path to the log4rs config file - #[arg(short, long, value_name = "FILE", default_value = "log4rs.yaml")] - log_config: PathBuf, -} +use config::{Args, Config}; +use state::State; #[tokio::main] async fn main() { let args = Args::parse(); log4rs::init_file(args.log_config, Deserializers::default()).unwrap(); let config = Config::from_yaml(&args.config).unwrap(); + let state = State::from_config(config.clone()).await.unwrap(); - let pgpool = PgPoolOptions::new() - .max_connections(5) - .connect(&format!( - "postgres://{}:{}@{}:{}/{}", - config.database.user, - config.database.password, - config.database.host, - config.database.port, - config.database.database - )) + sqlx::migrate!("./migrations") + .run(&state.pg_pool) .await - .unwrap(); + .expect("Failed to run migrations"); - let routes = routes::routes(); - let app = axum::Router::new() - .nest(&format!("{}/api", config.server.subpath), routes) - .layer(Extension(pgpool)); + let routes = routes::routes(state); + let app = axum::Router::new().nest(&format!("{}/api", config.server.subpath), routes); let addr = SocketAddr::from((config.server.address, config.server.port)); let listener = TcpListener::bind(addr).await.unwrap(); - info!("Listening on {addr}."); + info!("Listening on {}", listener.local_addr().unwrap()); serve(listener, app).await.unwrap(); } diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..01147b7 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,317 @@ +use std::{borrow::Cow, convert::Infallible}; + +use async_session::{MemoryStore, Session, SessionStore}; +use axum::{ + RequestPartsExt, Router, + extract::{self, FromRef, FromRequestParts, OptionalFromRequestParts}, + http::{HeaderMap, StatusCode, header, request::Parts}, + response::{IntoResponse, Redirect, Response}, + routing, +}; +use axum_extra::{TypedHeader, headers::Cookie, typed_header::TypedHeaderRejectionReason}; +use log::error; +use openidconnect::{ + AccessTokenHash, AuthorizationCode, CsrfToken, EndUserEmail, EndUserUsername, Nonce, + OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, Scope, SubjectIdentifier, + TokenResponse, core::CoreAuthenticationFlow, reqwest, +}; +use serde::{Deserialize, Serialize}; + +use crate::state::{OAuthClient, State}; + +static COOKIE_NAME: &str = "veil_session"; + +#[derive(Clone, Deserialize, Serialize)] +pub struct User { + pub subject: SubjectIdentifier, + pub username: EndUserUsername, + pub email: Option, +} + +async fn login( + extract::State(oauth_client): extract::State, + extract::State(session_store): extract::State, +) -> Result { + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + let (auth_url, csrf_token, nonce) = oauth_client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .set_pkce_challenge(pkce_challenge) + .set_redirect_uri(Cow::Borrowed(oauth_client.redirect_uri().ok_or_else( + || { + error!("missing redirect URI"); + StatusCode::INTERNAL_SERVER_ERROR + }, + )?)) + .add_scope(Scope::new("profile".to_string())) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("groups".to_string())) + .url(); + + let mut session = Session::new(); + session + .insert("pkce_verifier", pkce_verifier) + .map_err(|e| { + error!("failed to insert pkce_verifier into session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + session.insert("csrf_token", csrf_token).map_err(|e| { + error!("failed to insert csrf_token into session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + session.insert("nonce", nonce).map_err(|e| { + error!("failed to insert nonce into session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let cookie = session_store + .store_session(session) + .await + .map_err(|e| { + error!("failed to store session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .ok_or_else(|| { + error!("failed to retrieve stored session cookie"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let cookie = + format!("{COOKIE_NAME}={cookie}; HttpOnly; SameSite=Lax; HttpOnly; Secure; Path=/"); + + let mut headers = HeaderMap::new(); + headers.insert( + header::SET_COOKIE, + cookie.parse().map_err(|e| { + error!("failed to parse cookie: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?, + ); + + Ok((headers, Redirect::to(auth_url.as_str()))) +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +struct CallbackParams { + code: String, + state: String, +} + +async fn callback( + extract::Query(params): extract::Query, + extract::State(http_client): extract::State, + extract::State(oauth_client): extract::State, + extract::State(session_store): extract::State, + TypedHeader(cookies): TypedHeader, +) -> Result { + let cookie = cookies + .get(COOKIE_NAME) + .ok_or(StatusCode::UNAUTHORIZED)? + .to_string(); + + let session = session_store + .load_session(cookie) + .await + .map_err(|e| { + error!("failed to load session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .ok_or(StatusCode::UNAUTHORIZED)?; + + let csrf_token = session + .get::("csrf_token") + .ok_or_else(|| { + error!("failed to get csrf_token from session"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .clone(); + + let pkce_verifier = session + .get::("pkce_verifier") + .ok_or_else(|| { + error!("failed to get pkce_verifier from session"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let nonce = session + .get::("nonce") + .ok_or_else(|| { + error!("failed to get nonce from session"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .clone(); + + session_store.destroy_session(session).await.map_err(|e| { + error!("failed to destroy session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if *csrf_token.secret() != params.state { + error!("csrf_token mismatch"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + + let token_response = oauth_client + .exchange_code(AuthorizationCode::new(params.code)) + .map_err(|e| { + error!("failed to exchange code: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .set_pkce_verifier(pkce_verifier) + .request_async(&http_client) + .await + .map_err(|e| { + error!("failed to request token: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let id_token = token_response.id_token().ok_or_else(|| { + error!("missing id_token in token response"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let id_token_verifier = oauth_client.id_token_verifier(); + let claims = id_token.claims(&id_token_verifier, &nonce).map_err(|e| { + error!("failed to verify id_token: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if let Some(expected_access_token_hash) = claims.access_token_hash() { + let actual_access_token_hash = AccessTokenHash::from_token( + token_response.access_token(), + id_token.signing_alg().map_err(|e| { + error!("failed to get signing algorithm: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?, + id_token.signing_key(&id_token_verifier).map_err(|e| { + error!("failed to get signing key: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?, + ) + .map_err(|e| { + error!("failed to compute access token hash: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if actual_access_token_hash != *expected_access_token_hash { + error!("access token hash mismatch"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + } + + let user = User { + subject: claims.subject().to_owned(), + username: claims.preferred_username().cloned().ok_or_else(|| { + error!("missing preferred_username in claims"); + StatusCode::INTERNAL_SERVER_ERROR + })?, + email: claims.email().cloned(), + }; + + let mut session = Session::new(); + session.insert("user", user).map_err(|e| { + error!("failed to insert user into session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(StatusCode::OK) +} + +async fn logout( + extract::State(session_store): extract::State, + TypedHeader(cookies): TypedHeader, +) -> Result { + let cookie = cookies.get(COOKIE_NAME).ok_or(StatusCode::UNAUTHORIZED)?; + + let Some(session) = session_store + .load_session(cookie.to_string()) + .await + .map_err(|e| { + error!("failed to load session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + else { + return Ok(StatusCode::OK); + }; + + session_store.destroy_session(session).await.map_err(|e| { + error!("failed to destroy session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(StatusCode::OK) +} + +pub fn routes(state: State) -> Router { + Router::new() + .route("/auth/login", routing::get(login)) + .route("/auth/callback", routing::get(callback)) + .route("/auth/logout", routing::get(logout)) + .with_state(state) +} + +impl FromRequestParts for User +where + MemoryStore: FromRef, + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let store = MemoryStore::from_ref(state); + + let cookies = parts.extract::>().await.map_err(|e| { + if *e.name() == header::COOKIE { + if matches!(e.reason(), TypedHeaderRejectionReason::Missing) { + StatusCode::UNAUTHORIZED.into_response() + } else { + error!("failed to extract cookies: {e}"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } else { + error!("failed to extract cookies: {e}"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + })?; + + let session_cookie = cookies + .get(COOKIE_NAME) + .ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?; + + let session = store + .load_session(session_cookie.to_string()) + .await + .map_err(|e| { + error!("failed to load session: {e}"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + })? + .ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?; + + let user = session + .get::("user") + .ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?; + + Ok(user) + } +} + +impl OptionalFromRequestParts for User +where + MemoryStore: FromRef, + S: Send + Sync, +{ + type Rejection = Infallible; + + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result, Self::Rejection> { + (>::from_request_parts(parts, state).await) + .map_or(Ok(None), |user| Ok(Some(user))) + } +} diff --git a/src/routes/health.rs b/src/routes/health.rs index 27ef21c..b70c568 100644 --- a/src/routes/health.rs +++ b/src/routes/health.rs @@ -1,9 +1,13 @@ -use axum::{Router, http::StatusCode, routing}; +use axum::{Router, http::StatusCode, response::IntoResponse, routing}; -pub async fn get() -> Result { +use crate::state::State; + +pub async fn get() -> Result { Ok(StatusCode::OK) } -pub fn routes() -> Router { - Router::new().route("/", routing::get(get)) +pub fn routes(state: State) -> Router { + Router::new() + .route("/health", routing::get(get)) + .with_state(state) } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index da5440f..e77ce07 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,9 +1,13 @@ +mod auth; mod health; use axum::Router; -pub fn routes() -> Router { - let health = health::routes(); +use crate::state::State; - Router::new().merge(health) +pub fn routes(state: State) -> Router { + let auth = auth::routes(state.clone()); + let health = health::routes(state); + + Router::new().merge(auth).merge(health) } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..fc414c7 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,139 @@ +use async_session::MemoryStore; +use axum::extract::FromRef; +use log::error; +use openidconnect::{ + ClientId, ClientSecret, EmptyAdditionalClaims, EndpointMaybeSet, EndpointNotSet, EndpointSet, + IssuerUrl, RedirectUrl, StandardErrorResponse, + core::{ + CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey, + CoreJweContentEncryptionAlgorithm, CoreProviderMetadata, CoreRevocableToken, + CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenResponse, + }, + reqwest, +}; +use tokio::{ + spawn, + time::{Duration, sleep}, +}; + +use crate::config::Config; + +pub type OAuthClient< + HasAuthUrl = EndpointSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointMaybeSet, + HasUserInfoUrl = EndpointMaybeSet, +> = openidconnect::Client< + EmptyAdditionalClaims, + CoreAuthDisplay, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJsonWebKey, + CoreAuthPrompt, + StandardErrorResponse, + CoreTokenResponse, + CoreTokenIntrospectionResponse, + CoreRevocableToken, + CoreRevocationErrorResponse, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + HasUserInfoUrl, +>; + +#[derive(Clone)] +pub struct State { + pub pg_pool: sqlx::PgPool, + pub oauth_http_client: reqwest::Client, + pub oauth_client: OAuthClient, + pub session_store: async_session::MemoryStore, +} + +impl State { + pub async fn from_config(config: Config) -> Result> { + let pg_pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(5) + .connect(&format!( + "postgres://{}:{}@{}:{}/{}", + config.database.user, + config.database.password, + config.database.host, + config.database.port, + config.database.database + )) + .await?; + + let mut http_client = + reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()); + + if config.oauth.insecure { + http_client = http_client.danger_accept_invalid_certs(true); + } + + let http_client = http_client.build()?; + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(config.oauth.issuer_url)?, + &http_client, + ) + .await?; + + let oauth_client = openidconnect::core::CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(config.oauth.client_id), + Some(ClientSecret::new(config.oauth.client_secret)), + ) + .set_redirect_uri(RedirectUrl::new(format!( + "{}{}/api/auth/callback", + config.server.host, config.server.subpath + ))?); + + let session_store = MemoryStore::new(); + + let session_store_clone = session_store.clone(); + spawn(async move { + loop { + match session_store_clone.cleanup().await { + Ok(()) => {} + Err(e) => error!("Failed to clean up session store: {e}"), + } + sleep(Duration::from_secs(60)).await; + } + }); + + Ok(Self { + pg_pool, + oauth_http_client: http_client, + oauth_client, + session_store, + }) + } +} + +impl FromRef for sqlx::PgPool { + fn from_ref(state: &State) -> Self { + state.pg_pool.clone() + } +} + +impl FromRef for reqwest::Client { + fn from_ref(state: &State) -> Self { + state.oauth_http_client.clone() + } +} + +impl FromRef for OAuthClient { + fn from_ref(state: &State) -> Self { + state.oauth_client.clone() + } +} + +impl FromRef for async_session::MemoryStore { + fn from_ref(state: &State) -> Self { + state.session_store.clone() + } +}