diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index efc1729..fcfaab8 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -42,6 +42,7 @@ jobs: - name: Build and test auth-service code working-directory: ./auth-service run: | + export JWT_SECRET=secret cargo build --verbose cargo test --verbose @@ -94,6 +95,7 @@ jobs: password: ${{ secrets.DROPLET_PASSWORD }} script: | cd ~ + export JWT_SECRET=${{ secrets.JWT_SECRET }} export AUTH_SERVICE_IP=${{ vars.DROPLET_IP }} docker-compose down docker-compose pull diff --git a/README.md b/README.md index 6d390a2..c82cebf 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ visit http://localhost:3000 ## Run servers locally (Docker) ```bash -docker compose build -docker compose up +./docker.sh ``` visit http://localhost:8000 and http://localhost:3000 \ No newline at end of file diff --git a/auth-service/Cargo.lock b/auth-service/Cargo.lock index 904e9e2..b22dc01 100644 --- a/auth-service/Cargo.lock +++ b/auth-service/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "async-trait" version = "0.1.78" @@ -43,7 +58,12 @@ version = "0.1.0" dependencies = [ "async-trait", "axum", + "axum-extra", + "chrono", + "dotenvy", "fake", + "jsonwebtoken", + "lazy_static", "quickcheck", "quickcheck_macros", "reqwest", @@ -71,10 +91,10 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", "http-body-util", - "hyper", + "hyper 1.2.0", "hyper-util", "itoa", "matchit", @@ -104,8 +124,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", @@ -116,6 +136,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie 0.18.1", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -131,6 +174,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -173,6 +222,99 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie 0.17.0", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[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 = "env_logger" version = "0.7.1" @@ -270,8 +412,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -280,6 +424,25 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.2" @@ -291,7 +454,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -311,6 +474,17 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.1.0" @@ -322,6 +496,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.0" @@ -329,7 +514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http", + "http 1.1.0", ] [[package]] @@ -340,8 +525,8 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" dependencies = [ "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -363,6 +548,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.2.0" @@ -372,16 +581,15 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.2", + "http 1.1.0", + "http-body 1.0.0", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", - "want", ] [[package]] @@ -391,17 +599,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", - "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", "pin-project-lite", "socket2", "tokio", - "tower", - "tower-service", - "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -455,6 +692,21 @@ 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", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -530,6 +782,40 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -578,6 +864,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -616,6 +912,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[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.17" @@ -631,6 +933,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quickcheck" version = "0.9.2" @@ -744,19 +1062,21 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.12.4" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", + "cookie 0.17.0", + "cookie_store", + "encoding_rs", "futures-core", "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "ipnet", "js-sys", "log", @@ -768,6 +1088,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", "tower-service", "url", @@ -777,6 +1098,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.14", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -863,6 +1199,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -888,6 +1236,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "syn" version = "1.0.109" @@ -916,6 +1270,78 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[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.6.0" @@ -1000,8 +1426,8 @@ dependencies = [ "bitflags 2.4.2", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", "http-body-util", "http-range-header", "httpdate", @@ -1084,6 +1510,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -1223,6 +1655,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1357,9 +1798,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" -version = "0.52.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", diff --git a/auth-service/Cargo.toml b/auth-service/Cargo.toml index a6fee33..4d5dd5c 100644 --- a/auth-service/Cargo.toml +++ b/auth-service/Cargo.toml @@ -12,11 +12,16 @@ tower-http = { version = "0.5.0", features = ["fs"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.7.0", features = ["v4", "serde"] } -reqwest = { version = "0.12.4", default-features = false, features = ["json"] } async-trait = "0.1.78" validator = "0.16.1" +axum-extra = { version = "0.9.2", features = ["cookie"] } +jsonwebtoken = "9.2.0" +chrono = "0.4.35" +dotenvy = "0.15.7" +lazy_static = "1.4.0" [dev-dependencies] fake = "=2.3.0" quickcheck = "0.9.2" -quickcheck_macros = "0.9.1" \ No newline at end of file +quickcheck_macros = "0.9.1" +reqwest = { version = "0.11.26", default-features = false, features = ["json", "cookies"] } \ No newline at end of file diff --git a/auth-service/src/domain/mod.rs b/auth-service/src/domain/mod.rs index 2587dff..5b61011 100644 --- a/auth-service/src/domain/mod.rs +++ b/auth-service/src/domain/mod.rs @@ -1,7 +1,7 @@ mod user; mod error; pub mod data_stores; -mod email; +pub mod email; mod password; pub use user::*; diff --git a/auth-service/src/lib.rs b/auth-service/src/lib.rs index 5deeb3f..796d0cc 100644 --- a/auth-service/src/lib.rs +++ b/auth-service/src/lib.rs @@ -15,6 +15,7 @@ pub mod routes; pub mod services; pub mod domain; pub mod app_state; +pub mod utils; #[derive(Serialize, Deserialize)] pub struct ErrorResponse { diff --git a/auth-service/src/routes/login.rs b/auth-service/src/routes/login.rs index 65da6e1..f1a5e23 100644 --- a/auth-service/src/routes/login.rs +++ b/auth-service/src/routes/login.rs @@ -1,8 +1,13 @@ use axum::{http::StatusCode, extract::State, response::IntoResponse, Json}; use serde::Deserialize; +use axum_extra::extract::CookieJar; -use crate::{app_state::AppState, domain::{AuthAPIError, Email, Password}}; +use crate::{ + app_state::AppState, + domain::{AuthAPIError, Email, Password}, + utils::auth::generate_auth_cookie +}; #[derive(Deserialize)] pub struct LoginRequest { @@ -11,19 +16,39 @@ pub struct LoginRequest { } -pub async fn login(State(state): State, Json(request): Json) -> Result { - let email = Email::parse(request.email.clone()).map_err(|_| AuthAPIError::InvalidCredentials)?; - let password = Password::parse(request.password.clone()).map_err(|_| AuthAPIError::InvalidCredentials)?; +pub async fn login( + State(state): State, + jar: CookieJar, + Json(request): Json) -> (CookieJar, Result) { + let email = match Email::parse(request.email.clone()) { + Ok(email) => email, + Err(_) => return (jar, Err(AuthAPIError::InvalidCredentials)) + }; + + let password = match Password::parse(request.password.clone()) { + Ok(password) => password, + Err(_) => return (jar, Err(AuthAPIError::InvalidCredentials)) + }; let user_store = &state.user_store.read().await; if user_store.validate_user(email.clone(), password.clone()).await.is_err() { - return Err(AuthAPIError::IncorrectCredentials) + return (jar, Err(AuthAPIError::IncorrectCredentials)) } - let _user = user_store.get_user(email).await.map_err(|_| AuthAPIError::IncorrectCredentials)?; + if user_store.get_user(email.clone()).await.is_err() { + return (jar, Err(AuthAPIError::IncorrectCredentials)) + }; + + let auth_cookie = match generate_auth_cookie(&email) { + Ok(auth_cookie) => auth_cookie, + Err(_) => return (jar, Err(AuthAPIError::UnexpectedError)) + }; + + let updated_jar = jar.add(auth_cookie); + - Ok(StatusCode::OK.into_response()) + (updated_jar, Ok(StatusCode::OK.into_response())) } diff --git a/auth-service/src/utils/auth.rs b/auth-service/src/utils/auth.rs new file mode 100644 index 0000000..a3a4621 --- /dev/null +++ b/auth-service/src/utils/auth.rs @@ -0,0 +1,132 @@ +use axum_extra::extract::cookie::{Cookie, SameSite}; +use chrono::Utc; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Validation}; +use serde::{Deserialize, Serialize}; + +use crate::domain::email::Email; + +use super::constants::{JWT_COOKIE_NAME, JWT_SECRET}; + +pub fn generate_auth_cookie(email: &Email) -> Result, GenerateTokenError> { + let token = generate_auth_token(email)?; + Ok(create_auth_cookie(token)) +} + + +fn create_auth_cookie(token: String) -> Cookie<'static> { + let cookie = Cookie::build((JWT_COOKIE_NAME, token)) + .path("/") + .http_only(true) + .same_site(SameSite::Lax) + .build(); + + cookie +} + +#[derive(Debug)] +pub enum GenerateTokenError { + TokenError(jsonwebtoken::errors::Error), + UnexpectedError, +} + +pub const TOKEN_TTL_SECONDS: i64 = 600; + +fn generate_auth_token(email: &Email) -> Result { + let delta = chrono::Duration::try_seconds(TOKEN_TTL_SECONDS) + .ok_or(GenerateTokenError::UnexpectedError)?; + + let exp = Utc::now() + .checked_add_signed(delta) + .ok_or(GenerateTokenError::UnexpectedError)? + .timestamp(); + + let exp: usize = exp + .try_into() + .map_err(|_| + GenerateTokenError::UnexpectedError)?; + + let sub = email.as_ref().to_owned(); + + let claims = Claims {sub, exp}; + + create_token(&claims).map_err(GenerateTokenError::TokenError) +} + +pub async fn validate_token(token: &str) -> Result { + decode::( + token, + &DecodingKey::from_secret(JWT_SECRET.as_bytes()), + &Validation::default(), + ) + .map(|data| data.claims) +} + +fn create_token(claims: &Claims) -> Result { + encode( + &jsonwebtoken::Header::default(), + &claims, + &EncodingKey::from_secret(JWT_SECRET.as_bytes()), + ) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_generate_auth_cookie() { + let email = Email::parse("test@example.com".to_owned()).unwrap(); + let cookie = generate_auth_cookie(&email).unwrap(); + assert_eq!(cookie.name(), JWT_COOKIE_NAME); + assert_eq!(cookie.value().split('.').count(), 3); + assert_eq!(cookie.path(), Some("/")); + assert_eq!(cookie.http_only(), Some(true)); + assert_eq!(cookie.same_site(), Some(SameSite::Lax)); + } + + #[tokio::test] + async fn test_create_auth_cookie() { + let token = "test_token".to_owned(); + let cookie = create_auth_cookie(token.clone()); + assert_eq!(cookie.name(), JWT_COOKIE_NAME); + assert_eq!(cookie.value(), token); + assert_eq!(cookie.path(), Some("/")); + assert_eq!(cookie.http_only(), Some(true)); + assert_eq!(cookie.same_site(), Some(SameSite::Lax)); + } + + #[tokio::test] + async fn test_generate_auth_token() { + let email = Email::parse("test@example.com".to_owned()).unwrap(); + let result = generate_auth_token(&email).unwrap(); + assert_eq!(result.split('.').count(), 3); + } + + #[tokio::test] + async fn test_validate_token_with_valid_token() { + let email = Email::parse("test@example.com".to_owned()).unwrap(); + let token = generate_auth_token(&email).unwrap(); + let result = validate_token(&token).await.unwrap(); + assert_eq!(result.sub, "test@example.com"); + + let exp = Utc::now() + .checked_add_signed(chrono::Duration::try_minutes(9).expect("valid duration")) + .expect("valid timestamp") + .timestamp(); + + assert!(result.exp > exp as usize); + } + + #[tokio::test] + async fn test_validate_token_with_invalid_token() { + let token = "invalid_token".to_owned(); + let result = validate_token(&token).await; + assert!(result.is_err()); + } +} diff --git a/auth-service/src/utils/constants.rs b/auth-service/src/utils/constants.rs new file mode 100644 index 0000000..b83bd37 --- /dev/null +++ b/auth-service/src/utils/constants.rs @@ -0,0 +1,25 @@ +use dotenvy::dotenv; +use lazy_static::lazy_static; +use std::env as std_env; + +lazy_static! { + pub static ref JWT_SECRET: String = set_token(); +} + +fn set_token() -> String { + dotenv().ok(); + + let secret = std_env::var(env::JWT_SECRET_ENV_VAR) + .expect("JWT_SECRET must be set"); + + if secret.is_empty() { + panic!("JWT_SECRET must not be empty"); + } + secret +} + +pub mod env { + pub const JWT_SECRET_ENV_VAR: &str = "JWT_SECRET"; +} + +pub const JWT_COOKIE_NAME: &str = "jwt"; \ No newline at end of file diff --git a/auth-service/src/utils/mod.rs b/auth-service/src/utils/mod.rs new file mode 100644 index 0000000..41bb9fa --- /dev/null +++ b/auth-service/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod constants; +pub mod auth; \ No newline at end of file diff --git a/auth-service/tests/api/login.rs b/auth-service/tests/api/login.rs index 67b8928..35f747b 100644 --- a/auth-service/tests/api/login.rs +++ b/auth-service/tests/api/login.rs @@ -1,4 +1,4 @@ -use axum::response; +use auth_service::utils::constants::JWT_COOKIE_NAME; use crate::helpers::{TestApp, get_random_email}; @@ -130,3 +130,42 @@ async fn should_return_401_login_if_incorrect_credentials() { ) } } + +#[tokio::test] +async fn should_return_200_login_if_valid_credentials_and_2fa_disabled() { + let app = TestApp::new().await; + + let random_email = get_random_email(); + + let valid_signup_body = serde_json::json!({ + "email": random_email, + "password": "password123", + "requires2FA": false + }); + + let response = app.signup(&valid_signup_body).await; + + assert_eq!( + response.status().as_u16(), + 201 + ); + + let login_body = serde_json::json!({ + "email": random_email, + "password": "password123" + }); + + let response = app.login(&login_body).await; + + assert_eq!( + response.status().as_u16(), + 200 + ); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); +} diff --git a/compose.yml b/compose.yml index 348cc9d..8a06c7a 100644 --- a/compose.yml +++ b/compose.yml @@ -15,5 +15,7 @@ services: # TODO: change "letsgetrusty" to your Docker Hub username image: redwallet212/auth-service restart: "always" # automatically restart container when server crashes + environment: + JWT_SECRET: ${JWT_SECRET} ports: - "3000:3000" # expose port 3000 so that applications outside the container can connect to it \ No newline at end of file diff --git a/docker.sh b/docker.sh new file mode 100755 index 0000000..2446e2d --- /dev/null +++ b/docker.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Define the location of the .env file (change if needed) +ENV_FILE="./auth-service/.env" + +# Check if the .env file exists +if ! [[ -f "$ENV_FILE" ]]; then + echo "Error: .env file not found!" + exit 1 +fi + +# Read each line in the .env file (ignoring comments) +while IFS= read -r line; do + # Skip blank lines and lines starting with # + if [[ -n "$line" ]] && [[ "$line" != \#* ]]; then + # Split the line into key and value + key=$(echo "$line" | cut -d '=' -f1) + value=$(echo "$line" | cut -d '=' -f2-) + # Export the variable + export "$key=$value" + fi +done < <(grep -v '^#' "$ENV_FILE") + +# Run docker-compose commands with exported variables +docker-compose build +docker-compose up \ No newline at end of file