From 7d3f9d4f04dfae783f765160e1eb013b95440a72 Mon Sep 17 00:00:00 2001 From: eraden Date: Mon, 19 Aug 2024 08:01:24 +0200 Subject: [PATCH] Improve rauthy impl --- Cargo.lock | 552 ++++++++++- crates/idp/Cargo.toml | 6 + crates/idp/src/main.rs | 6 + crates/idp/src/rauthy/api_types.rs | 1489 ++++++++++++++++++++++++++++ crates/idp/src/rauthy/mod.rs | 362 +++++++ crates/idp/src/rauthy/newtypes.rs | 26 + 6 files changed, 2425 insertions(+), 16 deletions(-) create mode 100644 crates/idp/src/rauthy/api_types.rs create mode 100644 crates/idp/src/rauthy/mod.rs create mode 100644 crates/idp/src/rauthy/newtypes.rs diff --git a/Cargo.lock b/Cargo.lock index 5d929f9..be07c5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,11 +41,11 @@ dependencies = [ "brotli", "bytes 1.6.0", "bytestring", - "derive_more", + "derive_more 0.99.18", "encoding_rs", "flate2", "futures-core", - "h2", + "h2 0.3.26", "http 0.2.12", "httparse", "httpdate", @@ -157,7 +157,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie 0.16.2", - "derive_more", + "derive_more 0.99.18", "encoding_rs", "futures-core", "futures-util", @@ -326,6 +326,45 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "asn1-rs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -521,7 +560,7 @@ dependencies = [ "hmac", "http-types", "hyper 0.14.29", - "hyper-tls", + "hyper-tls 0.5.0", "serde", "serde_json", "serde_path_to_error", @@ -609,6 +648,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "authenticator-ctap2-2021" +version = "0.3.2-dev.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06c690e5e2800f70c0cf8773a9fe7680d66e719dae9b4cabedd13ef4885d056" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "cfg-if", + "core-foundation", + "devd-rs", + "libc", + "libudev", + "log", + "memoffset 0.6.5", + "nom", + "openssl", + "openssl-sys", + "rand 0.8.5", + "runloop", + "serde", + "serde_bytes", + "serde_cbor", + "serde_json", + "sha2", + "winapi", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -744,6 +811,28 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "base64urlsafedata" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b3d30abb74120a9d5267463b9e0045fdccc4dd152e7249d966612dc1721384" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "base64urlsafedata" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56894edf5cd1efa7068d7454adeb7ce0b3da4ffa5ab08cfc06165bbc62f0c7" +dependencies = [ + "base64 0.21.7", + "paste", + "serde", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -1209,6 +1298,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" +[[package]] +name = "compact_jwt" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa76ef19968577838a34d02848136bb9b6bdbfd7675fb968fe9c931bc434b33" +dependencies = [ + "base64 0.13.1", + "base64urlsafedata 0.1.3", + "hex", + "openssl", + "serde", + "serde_json", + "tracing", + "url", + "uuid 1.9.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1226,7 +1332,7 @@ dependencies = [ "cookie 0.18.1", "parking_lot 0.12.3", "password-hash", - "rand 0.7.3", + "rand 0.8.5", "serde", "serde_json", "thiserror", @@ -1265,6 +1371,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -1473,6 +1588,12 @@ dependencies = [ "parking_lot_core 0.9.10", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "db-utils" version = "0.1.0" @@ -1512,6 +1633,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1528,19 +1663,49 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", "syn 2.0.68", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "deunicode" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" +[[package]] +name = "devd-rs" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9313f104b590510b46fc01c0a324fc76505c13871454d3c48490468d04c8d395" +dependencies = [ + "libc", + "nom", +] + [[package]] name = "digest" version = "0.10.7" @@ -1573,6 +1738,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -2221,6 +2397,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes 1.6.0", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hashbrown" version = "0.12.3" @@ -2461,7 +2662,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2484,6 +2685,7 @@ dependencies = [ "bytes 1.6.0", "futures-channel", "futures-util", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2537,6 +2739,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.6.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.5" @@ -2604,23 +2822,29 @@ dependencies = [ "bytes 1.6.0", "channels", "config", + "derive_more 1.0.0", "dotenv", "fake", - "futures 0.1.31", + "futures 0.3.30", "gumdrop", "json", "model", + "nutype", "rauthy-client", + "reqwest 0.12.5", "rumqttc", "serde", + "serde_json", "sqlx", - "sqlx-core 0.6.3", + "sqlx-core 0.7.4", "tarpc", "testx", "thiserror", "tokio", "tracing", "uuid 1.9.0", + "validator", + "webauthn-authenticator-rs", ] [[package]] @@ -2803,6 +3027,27 @@ dependencies = [ "signature", ] +[[package]] +name = "kinded" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" +dependencies = [ + "kinded_macros", +] + +[[package]] +name = "kinded_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2861,6 +3106,26 @@ dependencies = [ "libc", ] +[[package]] +name = "libudev" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "libz-sys" version = "1.1.18" @@ -2994,6 +3259,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -3066,7 +3340,7 @@ version = "0.1.0" dependencies = [ "argon2", "chrono", - "derive_more", + "derive_more 0.99.18", "email_address", "fake", "password-hash", @@ -3108,7 +3382,7 @@ dependencies = [ "cfg-if", "cfg_aliases 0.1.1", "libc", - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -3164,6 +3438,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -3204,6 +3489,29 @@ dependencies = [ "libc", ] +[[package]] +name = "nutype" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362399c4581483ed2813c9b05dd6bcd903c60e61005c4b838c65ae755be69dd6" +dependencies = [ + "nutype_macros", +] + +[[package]] +name = "nutype_macros" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0625bcc0c714bdf12a451c4f6510b949abb095d98cc3cc8fe3812a8100ca6592" +dependencies = [ + "cfg-if", + "kinded", + "proc-macro2", + "quote", + "syn 2.0.68", + "urlencoding", +] + [[package]] name = "object" version = "0.36.0" @@ -3213,6 +3521,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -3596,7 +3913,7 @@ dependencies = [ "actix-web", "async-stripe", "async-trait", - "derive_more", + "derive_more 0.99.18", "fulfillment_adapter", "payment-adapter", "plugin-api", @@ -3606,6 +3923,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "pcsc" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ed9d7f816b7d9ce9ddb0062dd2f393b3af31411a95a35411809b4b9116ea08" +dependencies = [ + "bitflags 1.3.2", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09e9ba80f2c4d167f936d27594f7248bca3295921ffbfa44a24b339b6cb7403" +dependencies = [ + "pkg-config", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3700,7 +4036,7 @@ dependencies = [ "bincode", "cache-adapter", "config", - "derive_more", + "derive_more 0.99.18", "event-bus-adapter", "file-storage-adapter", "futures 0.3.30", @@ -4235,11 +4571,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.29", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -4275,18 +4611,22 @@ dependencies = [ "async-compression", "base64 0.22.1", "bytes 1.6.0", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "http-body-util", "hyper 1.3.1", "hyper-rustls", + "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -4298,7 +4638,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", + "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.0", "tokio-util", "tower-service", @@ -4396,6 +4738,16 @@ dependencies = [ "serde", ] +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rsa" version = "0.9.6" @@ -4435,6 +4787,12 @@ dependencies = [ "tokio-rustls 0.25.0", ] +[[package]] +name = "runloop" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d79b4b604167921892e84afbbaad9d5ad74e091bf6c511d9dbfb0593f09fabd" + [[package]] name = "rust-ini" version = "0.18.0" @@ -4515,6 +4873,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.37.27" @@ -4888,6 +5255,35 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_cbor_2" +version = "0.12.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.203" @@ -5386,6 +5782,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -5691,6 +6099,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -5790,7 +6199,7 @@ dependencies = [ "bytes 1.6.0", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.29", @@ -6032,6 +6441,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -6285,6 +6700,93 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webauthn-attestation-ca" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b0f2ebaf5650ca15b515a761f31ed6477fa2312491cf632a71102ac22b82784" +dependencies = [ + "base64urlsafedata 0.5.0", + "openssl", + "serde", + "tracing", + "uuid 1.9.0", +] + +[[package]] +name = "webauthn-authenticator-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0200dacdf1e6f9e48c6d6671de3d001b0ccd30ac21df115bcc07de2ed12bef" +dependencies = [ + "async-stream", + "async-trait", + "authenticator-ctap2-2021", + "base64 0.21.7", + "base64urlsafedata 0.5.0", + "bitflags 1.3.2", + "futures 0.3.30", + "hex", + "nom", + "num-derive", + "num-traits", + "openssl", + "pcsc", + "rpassword", + "serde", + "serde_bytes", + "serde_cbor_2", + "serde_json", + "tokio", + "tokio-stream", + "tracing", + "unicode-normalization", + "url", + "uuid 1.9.0", + "webauthn-rs-core", + "webauthn-rs-proto", +] + +[[package]] +name = "webauthn-rs-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1ee1dc7f4138b8fd05a74a6eae93ddaf504c5a60861f1eb95d9de3172900b3" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata 0.5.0", + "compact_jwt", + "der-parser", + "hex", + "nom", + "openssl", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "serde_cbor_2", + "serde_json", + "thiserror", + "tracing", + "url", + "uuid 1.9.0", + "webauthn-attestation-ca", + "webauthn-rs-proto", + "x509-parser", +] + +[[package]] +name = "webauthn-rs-proto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1c6dc254607f48eec3bdb35b86b377202436859ca1e4c9290afafd7349dcc3" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata 0.5.0", + "serde", + "serde_json", + "url", +] + [[package]] name = "webpki" version = "0.22.4" @@ -6560,6 +7062,24 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/crates/idp/Cargo.toml b/crates/idp/Cargo.toml index d83479a..dfb81d5 100644 --- a/crates/idp/Cargo.toml +++ b/crates/idp/Cargo.toml @@ -12,14 +12,18 @@ bincode = { version = "1" } bytes = { version = "1" } channels = { path = "../channels" } config = { path = "../config" } +derive_more = "1.0.0" dotenv = { version = "0" } futures = { version = "0" } gumdrop = { version = "0" } json = { version = "0" } model = { path = "../model", features = ['db'] } +nutype = { version = "0.4.3", features = ["serde"] } rauthy-client = { version = "0.4.0", features = ["qrcode", "userinfo"] } +reqwest = "0.12.5" rumqttc = { version = "*" } serde = { version = "1", features = ["derive"] } +serde_json.workspace = true sqlx = { version = "0", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } sqlx-core = { version = "0", features = [] } tarpc = { version = "0", features = ["tokio1", "serde-transport-bincode", "serde-transport", "serde", "serde-transport-json", "tcp"] } @@ -27,6 +31,8 @@ thiserror = { version = "1" } tokio = { version = "1", features = ['full'] } tracing = { version = "0" } uuid = { workspace = true, features = ["v4"] } +validator.workspace = true +webauthn-authenticator-rs = { version = "0.5.0", features = ["mozilla", "nfc", "vendor-yubikey"] } [dev-dependencies] fake = { version = "2" } diff --git a/crates/idp/src/main.rs b/crates/idp/src/main.rs index 1ba94c0..db04160 100644 --- a/crates/idp/src/main.rs +++ b/crates/idp/src/main.rs @@ -6,6 +6,7 @@ pub mod actions; pub mod db; // pub mod idp; pub mod mqtt; +mod rauthy; pub mod rpc; pub type Result = std::result::Result; @@ -33,12 +34,17 @@ async fn main() { dotenv::dotenv().ok(); config::init_tracing("account-manager"); + let http_client = reqwest::Client::new(); + let opts = Opts {}; let config = config::default_load(&opts); let db = db::Database::build(config.clone()).await; + let rauthy_client = + rauthy::RauthyClient::new(config.lock().idp().idm_url.clone(), http_client.clone()); + let mqtt_client = mqtt::start(config.clone(), db.clone()).await; rpc::start(config.clone(), db.clone(), mqtt_client.clone()).await; } diff --git a/crates/idp/src/rauthy/api_types.rs b/crates/idp/src/rauthy/api_types.rs new file mode 100644 index 0000000..82081f2 --- /dev/null +++ b/crates/idp/src/rauthy/api_types.rs @@ -0,0 +1,1489 @@ +use serde::{Deserialize, Serialize}; + +use super::newtypes::*; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum AccessGroup { + Blacklist, + Clients, + Events, + Generic, + Groups, + Roles, + Secrets, + Sessions, + Scopes, + UserAttributes, + Users, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AccessRights { + Read, + Create, + Update, + Delete, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApiKeyAccess { + pub group: AccessGroup, + pub access_rights: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApiKeyRequest { + /// Validation: `^[a-zA-Z0-9_-/]{2,24}$` + pub name: String, + // TODO max validation for inner i64 is broken in the macro in v0.18.1 + /// Unix timestamp + pub exp: Option, + pub access: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApiKeysResponse { + pub keys: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApiKeyResponse { + pub name: String, + /// unix timestamp + pub created: i64, + /// unix timestamp + pub expires: Option, + pub access: Vec, +} + +// AUTH PROVIDERS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AuthProviderType { + Custom, + Github, + Google, + OIDC, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderRequest { + /// Validation: `[a-zA-Z0-9À-ÿ-\s]{2,128}]` + pub name: String, + pub typ: AuthProviderType, + pub enabled: bool, + + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub issuer: String, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub authorization_endpoint: String, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub token_endpoint: String, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub userinfo_endpoint: String, + + pub danger_allow_insecure: Option, + pub use_pkce: bool, + + // This validation is pretty loose, but if we make it too strict, + // we will most probably get into compatibility issues. + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub client_id: String, + /// Validation: max length is 256 + pub client_secret: Option, + /// Validation: `[a-z0-9-_/:\s*]{0,512}` + pub scope: String, + /// Validation: `(-----BEGIN CERTIFICATE-----)[a-zA-Z0-9+/=\n]+(-----END + /// CERTIFICATE-----)` + pub root_pem: Option, + + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub admin_claim_path: Option, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub admin_claim_value: Option, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub mfa_claim_path: Option, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub mfa_claim_value: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderCallbackRequest { + /// Validation: `[a-zA-Z0-9]` + pub state: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub code: String, + /// Validation: `[a-zA-Z0-9]` + pub xsrf_token: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub pkce_verifier: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderLoginRequest { + // values for the downstream client + /// Validation: `email` + pub email: Option, + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub redirect_uri: String, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub scopes: Option>, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub state: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub nonce: Option, + /// Validation: `[a-zA-Z0-9-._~]{43,128}` + pub code_challenge: Option, + /// Validation: `[a-zA-Z0-9]` + pub code_challenge_method: Option, + + // values for the callback from upstream + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub provider_id: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub pkce_challenge: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderLookupRequest { + /// Validation: `[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]` + pub issuer: Option, + /// Validation: `[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]` + pub metadata_url: Option, + pub danger_allow_insecure: Option, + // no validation since it will throw an error later if not correctly formed + pub root_pem: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderResponse { + pub id: String, + pub name: String, + pub typ: AuthProviderType, + pub enabled: bool, + + pub issuer: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub userinfo_endpoint: String, + + pub client_id: String, + pub client_secret: Option, + pub scope: String, + + pub admin_claim_path: Option, + pub admin_claim_value: Option, + pub mfa_claim_path: Option, + pub mfa_claim_value: Option, + + pub danger_allow_insecure: bool, + pub use_pkce: bool, + + pub root_pem: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderLinkedUserResponse { + pub id: String, + pub email: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProviderLookupResponse { + pub issuer: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub userinfo_endpoint: String, + pub scope: String, + pub root_pem: &'a Option, + pub use_pkce: bool, + pub danger_allow_insecure: bool, +} + +// BLACKLISTS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct IpBlacklistRequest { + /// Validation: Ipv4Addr + pub ip: Ipv4Addr, + // TODO max validation for inner i64 is broken in the macro in v0.18.1 + /// Unix timestamp + pub exp: i64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BlacklistResponse { + pub ips: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BlacklistedIp { + pub ip: String, + pub exp: i64, +} + +// CLIENTS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ColorsRequest { + pub act1: String, + + pub act1a: String, + + pub act2: String, + + pub act2a: String, + + pub acnt: String, + + pub acnta: String, + + pub ok: String, + + pub err: String, + + pub glow: String, + + pub gmid: String, + + pub ghigh: String, + + pub text: String, + + pub bg: String, +} + +impl ColorsRequest { + pub fn validate_css(&self) -> Result<(), ErrorResponse> { + Srgb::from_str(&self.act1)?; + Srgb::from_str(&self.act1a)?; + Srgb::from_str(&self.act2)?; + Srgb::from_str(&self.act2a)?; + Srgb::from_str(&self.acnt)?; + Srgb::from_str(&self.acnta)?; + Srgb::from_str(&self.ok)?; + Srgb::from_str(&self.err)?; + Srgb::from_str(&self.glow)?; + Srgb::from_str(&self.gmid)?; + Srgb::from_str(&self.ghigh)?; + Srgb::from_str(&self.text)?; + Srgb::from_str(&self.bg)?; + Ok(()) + } +} + +// https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DynamicClientRequest { + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub redirect_uris: Vec, + /// Validation: + /// `Vec<^(authorization_code|client_credentials|password|refresh_token)$>` + pub grant_types: Vec, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub client_name: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub client_uri: Option, + /// Validation: `Vec<^[a-zA-Z0-9\+.@/]{0,48}$>` + pub contacts: Option>, + /// Validation: `^(RS256|RS384|RS512|EdDSA)$` + pub id_token_signed_response_alg: Option, + /// Validation: `^(client_secret_post|client_secret_basic|none)$` + pub token_endpoint_auth_method: Option, + /// Validation: `^(RS256|RS384|RS512|EdDSA)$` + pub token_endpoint_auth_signing_alg: Option, + // Rauthy will only accept the following defaults + // `response_type=code` + // `subject_type=public` + // `require_auth_time=true` (always added by Rauthy anyway) + // + // The following must never be accepted for security reasons, + // because the registration may be unauthenticated: + // - logo_uri + // - client_uri + // - policy_uri + // - tos_uri + // + // Unsupported values: + // - application_type (may come in the future) + // - contacts (may come in the future) + // - jwks_uri + // - jwks + // - sector_identifier_uri + // - id_token_encrypted_response_alg + // - id_token_encrypted_response_enc + // - userinfo_signed_response_alg + // - userinfo_encrypted_response_alg + // - userinfo_encrypted_response_enc + // - request_object_signing_alg + // - request_object_encryption_alg + // - request_object_encryption_enc + // - default_max_age (can be specified during auth init with `max_age`) + // - default_acr_values + // - initiate_login_uri + // - request_uris (may come in the future with `request_uri` during login) + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub post_logout_redirect_uri: Option, +} + +/// This request is used for ephemeral clients, which are needed for Solid OIDC +/// for instance. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EphemeralClientRequest { + /// Validation: `^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]{2,256}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub client_name: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub client_uri: Option, + /// Validation: `Vec<^[a-zA-Z0-9\+.@/]{0,48}$>` + pub contacts: Option>, + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub redirect_uris: Vec, + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub post_logout_redirect_uris: Option>, + /// Validation: + /// `Vec<^(authorization_code|client_credentials|password|refresh_token)$>` + pub grant_types: Option>, + /// Validation: `60 <= access_token_lifetime <= 86400` + pub default_max_age: Option, + /// Validation: `[a-z0-9-_/:\s*]{0,512}` + pub scope: Option, + pub require_auth_time: Option, + + /// Validation: `^(RS256|RS384|RS512|EdDSA)$` + pub access_token_signed_response_alg: Option, + /// Validation: `^(RS256|RS384|RS512|EdDSA)$` + pub id_token_signed_response_alg: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewClientRequest { + /// Validation: `^[a-z0-9-_/]{2,128}$` + pub id: String, + /// Validation: None - will not be deserialized + #[serde(skip_deserializing)] + pub secret: Option>, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub name: Option, + /// Validation: bool + pub confidential: bool, + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub redirect_uris: Vec, + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub post_logout_redirect_uris: Option>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UpdateClientRequest { + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,256}$` + pub id: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub name: Option, + pub confidential: bool, + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub redirect_uris: Vec, + /// Validation: `Vec<^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$>` + pub post_logout_redirect_uris: Option>, + /// Validation: `Vec<^(http|https)://[a-z0-9.:-]+$>` + pub allowed_origins: Option>, + pub enabled: bool, + /// Validation: + /// `Vec<^(authorization_code|client_credentials|password|refresh_token)$>` + pub flows_enabled: Vec, + /// Validation: `^(RS256|RS384|RS512|EdDSA)$` + pub access_token_alg: JwkKeyPairAlg, + /// Validation: `^(RS256|RS384|RS512|EdDSA)$` + pub id_token_alg: JwkKeyPairAlg, + /// Validation: `10 <= auth_code_lifetime <= 300` + pub auth_code_lifetime: i32, + /// Validation: `10 <= access_token_lifetime <= 86400` + pub access_token_lifetime: i32, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub scopes: Vec, + /// Validation: `Vec<^[a-z0-9-_/:\s]{0,64}$>` + pub default_scopes: Vec, + /// Validation: `Vec<^(plain|S256)$>` + pub challenges: Option>, + pub force_mfa: bool, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub client_uri: Option, + /// Validation: `Vec<^[a-zA-Z0-9\+.@/]{0,48}$>` + pub contacts: Option>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ClientResponse { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub enabled: bool, + pub confidential: bool, + pub redirect_uris: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub post_logout_redirect_uris: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_origins: Option>, + pub flows_enabled: Vec, + pub access_token_alg: JwkKeyPairAlg, + pub id_token_alg: JwkKeyPairAlg, + pub auth_code_lifetime: i32, + pub access_token_lifetime: i32, + pub scopes: Vec, + pub default_scopes: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub challenges: Option>, + pub force_mfa: bool, + pub client_uri: Option, + pub contacts: Option>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ClientSecretResponse { + pub id: String, + pub confidential: bool, + pub secret: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DynamicClientResponse { + pub client_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub contacts: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub client_secret: Option, + // TODO can we "trust" in a client doing a PUT on Self before en expiry to + // implement proper forced secret rotation from time to time? -> not mentioned in RFC + pub client_secret_expires_at: i64, + + pub redirect_uris: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub post_logout_redirect_uri: Option, + + // only Some(_) after new token has been issued + // TODO rotate on PUT + #[serde(skip_serializing_if = "Option::is_none")] + pub registration_access_token: Option, + // This is the uri for PUT requests from Self -> only provide if `registration_access_token` + // has been updated as well + #[serde(skip_serializing_if = "Option::is_none")] + pub registration_client_uri: Option, + + pub grant_types: Vec, + pub id_token_signed_response_alg: String, + pub token_endpoint_auth_method: String, + pub token_endpoint_auth_signing_alg: String, +} + +// EVENTS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum EventLevel { + Info, + Notice, + Warning, + Critical, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum EventType { + InvalidLogins, + IpBlacklisted, + IpBlacklistRemoved, + JwksRotated, + NewUserRegistered, + NewRauthyAdmin, + NewRauthyVersion, + PossibleBruteForce, + RauthyStarted, + RauthyHealthy, + RauthyUnhealthy, + SecretsMigrated, + UserEmailChange, + UserPasswordReset, + Test, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EventsListenParams { + /// Validation: `0 <= latest <= 1000` + pub latest: Option, + pub level: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EventsRequest { + /// Unix timestamp in seconds + pub from: i64, + /// Unix timestamp in seconds + pub until: Option, + pub level: EventLevel, + pub typ: Option, +} + +// FED CM + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FedCMAssertionRequest { + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub nonce: Option, + /// Validation: `[a-zA-Z0-9]` + pub account_id: String, + /// Whether the user agent has explicitly shown to the user what specific + /// information the IDP intends to share with the RP (e.g. "idp.example + /// will share your name, email... with rp.example"), used by the + /// request permission to sign-up algorithm for new users. It is used as + /// an assurance by the user agent to the IDP that it has indeed shown the + /// terms of service and privacy policy to the user in the cases where + /// it is required to do so. + pub disclosure_text_shown: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FedCMClientMetadataRequest { + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: String, +} + +// GENERIC + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EncKeyMigrateRequest { + pub key_id: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct I18nRequest { + pub content: I18nContent, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum I18nContent { + Account, + Authorize, + Device, + EmailChangeConfirm, + Error, + Index, + Logout, + PasswordReset, + Register, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Language { + En, + De, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PaginationParams { + pub page_size: Option, + pub offset: Option, + pub backwards: Option, + /// Validation: `[a-zA-Z0-9]` + pub continuation_token: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PasswordHashTimesRequest { + pub target_time: u32, + + pub m_cost: Option, + + pub p_cost: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PasswordPolicyRequest { + /// Validation: `8 <= length_min <= 128` + pub length_min: i32, + /// Validation: `8 <= length_max <= 128` + pub length_max: i32, + /// Validation: `1 <= include_lower_case <= 32` + pub include_lower_case: Option, + /// Validation: `1 <= include_upper_case <= 32` + pub include_upper_case: Option, + /// Validation: `1 <= include_digits <= 32` + pub include_digits: Option, + /// Validation: `1 <= include_special <= 32` + pub include_special: Option, + /// Validation: `1 <= valid_days <= 3650` + pub valid_days: Option, + /// Validation: `1 <= not_recently_used <= 10` + pub not_recently_used: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SearchParams { + /// Data type + pub ty: SearchParamsType, + /// Index + pub idx: SearchParamsIdx, + /// The actual search query - validation: + /// `[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%@]+` + pub q: String, + pub limit: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SearchParamsIdx { + // user params + Id, + Email, + // session params + UserId, + SessionId, + Ip, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SearchParamsType { + User, + Session, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AppVersionResponse { + pub current: String, + pub last_check: Option, + pub latest: Option, + pub latest_url: Option, + pub update_available: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Argon2ParamsResponse { + pub m_cost: u32, + pub t_cost: u32, + pub p_cost: u32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EncKeysResponse { + pub active: String, + pub keys: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct HealthResponse { + pub is_db_alive: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_health: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_connected_hosts: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LoginTimeResponse { + pub argon2_params: Argon2ParamsResponse, + pub login_time: u32, + pub num_cpus: usize, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PasswordPolicyResponse { + pub length_min: i32, + pub length_max: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_lower_case: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_upper_case: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_special: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub valid_days: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub not_recently_used: Option, +} + +// GROUPS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewGroupRequest { + /// Validation: `^[a-z0-9-_/,:*]{2,64}$` + pub group: String, +} + +// OIDC + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AddressClaim { + pub formatted: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub street_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub locality: Option, + // pub region: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub postal_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AuthRequest { + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub redirect_uri: String, + /// Validation: `[a-z0-9-_/]{2,128}` + pub response_type: String, + /// Validation: `[a-z0-9-_/:\s*]{0,512}` + + #[serde(default = "default_scope")] + pub scope: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub state: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub code_challenge: Option, + /// Validation: `[a-zA-Z0-9]` + pub code_challenge_method: Option, + + pub max_age: Option, + /// Validation: `[a-zA-Z0-9]` + pub prompt: Option, +} + +fn default_scope() -> String { + String::from("openid") +} + +// TODO is this not being used anymore? -> check! +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AuthCodeRequest { + /// Validation: `^[a-z0-9-_/]{2,128}$` + pub grant_type: String, + /// Validation: `[a-zA-Z0-9]` + pub code: String, + /// Validation: `^[a-z0-9-_/]{2,128}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9]` + pub client_secret: Option, + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$` + pub redirect_uri: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CsrfTokenResponse { + pub csrf_token: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DeviceAcceptedRequest { + Accept, + Decline, + Pending, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LoginRequest { + /// Validation: `email` + pub email: String, + /// Validation: Applies password policy - max 256 characters + pub password: Option, + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub redirect_uri: String, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub scopes: Option>, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub state: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub nonce: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub code_challenge: Option, + /// Validation: `[a-zA-Z0-9]` + pub code_challenge_method: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LoginRefreshRequest { + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: String, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub redirect_uri: String, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub scopes: Option>, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub state: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub nonce: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub code_challenge: Option, + /// Validation: `[a-zA-Z0-9]` + pub code_challenge_method: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LogoutRequest { + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub id_token_hint: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub post_logout_redirect_uri: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub state: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceGrantRequest { + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$` + pub client_id: String, + /// Validation: max length is 256 + pub client_secret: Option, + /// Validation: `[a-z0-9-_/:\s*]{0,512}` + pub scope: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceVerifyRequest { + /// Validation: `[a-zA-Z0-9]` + pub user_code: String, + /// Validation: `[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]+` + pub pow: String, + /// If `DeviceAcceptedRequest::Pending`, information about the request will + /// be returned. If `DeviceAcceptedRequest::Accept` - the device will + /// get a Token Set If `DeviceAcceptedRequest::Decline` - the code + /// request will be deleted and rejected + pub device_accepted: DeviceAcceptedRequest, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TokenRequest { + /// Validation: + /// `^(authorization_code|client_credentials|urn:ietf:params:oauth: + /// grant-type:device_code|password|refresh_token)$` + pub grant_type: String, + /// Validation: `[a-zA-Z0-9]` + pub code: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub redirect_uri: Option, + /// Validation: `^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]{2,128}$` + pub client_id: Option, + /// Validation: `[a-zA-Z0-9]` + pub client_secret: Option, + /// Validation: `[a-zA-Z0-9-\\._~+/=]+` + pub code_verifier: Option, + /// Validation: max length is 256 + pub device_code: Option, + /// Validation: `email` + pub username: Option, + /// max 256 characters + pub password: Option, + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub refresh_token: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TokenValidationRequest { + /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` + pub token: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceCodeResponse { + pub device_code: String, + pub user_code: String, + pub verification_uri: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub verification_uri_complete: Option, + pub expires_in: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub interval: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceVerifyResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub scopes: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct JktClaim { + pub jkt: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum JwkKeyPairAlg { + RS256, + RS384, + RS512, + EdDSA, +} + +impl Default for JwkKeyPairAlg { + fn default() -> Self { + Self::EdDSA + } +} + +impl Display for JwkKeyPairAlg { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + JwkKeyPairAlg::RS256 => "RS256", + JwkKeyPairAlg::RS384 => "RS384", + JwkKeyPairAlg::RS512 => "RS512", + JwkKeyPairAlg::EdDSA => "EdDSA", + }; + write!(f, "{}", s) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum JwkKeyPairType { + RSA, + OKP, +} + +impl Default for JwkKeyPairType { + fn default() -> Self { + Self::OKP + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct JWKSPublicKeyCerts { + pub kty: JwkKeyPairType, + pub alg: JwkKeyPairAlg, + #[serde(skip_serializing_if = "Option::is_none")] + pub crv: Option, // Ed25519 + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub n: Option, // RSA + #[serde(skip_serializing_if = "Option::is_none")] + pub e: Option, // RSA + #[serde(skip_serializing_if = "Option::is_none")] + pub x: Option, // OCT +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct JWKSCerts { + pub keys: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct OAuth2ErrorResponse { + pub error: OAuth2ErrorTypeResponse, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_description: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuth2ErrorTypeResponse { + InvalidRequest, + InvalidClient, + InvalidGrant, + UnauthorizedClient, + UnsupportedGrantType, + InvalidScope, + // specific to the device grant + AuthorizationPending, + SlowDown, + AccessDenied, + ExpiredToken, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SessionInfoResponse { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub csrf_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub roles: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub groups: Option, + /// format: `OffsetDateTime` + + #[serde(with = "time::serde::rfc3339")] + pub exp: OffsetDateTime, + /// format: `OffsetDateTime` + + #[serde(with = "time::serde::rfc3339")] + pub timeout: OffsetDateTime, + pub state: SessionState, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TokenInfo { + pub active: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cnf: Option, +} + +// ROLES + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewRoleRequest { + /// Validation: `^[a-z0-9-_/,:*]{2,64}$` + pub role: String, +} + +// SCOPES + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ScopeRequest { + /// Validation: `^[a-z0-9-_/,:*]{2,64}$` + pub scope: String, + /// Validation: `^[a-zA-Z0-9-_/]{2,128}$` + pub attr_include_access: Option>, + /// Validation: `^[a-zA-Z0-9-_/]{2,128}$` + pub attr_include_id: Option>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ScopeResponse { + pub id: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub attr_include_access: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub attr_include_id: Option>, +} + +// SESSIONS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SessionState { + Open, + Init, + Auth, + LoggedOut, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SessionResponse { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + pub is_mfa: bool, + pub state: SessionState, + pub exp: i64, + pub last_seen: i64, + pub remote_ip: Option, +} + +// USERS + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceRequest { + /// Validation: `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$` + pub device_id: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub name: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MfaAwaitRequest { + /// Validation: `^[a-zA-Z0-9]{48}$` + pub code: String, + /// Validation: `^[a-zA-Z0-9]{12}$` + pub req_id: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum MfaPurpose { + Login(String), + PasswordNew, + PasswordReset, + Test, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewUserRequest { + /// Validation: `email` + pub email: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub family_name: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub given_name: String, + pub language: Language, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub groups: Option>, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub roles: Vec, + // TODO max validation for inner i64 is broken in the macro in v0.18.1 + /// Unix timestamp + pub user_expires: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewUserRegistrationRequest { + pub email: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub family_name: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub given_name: String, + /// Validation: `[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]+` + pub pow: String, + /// Validation: `[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]+` + pub redirect_uri: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PasskeyRequest { + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PasswordResetRequest { + /// Validation: `[a-zA-Z0-9]{64}` + pub magic_link_id: String, + /// Validation: Applies password policy - max 256 characters + pub password: Pass, + /// Validation: `[a-zA-Z0-9]{48}` + pub mfa_code: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RequestResetRequest { + /// Validation: `email` + pub email: String, + /// Redirect URI used after a successful reset - validation: + /// `[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]` + pub redirect_uri: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UpdateUserRequest { + /// Validation: `email` + pub email: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub given_name: String, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub family_name: String, + pub language: Option, + /// Validation: Applies password policy - max 256 characters + pub password: Option, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub roles: Vec, + /// Validation: `Vec<^[a-z0-9-_/,:*]{2,64}$>` + pub groups: Option>, + pub enabled: bool, + pub email_verified: bool, + // TODO max validation for inner i64 is broken in the macro in v0.18.1 + /// Unix timestamp + pub user_expires: Option, + + pub user_values: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UpdateUserSelfRequest { + /// Validation: `email` + pub email: Option, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub given_name: Option, + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}` + pub family_name: Option, + pub language: Option, + pub password_current: Option, + pub mfa_code: Option, + /// Validation: Applies password policy + pub password_new: Option, + + pub user_values: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserValuesRequest { + /// Validation: `[0-9]{4}-[0-9]{2}-[0-9]{2}` + pub birthdate: Option, + /// Validation: `+[0-9]{0,32}` + pub phone: Option, + /// Validation: `[a-zA-Z0-9À-ÿ-.\s]{0,48}` + pub street: Option, + + pub zip: Option, + /// Validation: `[a-zA-Z0-9À-ÿ-]{0,48}` + pub city: Option, + /// Validation: `[a-zA-Z0-9À-ÿ-]{0,48}` + pub country: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrConfigRequest { + /// Validation: `^[a-zA-Z0-9-_/]{2,32}$` + pub name: String, + /// Validation: `^[a-zA-Z0-9-_/]{0,128}$` + pub desc: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrValueRequest { + /// Validation: `^[a-zA-Z0-9-_/]{2,32}$` + pub key: String, + pub value: serde_json::Value, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrValuesUpdateRequest { + pub values: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnAuthStartRequest { + pub purpose: MfaPurpose, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnAuthFinishRequest { + /// Validation: `[a-zA-Z0-9]{48}` + pub code: String, + /// Note: `ToSchema` does currently not exist for + /// `webauthn_rs::prelude::PublicKeyCredential` + pub data: webauthn_rs::prelude::PublicKeyCredential, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnRegStartRequest { + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub passkey_name: String, + + /// Validation: `email` + pub email: Option, + /// Validation: `[a-zA-Z0-9]{64}` + pub magic_link_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnRegFinishRequest { + /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{1,32}` + pub passkey_name: String, + /// Note: `ToSchema` does currently not exist for + /// `webauthn_rs::prelude::PublicKeyCredential` + pub data: webauthn_rs::prelude::RegisterPublicKeyCredential, + /// Validation: `[a-zA-Z0-9]{64}` + pub magic_link_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebIdRequest { + pub custom_triples: Option, + pub expose_email: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceResponse { + pub id: String, + pub client_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + pub created: i64, + pub access_exp: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_exp: Option, + pub peer_ip: String, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PasskeyResponse { + pub name: String, + /// format: `NaiveDateTime` + pub registered: i64, + /// format: `NaiveDateTime` + pub last_used: i64, + pub user_verified: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrConfigValueResponse { + pub name: String, + pub desc: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrConfigResponse { + pub values: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrValueResponse { + pub key: String, + pub value: serde_json::Value, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttrValuesResponse { + pub values: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Userinfo { + pub id: String, + pub sub: String, + pub name: String, + pub roles: Vec, + pub mfa_enabled: bool, + + // scope: address + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + + // scope: email + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub email_verified: Option, + + // scope: groups + #[serde(skip_serializing_if = "Option::is_none")] + pub groups: Option>, + + // scope: profile + #[serde(skip_serializing_if = "Option::is_none")] + pub preferred_username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub given_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub family_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub birthdate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + + // scope: phone + #[serde(skip_serializing_if = "Option::is_none")] + pub phone: Option, + + // scope: webid + #[serde(skip_serializing_if = "Option::is_none")] + pub webid: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UserAccountTypeResponse { + New, + Password, + Passkey, + Federated, + FederatedPasskey, + FederatedPassword, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserResponse { + pub id: String, + pub email: String, + pub given_name: String, + pub family_name: String, + pub language: Language, + pub roles: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub groups: Option>, + pub enabled: bool, + pub email_verified: bool, + /// format: `NaiveDateTime` + #[serde(skip_serializing_if = "Option::is_none")] + pub password_expires: Option, + /// format: `NaiveDateTime` + pub created_at: i64, + /// format: `NaiveDateTime` + #[serde(skip_serializing_if = "Option::is_none")] + pub last_login: Option, + /// format: `NaiveDateTime` + #[serde(skip_serializing_if = "Option::is_none")] + pub last_failed_login: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub failed_login_attempts: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_expires: Option, + pub account_type: UserAccountTypeResponse, + pub webauthn_user_id: Option, /* TODO get rid of the webauthn user id ? Not needed + * at all? */ + pub user_values: UserValuesResponse, + pub auth_provider_id: Option, + pub federation_uid: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserResponseSimple { + pub id: String, + pub email: String, + pub created_at: i64, + pub last_login: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserValuesResponse { + pub birthdate: Option, + pub phone: Option, + pub street: Option, + pub zip: Option, + pub city: Option, + pub country: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebId { + pub user_id: String, + pub expose_email: bool, + pub custom_triples: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebIdResponse { + pub webid: WebId, + pub issuer: String, + pub email: String, + pub given_name: String, + pub family_name: String, + pub language: Language, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnAuthStartResponse { + pub code: String, + pub rcr: webauthn_rs::prelude::RequestChallengeResponse, + pub user_id: String, + pub exp: u64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnLoginFinishResponse { + pub loc: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WebauthnLoginResponse { + pub code: String, + pub user_id: String, + pub exp: u64, +} diff --git a/crates/idp/src/rauthy/mod.rs b/crates/idp/src/rauthy/mod.rs new file mode 100644 index 0000000..167c09a --- /dev/null +++ b/crates/idp/src/rauthy/mod.rs @@ -0,0 +1,362 @@ +pub use api_types::*; +pub use newtypes::*; +use nutype::nutype; +pub use rauthy_client::principal::Userinfo; +pub use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use webauthn_authenticator_rs::prelude::CreationChallengeResponse; + +pub mod api_types; +pub mod newtypes; + +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +pub struct UserValuesPayload { + pub birthdate: String, + pub city: String, + pub country: String, + pub phone: String, + pub street: String, + pub zip: ZipCode, +} + +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Lang { + En, + De, +} + +pub struct Token(String); + +impl std::fmt::Debug for Token { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("<<>>") + } +} + +#[derive(Debug)] +pub struct RauthyClient { + base_url: String, + client: reqwest::Client, + token: Token, +} + +impl RauthyClient { + pub fn new(base_url: String, client: reqwest::Client) -> Self { + Self { + base_url, + client, + token: Token(std::env::var("RAUTHY_API_TOKEN").expect( + "RAUTHY_API_TOKEN is required for users and +sessions managmenet", + )), + } + } +} + +#[derive(derive_mode::Deref)] +pub struct Users<'client>(&'client RauthyClient); + +impl<'client> Users<'client> { + pub async fn list(&self) -> Result, reqwest::Error> { + let res = self + .client + .get(format!( + "{base_url}/auth/v1/users", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .send() + .await?; + res.json().await + } + + pub async fn create(&self, payload: NewUserRequest) -> Result { + let res = self + .client + .get(format!( + "{base_url}/auth/v1/register", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + res.json().await + } + + pub async fn update( + &self, + user_id: UserId, + payload: UpdateUserRequest, + ) -> Result { + let res = self + .client + .put(format!( + "{base_url}/auth/v1/users/{user_id}", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + res.json().await + } + + pub async fn delete( + &self, + user_id: UserId, + payload: UpdateUserPayload, + ) -> Result { + let res = self + .client + .delete(format!( + "{base_url}/auth/v1/users/{user_id}", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + let status = res.status(); + res.text().await?; + Ok(status) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MfaError { + pub error: String, + pub message: String, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum MfaPurpose { + Login(String), + PasswordNew, + PasswordReset, + Test, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebauthnAuthStartRequest { + pub purpose: MfaPurpose, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebauthnAuthStartResponse { + pub code: String, + pub exp: i64, + pub rcr: String, + pub user_id: UserId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebauthnRegStartRequest { + pub email: String, + /** + * 64 long + */ + pub magic_link_id: String, + /** + * 1-32 long, UTF-8 text with white and - + */ + pub passkey_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebauthnAuthFinishRequest { + pub code: WebauthCode, + pub data: webauthn_rs::prelude::PublicKeyCredential, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebauthnLoginReq { + pub code: WebauthCode, + pub header_loc: String, + pub header_origin: String, + pub user_id: UserId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebauthnServiceReq { + pub code: WebauthCode, + pub user_id: UserId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum WebauthnAdditionalData { + Login(WebauthnLoginReq), + Service(WebauthnServiceReq), + Test, +} + +#[derive(derive_mode::Deref)] +pub struct Mfa<'client>(&'client RauthyClient); + +impl<'client> Mfa<'client> { + pub async fn start( + &self, + user_id: UserId, + payload: WebauthnAuthStartRequest, + ) -> Result { + let res = self + .client + .post(format!( + "{base_url}/auth/v1/users/{user_id}/webauthn/auth/start", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + res.json().await + } + + pub async fn finish( + &self, + user_id: UserId, + payload: WebauthnAuthFinishRequest, + ) -> Result { + let res = self + .client + .post(format!( + "{base_url}/auth/v1/users/{user_id}/webauthn/auth/finish", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + res.json().await + } + + pub async fn delete(&self, user_id: UserId, name: String) -> Result { + let res = self + .client + .delete(format!( + "{base_url}/auth/v1/users/{user_id}/webauthn/delete/{name}", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + let status = res.status(); + Ok(status) + } + + pub async fn register_start( + &self, + user_id: UserId, + payload: WebauthnRegStartRequest, + ) -> Result { + let res = self + .client + .post(format!( + "{base_url}/auth/v1/users/{user_id}/webauthn/register/start", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + let txt = res.text().await?; + tracing::debug!("Creation txt is: {txt:?}"); + let t = serde_json::from_str(&txt)?; + Ok(t) + } +} + +#[nutype(derive(Serialize, Deserialize, Debug, Display, Clone, Deref, PartialEq))] +pub struct SessionResponseId(String); + +#[nutype(derive(Serialize, Deserialize, Debug, Display, Clone, Deref, PartialEq))] +pub struct RemoteIp(String); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SessionState { + Open, + Init, + Auth, + LoggedOut, + Unknown, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SessionResponse<'a> { + pub id: SessionResponseId, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + pub is_mfa: bool, + pub state: SessionState, + pub exp: i64, + pub last_seen: i64, + pub remote_ip: Option, +} + +#[derive(derive_mode::Deref)] +pub struct Sessions<'client>(&'client RauthyClient); + +impl<'client> Sessions<'client> { + pub async fn list( + &self, + page_size: Option, + offset: Option, + backwards: Option, + continuation_token: Option, + ) -> Result { + let res = self + .client + .post(format!( + "{base_url}/auth/v1/users/{user_id}/webauthn/register/start", + base_url = self.base_url + )) + .header("accept", "application/json") + .header( + "Authorization", + &format!("API-Key {token}", token = self.token.0), + ) + .json(&payload) + .send() + .await?; + let txt = res.text().await?; + tracing::debug!("Creation txt is: {txt:?}"); + let t = serde_json::from_str(&txt)?; + Ok(t) + } +} diff --git a/crates/idp/src/rauthy/newtypes.rs b/crates/idp/src/rauthy/newtypes.rs new file mode 100644 index 0000000..89ba00c --- /dev/null +++ b/crates/idp/src/rauthy/newtypes.rs @@ -0,0 +1,26 @@ +use nutype::nutype; +use serde::{Deserialize, Serialize}; + +#[nutype(derive(Serialize, Deserialize, Debug, Display, Clone, Deref, PartialEq))] +pub struct UserId(String); + +#[nutype(derive(Serialize, Deserialize, Debug, Display, Copy, Clone, Deref, PartialEq))] +pub struct UserExpires(i64); + +#[nutype(derive(Serialize, Deserialize, Debug, Display, Copy, Clone, Deref, PartialEq))] +pub struct ZipCode(i64); + +#[nutype(derive(Serialize, Deserialize, Clone, PartialEq))] +pub struct Pass(String); + +impl std::fmt::Debug for Pass { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("<<>>") + } +} + +/** + * ASCII alphanum 48 long + */ +#[nutype(derive(Serialize, Deserialize, Clone, PartialEq))] +pub struct WebauthCode(String);