From ee53ee2f310a8db5a54f8b062f8121c8c1612b78 Mon Sep 17 00:00:00 2001 From: Szepesi Tibor Date: Fri, 5 Jul 2024 18:56:34 +0200 Subject: [PATCH] Add new key management --- .env.template | 3 - Cargo.lock | 240 ++++++++++++++++++----- cmds/src/generate_token.rs | 63 +++--- iam-common/Cargo.toml | 4 +- iam-common/src/keys/jwt.rs | 57 ++++++ iam-common/src/keys/key.rs | 52 +++++ iam-common/src/keys/manager.rs | 34 ++++ iam-common/src/keys/mod.rs | 6 + iam-common/src/lib.rs | 2 +- iam-common/src/token.rs | 96 --------- iam/src/auth/claims.rs | 3 +- iam/src/auth/layer.rs | 4 +- iam/src/handlers/internal/v1/decision.rs | 4 +- iam/src/handlers/mod.rs | 2 + iam/src/handlers/v1/apps/login.rs | 9 +- iam/src/handlers/v1/users/id/actions.rs | 7 +- iam/src/handlers/v1/users/login.rs | 12 +- iam/src/handlers/well_known/jwks.rs | 8 + iam/src/handlers/well_known/mod.rs | 8 + iam/src/shared.rs | 30 +-- libiam/src/lib.rs | 1 - libiam/src/user.rs | 5 +- 22 files changed, 416 insertions(+), 234 deletions(-) create mode 100644 iam-common/src/keys/jwt.rs create mode 100644 iam-common/src/keys/key.rs create mode 100644 iam-common/src/keys/manager.rs create mode 100644 iam-common/src/keys/mod.rs delete mode 100644 iam-common/src/token.rs create mode 100644 iam/src/handlers/well_known/jwks.rs create mode 100644 iam/src/handlers/well_known/mod.rs diff --git a/.env.template b/.env.template index 81c88de..56c3fd1 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1 @@ DATABASE_URL="mysql://iam:secret@localhost:3306/iam" - -JWT_RSA_PRIVATE='-----BEGIN RSA PRIVATE KEY-----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-----END RSA PRIVATE KEY-----' -JWT_RSA_PUBLIC='-----BEGIN PUBLIC KEY-----MIICITANBgkqhkiG9w0BAQEFAAOCAg4AMIICCQKCAgBdptOAsZZBpF7P0+79iKssZuYKq6UMVUYVHYFv2ClXAJmIMU+QXcued9oZA077BXhv5e7Lu8RXqylMNUd3hEEJjisXce3e33bcDrJZmGEljv/I3YCrNWK1LFpqd5YzossJpT+65TPxmeVipqJ65ZUpBCO3V82r3dDMx8d2CGMqOOygI1afeFxoZDVm/H1flR+uDusbB9EvYlKsiCfEGlQz/lnHiHI/bEmdeIzhpCLmRhtWxyyl6wqe07f718JdGpnPo6Aql8UtSMQRjtKcRW2G67hNXGgB7uS5y8qW8fj/fIcyePZGm8TUv3vL+wMUb6+05RN7i9BXt6Eurgok2NQGYvwforJHlCsj3aIzjTcfH3s6jkdZSj9Yho2BgtJi50qWxiWYxTTAmtVSDihjIty2h2NkzlXePWDF+iQW0bkbuYQQKzM6dLGNF0+z8t7ddlvKeqG8CI1+kZ3QR+XsKWSIhhGx+yncbxWUAzqMoLUUK9WlmQttoC8VenFSunNof2QxT+1BbYJt9ZFI5ZltTICR6K9kmRNQQ7qrdQacBsKi2SD+JVK7ESAARj9FZNvf0X78LM+H1NZACe4pT4tlObH4OwkHpl77oCmghNe49Q1CNv7d5QKesOS19kBoQYMYb+jKjKc/uj7iObwTuywX8I1d19gJeHD2XkZS9VVcHbYLUQIDAQAB-----END PUBLIC KEY-----' diff --git a/Cargo.lock b/Cargo.lock index 20d59a1..de41840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,9 +296,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bcrypt" @@ -495,6 +495,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +[[package]] +name = "const-oid" +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" @@ -519,9 +525,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -589,6 +595,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "cxx" version = "1.0.82" @@ -639,11 +672,21 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" dependencies = [ - "const-oid", + "const-oid 0.7.1", "crypto-bigint", "pem-rfc7468", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + [[package]] name = "digest" version = "0.10.6" @@ -660,6 +703,31 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.8.0" @@ -687,6 +755,12 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.25" @@ -829,8 +903,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1223,9 +1299,11 @@ dependencies = [ "bcrypt", "bytes", "chrono", + "ed25519-dalek", "iam-entity", "iam-macros", - "jsonwebtoken 8.2.0", + "jose-jwk", + "jsonwebtoken", "mime", "once_cell", "rand", @@ -1387,6 +1465,39 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +[[package]] +name = "jose-b64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" +dependencies = [ + "base64ct", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "jose-jwa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" +dependencies = [ + "serde", +] + +[[package]] +name = "jose-jwk" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" +dependencies = [ + "jose-b64", + "jose-jwa", + "serde", + "zeroize", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -1413,26 +1524,13 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828" -dependencies = [ - "base64 0.13.1", - "pem 1.0.2", - "ring 0.16.20", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "jsonwebtoken" -version = "9.1.0" +version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ "base64 0.21.7", - "pem 3.0.2", + "js-sys", + "pem", "ring 0.17.5", "serde", "serde_json", @@ -1486,7 +1584,7 @@ dependencies = [ "jsonpath-rust", "k8s-openapi", "kube-core", - "pem 3.0.2", + "pem", "rustls 0.23.10", "rustls-pemfile 2.1.2", "secrecy", @@ -1539,7 +1637,7 @@ dependencies = [ "futures", "iam-common", "iam-entity", - "jsonwebtoken 9.1.0", + "jsonwebtoken", "reqwest", "sea-orm", "serde", @@ -1684,9 +1782,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", @@ -1845,15 +1943,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" -[[package]] -name = "pem" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" -dependencies = [ - "base64 0.13.1", -] - [[package]] name = "pem" version = "3.0.2" @@ -1910,7 +1999,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.68", ] [[package]] @@ -1962,8 +2051,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" dependencies = [ - "der", - "pkcs8", + "der 0.5.1", + "pkcs8 0.8.0", "zeroize", ] @@ -1973,11 +2062,21 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" dependencies = [ - "der", - "spki", + "der 0.5.1", + "spki 0.5.4", "zeroize", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.9", + "spki 0.7.3", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2181,7 +2280,7 @@ dependencies = [ "num-iter", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.8.0", "rand_core", "smallvec", "subtle", @@ -2205,6 +2304,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.20.7" @@ -2505,11 +2613,17 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" -version = "1.0.204" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -2526,13 +2640,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.68", ] [[package]] @@ -2620,6 +2734,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "simple_asn1" version = "0.6.1" @@ -2686,7 +2809,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" dependencies = [ "base64ct", - "der", + "der 0.5.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.9", ] [[package]] @@ -2825,9 +2958,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.69" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -2872,7 +3005,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.68", ] [[package]] @@ -2944,7 +3077,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.68", ] [[package]] @@ -3588,3 +3721,6 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "serde", +] diff --git a/cmds/src/generate_token.rs b/cmds/src/generate_token.rs index cc7d8b6..2fd120c 100644 --- a/cmds/src/generate_token.rs +++ b/cmds/src/generate_token.rs @@ -1,31 +1,34 @@ -use iam_common::{ - database, - token::{Claims, Jwt, JwtTrait}, -}; -use iam_entity::users; -use sea_orm::EntityTrait; -use std::env; +// TODO: implement +fn main() {} -#[tokio::main] -async fn main() { - dotenvy::dotenv().ok(); - - let db = database::connect().await; - let input = env::args().nth(1).expect("no input user"); - - let jwt = Jwt::from_env(); - - let user = users::Entity::find_by_id(input) - .one(&db) - .await - .expect("database failed") - .expect("no such user"); - - let claims = Claims { - subject: user.id, - ..Default::default() - }; - - let token = jwt.encode(&claims).expect("failed to encode claims"); - println!("{token}"); -} +// use iam_common::{ +// database, +// token::{Claims, Jwt, JwtTrait}, +// }; +// use iam_entity::users; +// use sea_orm::EntityTrait; +// use std::env; +// +// #[tokio::main] +// async fn main() { +// dotenvy::dotenv().ok(); +// +// let db = database::connect().await; +// let input = env::args().nth(1).expect("no input user"); +// +// let jwt = Jwt::from_env(); +// +// let user = users::Entity::find_by_id(input) +// .one(&db) +// .await +// .expect("database failed") +// .expect("no such user"); +// +// let claims = Claims { +// subject: user.id, +// ..Default::default() +// }; +// +// let token = jwt.encode(&claims).expect("failed to encode claims"); +// println!("{token}"); +// } diff --git a/iam-common/Cargo.toml b/iam-common/Cargo.toml index 37fefc6..8a2aca2 100644 --- a/iam-common/Cargo.toml +++ b/iam-common/Cargo.toml @@ -14,7 +14,7 @@ rust-argon2 = { version = "1.0.0", default-features = false } bcrypt = "0.13.0" serde = "1.0.152" once_cell = "1.16.0" -jsonwebtoken = "8.2.0" +jsonwebtoken = "9.3.0" axum = { version = "0.6.1", default-features = false } iam-macros = { path = "../iam-macros/" } iam-entity = { path = "../iam-entity/" } @@ -22,3 +22,5 @@ bytes = "1.3.0" serde_json = "1.0.91" mime = "0.3.16" base64 = "0.21.0" +ed25519-dalek = { version = "2.1.1", features = ["pkcs8", "rand_core"] } +jose-jwk = { version = "0.1.2", default-features = false } diff --git a/iam-common/src/keys/jwt.rs b/iam-common/src/keys/jwt.rs new file mode 100644 index 0000000..afb2945 --- /dev/null +++ b/iam-common/src/keys/jwt.rs @@ -0,0 +1,57 @@ +use super::Key; +use crate::error::{self, Result}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::env; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub iss: String, + pub sub: String, + pub exp: i64, + pub nbf: i64, + pub iat: i64, +} + +impl Claims { + pub fn new>(subject: S) -> Self { + let issuer = env::var("HOSTNAME").unwrap_or_else(|_| "dev".to_owned()); + + let now = Utc::now(); + let timestamp = now.timestamp(); + + let exp = (now + Duration::weeks(1)).timestamp(); + + Self { + iss: issuer, + sub: subject.into(), + exp, + nbf: timestamp, + iat: timestamp, + } + } +} + +pub struct Jwt<'a>(&'a Key); + +impl<'a> Jwt<'a> { + #[inline] + pub(super) const fn new(key: &'a Key) -> Self { + Self(key) + } + + pub fn encode(&self, claims: &Claims) -> String { + let header = Header::new(self.0.get_alg()); + jsonwebtoken::encode(&header, claims, &self.0.encoding).unwrap() + } + + pub fn get_claims(&self, token: &str) -> Result { + let validation = Validation::new(self.0.get_alg()); + + jsonwebtoken::decode(token, &self.0.decoding, &validation) + .map(|data| data.claims) + .inspect_err(|err| tracing::warn!(token, error = err.to_string(), "invalid token")) + .map_err(|_| error::JWT_INVALID_TOKEN) + } +} diff --git a/iam-common/src/keys/key.rs b/iam-common/src/keys/key.rs new file mode 100644 index 0000000..681ed5f --- /dev/null +++ b/iam-common/src/keys/key.rs @@ -0,0 +1,52 @@ +use ed25519_dalek::{pkcs8::EncodePrivateKey, SigningKey}; +use jose_jwk::{Jwk, Okp, OkpCurves, Parameters}; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey}; +use rand::rngs::OsRng; + +pub struct Key { + pub(super) jwk: Jwk, + pub(super) encoding: EncodingKey, + pub(super) decoding: DecodingKey, +} + +impl Key { + pub(super) fn generate() -> Key { + let private_key = SigningKey::generate(&mut OsRng); + let public_key = private_key.verifying_key(); + + let bytes = Box::new(public_key.to_bytes()) as Box<[u8]>; + + let jwk = Jwk { + key: jose_jwk::Key::Okp(Okp { + crv: OkpCurves::Ed25519, + x: bytes.into(), + d: None, + }), + prm: Parameters { + kid: Some("jwt".to_owned()), + ..Default::default() + }, + }; + + let encoding = EncodingKey::from_ed_der(private_key.to_pkcs8_der().unwrap().as_bytes()); + let decoding = DecodingKey::from_ed_der(public_key.as_bytes()); + + Key { + jwk, + encoding, + decoding, + } + } + + pub fn get_alg(&self) -> Algorithm { + match self.jwk.key { + jose_jwk::Key::Okp(Okp { + crv: OkpCurves::Ed25519, + .. + }) => Algorithm::EdDSA, + _ => { + panic!("unsupported key type"); + } + } + } +} diff --git a/iam-common/src/keys/manager.rs b/iam-common/src/keys/manager.rs new file mode 100644 index 0000000..427f886 --- /dev/null +++ b/iam-common/src/keys/manager.rs @@ -0,0 +1,34 @@ +pub use jose_jwk::JwkSet; + +use crate::keys::{jwt::Jwt, Key}; +use std::iter; + +pub struct KeyManager { + jwt_key: Key, +} + +impl KeyManager { + pub fn new() -> Self { + Self { + jwt_key: Key::generate(), + } + } + + pub fn jwt(&self) -> Jwt<'_> { + Jwt::new(&self.jwt_key) + } + + pub fn jwks(&self) -> JwkSet { + let keys = iter::once(self.jwt_key.jwk.clone()); + + JwkSet { + keys: keys.collect(), + } + } +} + +impl Default for KeyManager { + fn default() -> Self { + Self::new() + } +} diff --git a/iam-common/src/keys/mod.rs b/iam-common/src/keys/mod.rs new file mode 100644 index 0000000..e73f6bb --- /dev/null +++ b/iam-common/src/keys/mod.rs @@ -0,0 +1,6 @@ +pub mod jwt; +mod key; +mod manager; + +pub use key::*; +pub use manager::*; diff --git a/iam-common/src/lib.rs b/iam-common/src/lib.rs index a5eb334..4be61a1 100644 --- a/iam-common/src/lib.rs +++ b/iam-common/src/lib.rs @@ -2,8 +2,8 @@ pub mod app; pub mod database; pub mod error; mod id; +pub mod keys; pub mod password; -pub mod token; pub mod user; pub use id::*; diff --git a/iam-common/src/token.rs b/iam-common/src/token.rs deleted file mode 100644 index baffeb8..0000000 --- a/iam-common/src/token.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::error::{self, Result}; -use chrono::{Duration, Utc}; -use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; -use std::{default::Default, ops::Add}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Claims { - #[serde(rename = "iss")] - pub issuer: String, - #[serde(rename = "sub")] - pub subject: String, - #[serde(rename = "aud")] - pub audience: Vec, - #[serde(rename = "exp")] - pub expires_at: i64, - #[serde(rename = "nbf")] - pub not_before: i64, - #[serde(rename = "iat")] - pub issued_at: i64, -} - -impl Default for Claims { - fn default() -> Self { - Claims { - issuer: std::env::var("HOSTNAME").unwrap_or_else(|_| "dev".to_string()), - audience: vec!["https://verseghy-gimnazium.net".to_string()], - expires_at: Utc::now().add(Duration::weeks(1)).timestamp(), - not_before: Utc::now().timestamp(), - issued_at: Utc::now().timestamp(), - subject: String::new(), - } - } -} - -static VALIDATION: Lazy = Lazy::new(|| { - let mut validation = Validation::new(Algorithm::RS256); - validation.set_audience(&["https://verseghy-gimnazium.net"]); - validation.leeway = 5; - - validation -}); - -pub struct Jwt { - encoding: Option, - decoding: DecodingKey, -} - -impl Jwt { - pub fn new(encoding: Option<&[u8]>, decoding: &[u8]) -> Self { - let encoding = encoding.map(|k| EncodingKey::from_rsa_pem(k).expect("invalid public key")); - let decoding = DecodingKey::from_rsa_pem(decoding).expect("invalid private key"); - - Self { encoding, decoding } - } - - pub fn from_env() -> Self { - let encoding = std::env::var("JWT_RSA_PRIVATE").expect("JWT_RSA_PRIVATE not set"); - let decoding = std::env::var("JWT_RSA_PUBLIC").expect("JWT_RSA_PUBLIC not set"); - - Self::new(Some(encoding.as_ref()), decoding.as_ref()) - } -} - -impl Default for Jwt { - fn default() -> Self { - Self::from_env() - } -} - -pub trait JwtTrait { - fn get_claims(&self, token: &str) -> Result; - fn encode(&self, claims: &Claims) -> Result; -} - -impl JwtTrait for Jwt { - fn get_claims(&self, token: &str) -> Result { - match jsonwebtoken::decode(token, &self.decoding, &VALIDATION) { - Ok(decode) => Ok(decode.claims), - Err(error) => { - tracing::warn!(token, error = error.to_string(), "invalid token"); - Err(error::JWT_INVALID_TOKEN) - } - } - } - - fn encode(&self, claims: &Claims) -> Result { - let Some(encoding) = &self.encoding else { - return Err(error::INTERNAL); - }; - - jsonwebtoken::encode(&Header::new(Algorithm::RS256), &claims, encoding) - .map_err(|_| error::JWT_INVALID_TOKEN) - } -} diff --git a/iam/src/auth/claims.rs b/iam/src/auth/claims.rs index 1a26deb..e81faac 100644 --- a/iam/src/auth/claims.rs +++ b/iam/src/auth/claims.rs @@ -7,7 +7,6 @@ use axum::{ response::Response, Extension, }; -use iam_common::token::JwtTrait; use std::sync::Arc; pub async fn get_claims( @@ -23,7 +22,7 @@ where let mut request = Request::from_parts(parts, body); if let Ok(token) = token { - if let Ok(claims) = shared.jwt().get_claims(token.token()) { + if let Ok(claims) = shared.key_manager().jwt().get_claims(token.token()) { request.extensions_mut().insert(Arc::new(claims)); } } diff --git a/iam/src/auth/layer.rs b/iam/src/auth/layer.rs index 8a37a06..c089b2e 100644 --- a/iam/src/auth/layer.rs +++ b/iam/src/auth/layer.rs @@ -3,7 +3,7 @@ use crate::shared::SharedTrait; use hyper::Request; use iam_common::{ error::{self, Result}, - token::Claims, + keys::jwt::Claims, }; use std::sync::Arc; @@ -18,7 +18,7 @@ where .get::>() .ok_or(error::INVALID_AUTH_HEADER)?; - permission::check(claims.subject.as_str(), actions, shared.db()).await?; + permission::check(claims.sub.as_str(), actions, shared.db()).await?; Ok(()) } diff --git a/iam/src/handlers/internal/v1/decision.rs b/iam/src/handlers/internal/v1/decision.rs index 784a105..942c10a 100644 --- a/iam/src/handlers/internal/v1/decision.rs +++ b/iam/src/handlers/internal/v1/decision.rs @@ -1,6 +1,6 @@ use crate::{auth, json::Json, shared::SharedTrait}; use axum::{http::StatusCode, Extension}; -use iam_common::{error::Result, token::Claims}; +use iam_common::{error::Result, keys::jwt::Claims}; use serde::Deserialize; use std::sync::Arc; @@ -21,7 +21,7 @@ pub async fn decision( ) -> Result { let actions: Vec<&str> = req.action_list.iter().map(|x| x.name.as_str()).collect(); - auth::check(&claims.subject, &actions, shared.db()).await?; + auth::check(&claims.sub, &actions, shared.db()).await?; Ok(StatusCode::NO_CONTENT) } diff --git a/iam/src/handlers/mod.rs b/iam/src/handlers/mod.rs index e83c540..267339a 100644 --- a/iam/src/handlers/mod.rs +++ b/iam/src/handlers/mod.rs @@ -1,5 +1,6 @@ mod internal; mod v1; +mod well_known; use crate::shared::SharedTrait; use axum::Router; @@ -8,4 +9,5 @@ pub fn routes() -> Router { Router::new() .nest("/v1", v1::routes::()) .nest("/internal", internal::routes::()) + .nest("/.well-known", well_known::routes::()) } diff --git a/iam/src/handlers/v1/apps/login.rs b/iam/src/handlers/v1/apps/login.rs index 4292d11..0a00ebd 100644 --- a/iam/src/handlers/v1/apps/login.rs +++ b/iam/src/handlers/v1/apps/login.rs @@ -2,7 +2,7 @@ use crate::{json::Json, SharedTrait}; use axum::Extension; use iam_common::{ error::{self, Result}, - token::{self, JwtTrait}, + keys::jwt::Claims, }; use iam_entity::apps; use sea_orm::EntityTrait; @@ -37,12 +37,7 @@ pub async fn login_app( return Err(error::APP_INVALID_TOKEN); } - let claims = token::Claims { - subject: id, - ..Default::default() - }; - - let token = shared.jwt().encode(&claims)?; + let token = shared.key_manager().jwt().encode(&Claims::new(id)); Ok(Json(Response { token })) } diff --git a/iam/src/handlers/v1/users/id/actions.rs b/iam/src/handlers/v1/users/id/actions.rs index 263bebd..974785e 100644 --- a/iam/src/handlers/v1/users/id/actions.rs +++ b/iam/src/handlers/v1/users/id/actions.rs @@ -1,14 +1,13 @@ -use std::sync::Arc; - use crate::{json::Json, SharedTrait}; use axum::{extract::Path, Extension}; use iam_common::{ error::{self, Result}, - token::Claims, + keys::jwt::Claims, }; use iam_entity::{actions, users}; use sea_orm::{query::QueryFilter, ColumnTrait, FromQueryResult, Related}; use serde::Serialize; +use std::sync::Arc; #[derive(Debug, FromQueryResult, Serialize)] pub struct Action { @@ -22,7 +21,7 @@ pub async fn get_actions( Path(id): Path, Extension(claims): Extension>, ) -> Result>> { - if id != claims.subject { + if id != claims.sub { return Err(error::NO_PERMISSION); } diff --git a/iam/src/handlers/v1/users/login.rs b/iam/src/handlers/v1/users/login.rs index 4cce3a1..5273ce0 100644 --- a/iam/src/handlers/v1/users/login.rs +++ b/iam/src/handlers/v1/users/login.rs @@ -5,8 +5,8 @@ use crate::{ use axum::Extension; use iam_common::{ error::{self, Result}, + keys::jwt::Claims, password, - token::{self, JwtTrait}, }; use iam_entity::users; use sea_orm::{ @@ -15,7 +15,6 @@ use sea_orm::{ ActiveValue, }; use serde::{Deserialize, Serialize}; -use std::default::Default; use validator::Validate; #[derive(Deserialize, Debug, Validate)] @@ -56,14 +55,9 @@ pub async fn login( return Err(error::INVALID_EMAIL_OR_PASSWORD); } - let claims = token::Claims { - subject: res.id.to_string(), - ..Default::default() - }; + crate::audit!(action = "login", user = res.id); - let token = shared.jwt().encode(&claims)?; - - crate::audit!(action = "login", user = res.id.to_string(),); + let token = shared.key_manager().jwt().encode(&Claims::new(res.id)); Ok(Json(LoginResponse { token })) } diff --git a/iam/src/handlers/well_known/jwks.rs b/iam/src/handlers/well_known/jwks.rs new file mode 100644 index 0000000..c9aa013 --- /dev/null +++ b/iam/src/handlers/well_known/jwks.rs @@ -0,0 +1,8 @@ +use crate::{json::Json, shared::SharedTrait}; +use axum::Extension; +use iam_common::keys::JwkSet; + +pub async fn get(Extension(shared): Extension) -> Json { + let set = shared.key_manager().jwks(); + Json(set) +} diff --git a/iam/src/handlers/well_known/mod.rs b/iam/src/handlers/well_known/mod.rs new file mode 100644 index 0000000..3e9d52f --- /dev/null +++ b/iam/src/handlers/well_known/mod.rs @@ -0,0 +1,8 @@ +mod jwks; + +use crate::shared::SharedTrait; +use axum::{routing::get, Router}; + +pub fn routes() -> Router { + Router::new().route("/jwks.json", get(jwks::get::)) +} diff --git a/iam/src/shared.rs b/iam/src/shared.rs index 652b765..0d69c36 100644 --- a/iam/src/shared.rs +++ b/iam/src/shared.rs @@ -1,18 +1,17 @@ -use iam_common::{database, token::Jwt}; +use iam_common::{database, keys::KeyManager}; use sea_orm::DbConn; use std::sync::Arc; pub trait SharedTrait: Clone + Send + Sync + 'static { type Db: sea_orm::ConnectionTrait + sea_orm::TransactionTrait; - type Jwt: iam_common::token::JwtTrait; fn db(&self) -> &Self::Db; - fn jwt(&self) -> &Self::Jwt; + fn key_manager(&self) -> &KeyManager; } pub struct SharedInner { pub db: DbConn, - pub jwt: Jwt, + pub key_manager: KeyManager, } #[derive(Clone)] @@ -22,14 +21,13 @@ pub struct Shared { impl SharedTrait for Shared { type Db = DbConn; - type Jwt = Jwt; fn db(&self) -> &DbConn { &self.inner.db } - fn jwt(&self) -> &Jwt { - &self.inner.jwt + fn key_manager(&self) -> &KeyManager { + &self.inner.key_manager } } @@ -37,7 +35,7 @@ pub async fn create_shared() -> Shared { Shared { inner: Arc::new(SharedInner { db: database::connect().await, - jwt: Jwt::from_env(), + key_manager: KeyManager::new(), }), } } @@ -51,7 +49,6 @@ pub mod mock { pub struct MockSharedInner { db: Option, - jwt: Option, } #[derive(Clone)] @@ -61,10 +58,7 @@ pub mod mock { impl MockShared { pub fn builder() -> MockSharedInner { - MockSharedInner { - db: None, - jwt: None, - } + MockSharedInner { db: None } } pub fn empty() -> Self { @@ -78,11 +72,6 @@ pub mod mock { self } - pub fn jwt(mut self, jwt: Jwt) -> Self { - self.jwt = Some(jwt); - self - } - pub fn build(mut self) -> MockShared { MockShared { inner: Arc::new(self), @@ -92,14 +81,13 @@ pub mod mock { impl SharedTrait for MockShared { type Db = DbConn; - type Jwt = Jwt; fn db(&self) -> &DbConn { self.inner.db.as_ref().expect("database not set") } - fn jwt(&self) -> &Jwt { - self.inner.jwt.as_ref().expect("jwt not set") + fn key_manager(&self) -> &KeyManager { + todo!() } } } diff --git a/libiam/src/lib.rs b/libiam/src/lib.rs index 67a8149..6141c7c 100644 --- a/libiam/src/lib.rs +++ b/libiam/src/lib.rs @@ -7,7 +7,6 @@ mod utils; use std::sync::Arc; pub use app::App; -pub use iam_common::token::Jwt; pub use user::User; #[derive(Debug)] diff --git a/libiam/src/user.rs b/libiam/src/user.rs index 5e185c2..70f0e70 100644 --- a/libiam/src/user.rs +++ b/libiam/src/user.rs @@ -3,8 +3,7 @@ use crate::{ utils::Either, Iam, }; -use iam_common::token::Claims; -use iam_common::Id; +use iam_common::{keys::jwt::Claims, Id}; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use reqwest::Client; use serde::Deserialize; @@ -78,7 +77,7 @@ impl User { ) .unwrap() .claims - .subject + .sub .as_str() ) .as_str(),