diff options
| -rw-r--r-- | Cargo.lock | 336 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | lib/auth-service/Cargo.toml | 1 | ||||
| -rw-r--r-- | lib/auth-service/src/client/http.rs | 56 | ||||
| -rw-r--r-- | lib/auth-service/src/client/mod.rs | 29 | ||||
| -rw-r--r-- | lib/auth-service/src/lib.rs | 3 | ||||
| -rw-r--r-- | lib/auth-service/src/service/mod.rs | 71 | ||||
| -rw-r--r-- | migrations/20260210193544_profile.sql | 26 | ||||
| -rw-r--r-- | migrations/20260210194218_oauth_account.sql | 11 | ||||
| -rw-r--r-- | sellershut/Cargo.toml | 2 | ||||
| -rw-r--r-- | sellershut/sellershut.toml | 2 | ||||
| -rw-r--r-- | sellershut/src/server/entity/mod.rs | 1 | ||||
| -rw-r--r-- | sellershut/src/server/entity/user.rs | 6 | ||||
| -rw-r--r-- | sellershut/src/server/mod.rs | 4 | ||||
| -rw-r--r-- | sellershut/src/server/routes/auth/discord.rs | 47 | ||||
| -rw-r--r-- | sellershut/src/server/routes/auth/mod.rs | 141 | ||||
| -rw-r--r-- | sellershut/src/state/mod.rs | 4 |
17 files changed, 723 insertions, 18 deletions
@@ -187,6 +187,7 @@ dependencies = [ "async-session", "async-trait", "oauth2", + "reqwest 0.13.2", "secrecy", "serde_json", "shared-svc", @@ -204,6 +205,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] name = "axum" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -256,6 +279,28 @@ dependencies = [ ] [[package]] +name = "axum-extra" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] name = "backon" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -380,10 +425,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] [[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -456,6 +509,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -497,6 +559,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -665,6 +737,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -766,6 +844,12 @@ dependencies = [ ] [[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -901,6 +985,30 @@ dependencies = [ ] [[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1223,6 +1331,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.4", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] name = "js-sys" version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1451,7 +1591,7 @@ dependencies = [ "getrandom 0.2.17", "http", "rand 0.8.5", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_path_to_error", @@ -1479,6 +1619,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1632,6 +1778,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -1851,6 +1998,43 @@ dependencies = [ ] [[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1930,6 +2114,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -1939,6 +2124,18 @@ dependencies = [ ] [[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] name = "rustls-pki-types" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1949,11 +2146,39 @@ dependencies = [ ] [[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1981,6 +2206,15 @@ dependencies = [ ] [[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1997,6 +2231,29 @@ dependencies = [ ] [[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "sellershut" version = "0.1.0" dependencies = [ @@ -2004,9 +2261,11 @@ dependencies = [ "async-session", "auth-service", "axum", + "axum-extra", "clap", "dotenvy", "http-body-util", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -3135,6 +3394,15 @@ dependencies = [ ] [[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3232,6 +3500,15 @@ dependencies = [ [[package]] name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" @@ -3268,6 +3545,21 @@ dependencies = [ [[package]] name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" @@ -3316,6 +3608,12 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" @@ -3334,6 +3632,12 @@ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" @@ -3352,6 +3656,12 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" @@ -3382,6 +3692,12 @@ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" @@ -3400,6 +3716,12 @@ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" @@ -3418,6 +3740,12 @@ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" @@ -3436,6 +3764,12 @@ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" @@ -10,6 +10,7 @@ documentation = "https://books.kanjala.com/sellershut" [workspace.dependencies] async-session = "3.0.0" async-trait = "0.1.89" +reqwest = { version = "0.13.2", default-features = false } secrecy = "0.10.3" serde = "1.0.228" serde_json = "1.0.149" diff --git a/lib/auth-service/Cargo.toml b/lib/auth-service/Cargo.toml index c3b9be7..2cea550 100644 --- a/lib/auth-service/Cargo.toml +++ b/lib/auth-service/Cargo.toml @@ -10,6 +10,7 @@ documentation.workspace = true async-session.workspace = true async-trait.workspace = true oauth2 = "5.0.0" +reqwest = { workspace = true, features = ["rustls"] } secrecy = "0.10.3" serde_json = "1.0.149" shared-svc = { workspace = true, features = ["cache"] } diff --git a/lib/auth-service/src/client/http.rs b/lib/auth-service/src/client/http.rs new file mode 100644 index 0000000..5621fb0 --- /dev/null +++ b/lib/auth-service/src/client/http.rs @@ -0,0 +1,56 @@ +use std::ops::Deref; + +use oauth2::http; + +#[derive(Clone, Debug)] +pub struct HttpAuthClient(reqwest::Client); + +impl From<reqwest::Client> for HttpAuthClient { + fn from(value: reqwest::Client) -> Self { + Self(value) + } +} + +impl Deref for HttpAuthClient { + type Target = reqwest::Client; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'c> oauth2::AsyncHttpClient<'c> for HttpAuthClient { + type Error = oauth2::HttpClientError<reqwest::Error>; + + #[cfg(target_arch = "wasm32")] + type Future = Pin<Box<dyn Future<Output = Result<HttpResponse, Self::Error>> + 'c>>; + #[cfg(not(target_arch = "wasm32"))] + type Future = std::pin::Pin< + Box<dyn Future<Output = Result<oauth2::HttpResponse, Self::Error>> + Send + Sync + 'c>, + >; + + fn call(&'c self, request: oauth2::HttpRequest) -> Self::Future { + Box::pin(async move { + let response = self + .0 + .execute(request.try_into().map_err(Box::new)?) + .await + .map_err(Box::new)?; + + let mut builder = http::Response::builder().status(response.status()); + + #[cfg(not(target_arch = "wasm32"))] + { + builder = builder.version(response.version()); + } + + for (name, value) in response.headers().iter() { + builder = builder.header(name, value); + } + + builder + .body(response.bytes().await.map_err(Box::new)?.to_vec()) + .map_err(oauth2::HttpClientError::Http) + }) + } +} diff --git a/lib/auth-service/src/client/mod.rs b/lib/auth-service/src/client/mod.rs index 45260fb..e02672b 100644 --- a/lib/auth-service/src/client/mod.rs +++ b/lib/auth-service/src/client/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod http; +use std::ops::Deref; + use oauth2::{ AuthUrl, ClientId, ClientSecret, CsrfToken, EndpointNotSet, EndpointSet, RedirectUrl, Scope, TokenUrl, @@ -8,16 +11,24 @@ use url::Url; use crate::{AuthServiceError, Provider}; +type Inner = oauth2::basic::BasicClient< + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointSet, +>; + #[derive(Debug, Clone)] -pub struct OauthClient( - oauth2::basic::BasicClient< - EndpointSet, - EndpointNotSet, - EndpointNotSet, - EndpointNotSet, - EndpointSet, - >, -); +pub struct OauthClient(Inner); + +impl Deref for OauthClient { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} #[derive(Debug)] pub struct ClientConfig { diff --git a/lib/auth-service/src/lib.rs b/lib/auth-service/src/lib.rs index 0965f86..61ff230 100644 --- a/lib/auth-service/src/lib.rs +++ b/lib/auth-service/src/lib.rs @@ -1,9 +1,12 @@ +pub use oauth2; pub mod client; mod service; +pub use client::http::HttpAuthClient; pub use service::*; use thiserror::Error; +#[derive(Debug)] pub enum Provider { Discord, } diff --git a/lib/auth-service/src/service/mod.rs b/lib/auth-service/src/service/mod.rs index 5150221..80b29de 100644 --- a/lib/auth-service/src/service/mod.rs +++ b/lib/auth-service/src/service/mod.rs @@ -1,15 +1,71 @@ use async_session::{Result, Session, SessionStore}; use async_trait::async_trait; use shared_svc::cache::{CacheKey, RedisManager, redis::AsyncCommands}; -use sqlx::PgPool; +use sqlx::{PgPool, Postgres, Transaction}; use tracing::{debug, instrument}; +use crate::Provider; + +#[async_trait] +pub trait AccountMgr { + async fn get_apid_by_email(&self, email: &str) -> Result<Option<String>>; + async fn create_account(&self, provider: Provider, provider_user_id: &str, ap_id: &str); + async fn create_account_step( + &self, + provider: Provider, + provider_user_id: &str, + ap_id: &str, + email: &str, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result; + async fn persist_session(&self); +} + #[derive(Debug, Clone)] pub struct AuthService { cache: RedisManager, database: PgPool, } +#[async_trait] +impl AccountMgr for AuthService { + #[instrument(skip(self))] + async fn get_apid_by_email(&self, email: &str) -> Result<Option<String>> { + todo!() + } + + #[instrument(skip(self))] + async fn create_account(&self, provider: Provider, provider_user_id: &str, ap_id: &str) { + todo!() + } + #[instrument(skip(self, transaction))] + async fn create_account_step( + &self, + provider: Provider, + provider_user_id: &str, + ap_id: &str, + email: &str, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { + sqlx::query!( + "insert into account + (provider_id, provider_user_id, email, ap_id) + values ($1, $2, $3, $4) + ", + "", + provider_user_id, + "", + ap_id + ) + .execute(&mut **transaction) + .await?; + todo!() + } + + #[instrument(skip(self))] + async fn persist_session(&self) {} +} + impl AuthService { pub fn new(cache: &RedisManager, database: &PgPool) -> Self { Self { @@ -26,9 +82,18 @@ impl SessionStore for AuthService { #[doc = " The input is expected to be the value of an identifying"] #[doc = " cookie. This will then be parsed by the session middleware"] #[doc = " into a session if possible"] - #[instrument(skip(self))] + #[instrument(skip(self, cookie_value))] async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> { - todo!() + debug!("getting session"); + let id = Session::id_from_cookie_value(&cookie_value)?; + let mut client = self.cache.get().await?; + let session = client + .get::<_, Option<Vec<u8>>>(CacheKey::Session(&id)) + .await?; + match session { + Some(value) => Ok(Some(serde_json::from_slice(&value)?)), + None => Ok(None), + } } #[doc = " Store a session on the storage backend."] diff --git a/migrations/20260210193544_profile.sql b/migrations/20260210193544_profile.sql new file mode 100644 index 0000000..f47d034 --- /dev/null +++ b/migrations/20260210193544_profile.sql @@ -0,0 +1,26 @@ +create type actor_kind as enum ( + 'application', + 'group', + 'organization', + 'person', + 'service' +); + +create table profile ( + ap_id text primary key, + username varchar(15), + description varchar(255), + inbox text not null, + role actor_kind not null default 'person', + outbox text, + picture text, + public_key text not null, + private_key text, + created_at timestamptz not null default now(), + last_refreshed_at timestamptz not null default now() +); + +create index user_inbox_idx on profile (inbox); +create index user_outbox_idx on profile (outbox); +create index user_role_idx on profile (role); +create index user_username_idx on profile (username); diff --git a/migrations/20260210194218_oauth_account.sql b/migrations/20260210194218_oauth_account.sql new file mode 100644 index 0000000..ce660fe --- /dev/null +++ b/migrations/20260210194218_oauth_account.sql @@ -0,0 +1,11 @@ +create extension if not exists citext; + +create table account ( + provider_id text not null, + provider_user_id text not null, + email citext not null, + ap_id text not null references profile(ap_id) on delete cascade, + primary key (provider_id, provider_user_id) +); + +create index account_email_idx on account (email); diff --git a/sellershut/Cargo.toml b/sellershut/Cargo.toml index 74dfd5d..2a4de31 100644 --- a/sellershut/Cargo.toml +++ b/sellershut/Cargo.toml @@ -11,8 +11,10 @@ anyhow = "1.0.101" async-session.workspace = true auth-service = { path = "../lib/auth-service" } axum = "0.8.8" +axum-extra = { version = "0.12.5", features = ["typed-header"] } clap = { version = "4.5.57", features = ["derive", "env"] } dotenvy = "0.15.7" +reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls"] } secrecy = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/sellershut/sellershut.toml b/sellershut/sellershut.toml index ce2f4ff..01b9481 100644 --- a/sellershut/sellershut.toml +++ b/sellershut/sellershut.toml @@ -11,7 +11,7 @@ pool-size = 10 url = "redis://localhost:6379" [oauth] -redirect-url = "http://localhost:2210" +redirect-url = "http://localhost:2210/auth/authorised" [oauth.discord] client-id = "" diff --git a/sellershut/src/server/entity/mod.rs b/sellershut/src/server/entity/mod.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/sellershut/src/server/entity/mod.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/sellershut/src/server/entity/user.rs b/sellershut/src/server/entity/user.rs new file mode 100644 index 0000000..91da03a --- /dev/null +++ b/sellershut/src/server/entity/user.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct User { + pub ap_id: String, +} diff --git a/sellershut/src/server/mod.rs b/sellershut/src/server/mod.rs index cc96c84..9818ce5 100644 --- a/sellershut/src/server/mod.rs +++ b/sellershut/src/server/mod.rs @@ -1,3 +1,4 @@ +mod entity; pub mod error; mod middleware; mod routes; @@ -35,7 +36,8 @@ pub async fn router(state: Arc<AppState>) -> Router<()> { let stubs = OpenApiRouter::with_openapi(doc) .routes(utoipa_axum::routes!(routes::health)) - .routes(utoipa_axum::routes!(routes::auth::auth)); + .routes(utoipa_axum::routes!(routes::auth::auth)) + .routes(utoipa_axum::routes!(routes::auth::authorised)); let (router, _api) = stubs.split_for_parts(); diff --git a/sellershut/src/server/routes/auth/discord.rs b/sellershut/src/server/routes/auth/discord.rs new file mode 100644 index 0000000..e6c086a --- /dev/null +++ b/sellershut/src/server/routes/auth/discord.rs @@ -0,0 +1,47 @@ +use anyhow::Context as _; +use auth_service::HttpAuthClient; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use crate::server::error::AppError; + +// https://discord.com/developers/docs/resources/user#user-object-user-structure +#[derive(Debug, Serialize, Deserialize)] +struct User { + id: String, + username: String, + email: String, + verified: bool, +} + +impl From<User> for super::OauthUser { + fn from(value: User) -> Self { + Self { + id: value.id, + username: value.username, + email: value.email, + } + } +} + +#[instrument(skip(client, token))] +pub(super) async fn get_data( + client: &HttpAuthClient, + token: &str, +) -> Result<super::OauthUser, AppError> { + let user_data: User = client + .get("https://discordapp.com/api/users/@me") + .bearer_auth(token) + .send() + .await + .context("failed in sending request to target Url")? + .json::<User>() + .await + .context("failed to deserialize response as JSON")?; + + if !user_data.verified { + return Err(anyhow::anyhow!("user must be verified").into()); + } + + Ok(user_data.into()) +} diff --git a/sellershut/src/server/routes/auth/mod.rs b/sellershut/src/server/routes/auth/mod.rs index bfc045f..04b6a2c 100644 --- a/sellershut/src/server/routes/auth/mod.rs +++ b/sellershut/src/server/routes/auth/mod.rs @@ -1,13 +1,22 @@ +mod discord; use std::sync::Arc; use anyhow::Context as _; -use async_session::{Session, SessionStore}; -use auth_service::Provider; +use async_session::{Session, SessionStore, log::debug}; +use auth_service::{ + AccountMgr, HttpAuthClient, Provider, + client::OauthClient, + oauth2::{AuthorizationCode, CsrfToken, TokenResponse}, +}; use axum::{ extract::{Query, State}, http::{HeaderMap, header::SET_COOKIE}, response::{IntoResponse, Redirect}, }; +use axum_extra::{ + TypedHeader, + headers::{self}, +}; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, OpenApi, ToSchema}; @@ -91,3 +100,131 @@ pub async fn auth( Ok((headers, Redirect::to(auth_url.as_ref()))) } + +#[derive(Debug, Deserialize, IntoParams)] +#[into_params(parameter_in = Query)] +pub struct AuthRequest { + code: String, + state: String, +} + +#[utoipa::path( + method(get), + path = "/auth/authorised", + params( + AuthRequest + ), + tag=AUTH, + responses( + ( + status = 200, + description = "OAuth authorised callback", + headers( + ( + "set-cookie" = String, + description = "Session cookie" + ) + ) + ) + ) +)] +pub async fn authorised( + TypedHeader(cookies): TypedHeader<headers::Cookie>, + Query(params): Query<AuthRequest>, + State(data): State<Arc<AppState>>, +) -> Result<impl IntoResponse, AppError> { + let provider = csrf_token_validation_workflow(¶ms, &cookies, &data).await?; + + // Get an auth token + + let user = match provider { + OauthProvider::Discord => { + let token = get_token(&data.discord_client, &data.http_client, ¶ms.code).await?; + discord::get_data(&data.http_client, token.access_token().secret()).await? + } + }; + + if let Some(ap_id) = data.auth_service.get_apid_by_email(&user.email).await? { + debug!("user exists"); + data.auth_service + .create_account(provider.into(), &user.id, "") + .await; + } else { + debug!("user does not exist, creating"); + // create account and user in a transaction + let mut transaction = data.database.begin().await?; + + data.auth_service + .create_account_step(provider.into(), &user.id, "", &user.email, &mut transaction) + .await?; + + transaction.commit().await?; + } + + Ok(String::default()) +} + +async fn csrf_token_validation_workflow( + auth_request: &AuthRequest, + cookies: &headers::Cookie, + data: &Arc<AppState>, +) -> Result<OauthProvider, AppError> { + let cookie = cookies + .get(COOKIE_NAME) + .context("unexpected error getting cookie name")? + .to_string(); + + let session = data + .auth_service + .load_session(cookie) + .await? + .context("Session not found")?; + + let stored_csrf_token = session + .get::<CsrfToken>(CSRF_TOKEN) + .context("CSRF token not found in session")? + .to_owned(); + + let provider = session + .get(OAUTH_PROVIDER) + .context("provider not found in session")?; + + data.auth_service + .destroy_session(session) + .await + .context("Failed to destroy old session")?; + + // Validate CSRF token is the same as the one in the auth request + if *stored_csrf_token.secret() != auth_request.state { + return Err(anyhow::anyhow!("CSRF token mismatch").into()); + } + + Ok(provider) +} + +async fn get_token( + oauth_client: &OauthClient, + client: &HttpAuthClient, + code: &str, +) -> Result< + auth_service::oauth2::StandardTokenResponse< + auth_service::oauth2::EmptyExtraTokenFields, + auth_service::oauth2::basic::BasicTokenType, + >, + AppError, +> { + // Get an auth token + let token = oauth_client + .exchange_code(AuthorizationCode::new(code.to_owned())) + .request_async(client) + .await + .context("failed in sending request request to authorization server")?; + Ok(token) +} + +#[derive(Debug, Deserialize)] +struct OauthUser { + pub id: String, + pub username: String, + pub email: String, +} diff --git a/sellershut/src/state/mod.rs b/sellershut/src/state/mod.rs index be20e13..ab0262e 100644 --- a/sellershut/src/state/mod.rs +++ b/sellershut/src/state/mod.rs @@ -1,4 +1,4 @@ -use auth_service::{AuthService, client::OauthClient}; +use auth_service::{AuthService, HttpAuthClient, client::OauthClient}; use shared_svc::cache::RedisManager; use sqlx::PgPool; use tracing::{debug, error}; @@ -11,6 +11,7 @@ pub struct AppState { pub cache: RedisManager, pub auth_service: AuthService, pub database: PgPool, + pub http_client: HttpAuthClient, } impl AppState { @@ -43,6 +44,7 @@ impl AppState { cache, auth_service, database, + http_client: reqwest::Client::new().into(), }) } } |
