From c6f7830d9d03992ef2ee2fca3c32b513a7d7c3e4 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Mar 2025 20:38:09 +0100 Subject: [PATCH] Start to support token authentication --- moneymgr_backend/Cargo.lock | 422 ++++++++++++++++++ moneymgr_backend/Cargo.toml | 3 +- moneymgr_backend/examples/api_curl.rs | 75 ++++ moneymgr_backend/src/constants.rs | 3 + .../src/extractors/auth_extractor.rs | 135 +++++- moneymgr_backend/src/models/tokens.rs | 9 + 6 files changed, 644 insertions(+), 3 deletions(-) create mode 100644 moneymgr_backend/examples/api_curl.rs diff --git a/moneymgr_backend/Cargo.lock b/moneymgr_backend/Cargo.lock index 5acd0f1..ba6f0ab 100644 --- a/moneymgr_backend/Cargo.lock +++ b/moneymgr_backend/Cargo.lock @@ -403,6 +403,18 @@ 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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.88" @@ -481,6 +493,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.20.0" @@ -493,12 +511,35 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "binstring" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed79c2a8151273c70956b5e3cdfdc1ff6c1a8b9779ba59c6807d281b32ee2f86" + [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -632,6 +673,17 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -665,6 +717,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-random" version = "0.1.18" @@ -685,6 +743,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -749,6 +813,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -760,6 +836,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b916ba8ce9e4182696896f015e8a5ae6081b305f74690baa8465e35f5a142ea4" + [[package]] name = "ctr" version = "0.9.2" @@ -804,6 +886,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -923,6 +1016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -961,12 +1055,57 @@ dependencies = [ "syn", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.15", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1021,6 +1160,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "flate2" version = "1.1.0" @@ -1164,6 +1313,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1173,8 +1323,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1205,6 +1357,17 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -1285,6 +1448,30 @@ dependencies = [ "digest", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b3a0f572aa8389d325f5852b9e0a333a15b0f86ecccbb3fdb6e97cd86dc67c" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.11" @@ -1657,6 +1844,46 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1691,6 +1918,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1698,6 +1928,12 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "light-openid" version = "1.0.4" @@ -1853,6 +2089,7 @@ dependencies = [ "env_logger", "futures-util", "ipnet", + "jwt-simple", "lazy-regex", "lazy_static", "light-openid", @@ -1892,6 +2129,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1907,6 +2161,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1914,6 +2179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1991,6 +2257,30 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -2020,6 +2310,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2058,6 +2357,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2116,6 +2436,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -2338,6 +2667,16 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2352,6 +2691,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-ini" version = "0.21.1" @@ -2519,6 +2879,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2647,6 +3021,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.9" @@ -2672,6 +3056,22 @@ 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 = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2696,6 +3096,19 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.15", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "syn" version = "2.0.100" @@ -3133,6 +3546,15 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" diff --git a/moneymgr_backend/Cargo.toml b/moneymgr_backend/Cargo.toml index 82ceaa9..d5f5abc 100644 --- a/moneymgr_backend/Cargo.toml +++ b/moneymgr_backend/Cargo.toml @@ -25,4 +25,5 @@ serde_json = "1.0.140" light-openid = "1.0.4" rand = "0.9.0" ipnet = { version = "2.11.0", features = ["serde"] } -lazy-regex = "3.4.1" \ No newline at end of file +lazy-regex = "3.4.1" +jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] } \ No newline at end of file diff --git a/moneymgr_backend/examples/api_curl.rs b/moneymgr_backend/examples/api_curl.rs new file mode 100644 index 0000000..a97bac7 --- /dev/null +++ b/moneymgr_backend/examples/api_curl.rs @@ -0,0 +1,75 @@ +use clap::Parser; +use jwt_simple::algorithms::HS256Key; +use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike}; +use moneymgr_backend::constants; +use moneymgr_backend::extractors::auth_extractor::TokenClaims; +use moneymgr_backend::utils::rand_utils::rand_string; +use std::ops::Add; +use std::os::unix::prelude::CommandExt; +use std::process::Command; + +/// cURL wrapper to query Money manager +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// URL to Money manager API + #[arg(short('U'), long, env, default_value = "http://localhost:8000/api")] + matrix_gw_url: String, + + /// Token ID + #[arg(short('i'), long, env)] + token_id: u32, + + /// Token secret + #[arg(short('t'), long, env)] + token_secret: String, + + /// Request verb + #[arg(short('X'), long, default_value = "GET")] + method: String, + + /// Request URI + uri: String, + + /// Command line arguments to pass to cURL + #[clap(trailing_var_arg = true, allow_hyphen_values = true)] + run: Vec, +} + +fn main() { + let args: Args = Args::parse(); + + let full_url = format!("{}{}", args.matrix_gw_url, args.uri); + log::debug!("Full URL: {full_url}"); + + let key = HS256Key::from_bytes(args.token_secret.as_bytes()); + + let claims = JWTClaims:: { + issued_at: Some(Clock::now_since_epoch()), + expires_at: Some(Clock::now_since_epoch().add(Duration::from_mins(15))), + invalid_before: None, + issuer: None, + subject: None, + audiences: None, + jwt_id: None, + nonce: Some(rand_string(10)), + custom: TokenClaims { + method: args.method.to_string(), + uri: args.uri, + }, + }; + + let jwt = key + .with_key_id(&args.token_id.to_string()) + .authenticate(claims) + .expect("Failed to sign JWT!"); + + let _ = Command::new("curl") + .args(["-X", &args.method]) + .args(["-H", &format!("{}: {jwt}", constants::API_TOKEN_HEADER)]) + .args(args.run) + .arg(full_url) + .exec(); + + panic!("Failed to run cURL!") +} diff --git a/moneymgr_backend/src/constants.rs b/moneymgr_backend/src/constants.rs index 2e52df7..0f2356d 100644 --- a/moneymgr_backend/src/constants.rs +++ b/moneymgr_backend/src/constants.rs @@ -6,6 +6,9 @@ pub const TOKENS_LEN: usize = 50; /// Header used to authenticate API requests made using a token pub const API_TOKEN_HEADER: &str = "X-Auth-Token"; +/// Max token validity +pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60; + /// Session-specific constants pub mod sessions { /// OpenID auth session state key diff --git a/moneymgr_backend/src/extractors/auth_extractor.rs b/moneymgr_backend/src/extractors/auth_extractor.rs index d37a6e6..5b43875 100644 --- a/moneymgr_backend/src/extractors/auth_extractor.rs +++ b/moneymgr_backend/src/extractors/auth_extractor.rs @@ -1,11 +1,24 @@ use crate::app_config::AppConfig; +use crate::constants; use crate::extractors::money_session::MoneySession; -use crate::models::tokens::Token; +use crate::models::tokens::{Token, TokenID}; use crate::models::users::{User, UserID}; -use crate::services::users_service; +use crate::services::{tokens_service, users_service}; +use actix_remote_ip::RemoteIP; use actix_web::dev::Payload; use actix_web::error::ErrorPreconditionFailed; use actix_web::{Error, FromRequest, HttpRequest}; +use jwt_simple::algorithms::{HS256Key, MACLike}; +use jwt_simple::common::VerificationOptions; +use jwt_simple::prelude::Duration; +use std::str::FromStr; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct TokenClaims { + #[serde(rename = "met")] + pub method: String, + pub uri: String, +} #[derive(Debug, Clone)] pub enum AuthenticatedMethod { @@ -36,7 +49,125 @@ impl FromRequest for AuthExtractor { fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let req = req.clone(); + + let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() { + Ok(ip) => ip, + Err(e) => return Box::pin(async { Err(e) }), + }; + Box::pin(async move { + // Check for authentication using OpenID + if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) { + let Ok(jwt_token) = token.to_str() else { + return Err(actix_web::error::ErrorBadRequest( + "Failed to decode token as string!", + )); + }; + + let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) { + Ok(m) => m, + Err(e) => { + log::error!("Failed to decode JWT header metadata! {e}"); + return Err(actix_web::error::ErrorBadRequest( + "Failed to decode JWT header metadata!", + )); + } + }; + + // Extract token ID + let Some(kid) = metadata.key_id() else { + return Err(actix_web::error::ErrorBadRequest( + "Missing key id in request!", + )); + }; + + let token_id = match TokenID::from_str(kid) { + Ok(i) => i, + Err(e) => { + log::error!("Failed to parse token id! {e}"); + return Err(actix_web::error::ErrorBadRequest( + "Failed to parse token id!", + )); + } + }; + + // Get token information + let Ok(token) = tokens_service::get_by_id(token_id).await else { + log::error!("Token not found!"); + return Err(actix_web::error::ErrorForbidden("Token not found!")); + }; + + // Decode JWT + let key = HS256Key::from_bytes(token.token_value.as_ref()); + let verif = VerificationOptions { + max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)), + ..Default::default() + }; + + let claims = match key.verify_token::(jwt_token, Some(verif)) { + Ok(t) => t, + Err(e) => { + log::error!("JWT validation failed! {e}"); + return Err(actix_web::error::ErrorForbidden("JWT validation failed!")); + } + }; + + // Check for nonce + if claims.nonce.is_none() { + return Err(actix_web::error::ErrorBadRequest( + "A nonce is required in auth JWT!", + )); + } + + // Check IP restriction + if let Some(net) = token.ip_net() { + if !net.contains(&remote_ip.0) { + log::error!( + "Trying to use token {:?} from unauthorized IP address: {remote_ip:?}", + token.id() + ); + return Err(actix_web::error::ErrorForbidden( + "This token cannot be used from this IP address!", + )); + } + } + + // Check for write access + if token.read_only && !req.method().is_safe() { + return Err(actix_web::error::ErrorBadRequest( + "Read only token cannot perform write operations!", + )); + } + + // Check for authorization + let uri = req.uri().to_string(); + let authorized = (uri.starts_with("/api/account/") && token.right_account) + || (uri.starts_with("/api/movement/") && token.right_movement) + || (uri.starts_with("/api/inbox/") && token.right_inbox) + || (uri.starts_with("/api/attachment/") && token.right_attachment) + || (uri.starts_with("/api/auth/") && token.right_auth); + + if !authorized { + return Err(actix_web::error::ErrorBadRequest( + "This token cannot be used to query this route!", + )); + } + + // Get user information + let Ok(user) = users_service::get_user_by_id(token.user_id()) else { + return Err(actix_web::error::ErrorBadRequest( + "Failed to get user information from token!", + )); + }; + + // TODO : update token last activity & expiration + + return Ok(Self { + method: AuthenticatedMethod::Token(token), + user, + }); + } + // Check if login is hard-coded as program argument if let Some(email) = &AppConfig::get().unsecure_auto_login_email { let user = users_service::get_user_by_email(email).map_err(|e| { diff --git a/moneymgr_backend/src/models/tokens.rs b/moneymgr_backend/src/models/tokens.rs index 866d8b0..6c9f28b 100644 --- a/moneymgr_backend/src/models/tokens.rs +++ b/moneymgr_backend/src/models/tokens.rs @@ -3,11 +3,20 @@ use crate::schema::*; use crate::utils::time_utils::time; use diesel::prelude::*; use std::cmp::min; +use std::num::ParseIntError; use std::str::FromStr; #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct TokenID(pub i32); +impl FromStr for TokenID { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?)) + } +} + #[derive(Default, Queryable, Debug, Clone, serde::Serialize)] pub struct Token { id: i32,