From ebdb57bbe75c5cb2c7d4b9dca677906b36e4874f Mon Sep 17 00:00:00 2001 From: Firstyear Date: Sat, 26 Dec 2020 13:58:32 +1000 Subject: [PATCH] WIP - Improve Auth Proto to Support Webauthn (#333) This is a rewrite of the "on the wire" json for auth. This is a breaking change required to allow webauthn to work given limitations within Webauthn as a standard and how mixed credentials are challenged for. --- Cargo.lock | 355 +++--- designs/auth_proto_rewrite_late_2020.rst | 124 +- kanidm_client/src/asynchronous.rs | 61 +- kanidm_client/src/lib.rs | 145 ++- kanidm_proto/src/v1.rs | 71 +- kanidm_rlm_python/README.md | 27 + kanidm_rlm_python/kanidmradius.py | 19 +- kanidm_rlm_python/test_data/config.ini | 2 +- kanidmd/src/lib/be/dbvalue.rs | 20 + kanidmd/src/lib/core/https.rs | 36 +- kanidmd/src/lib/credential/mod.rs | 345 ++++-- kanidmd/src/lib/event.rs | 53 +- kanidmd/src/lib/idm/account.rs | 16 +- kanidmd/src/lib/idm/authsession.rs | 1288 +++++++++----------- kanidmd/src/lib/idm/mod.rs | 7 +- kanidmd/src/lib/idm/server.rs | 218 +++- kanidmd/src/lib/idm/unix.rs | 57 +- kanidmd/src/lib/plugins/password_import.rs | 11 +- kanidmd/src/lib/server.rs | 2 +- 19 files changed, 1628 insertions(+), 1229 deletions(-) create mode 100644 kanidm_rlm_python/README.md diff --git a/Cargo.lock b/Cargo.lock index 9d3b683a2..c36a7cf6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,9 +62,9 @@ checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" [[package]] name = "ahash" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6789e291be47ace86a60303502173d84af8327e3627ecf334356ee0f87a164c" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" dependencies = [ "const-random", ] @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.34" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" +checksum = "68803225a7b13e47191bab76f2687382b60d259e8cf37f6e1893658b84bb9479" [[package]] name = "arrayref" @@ -146,10 +146,12 @@ dependencies = [ [[package]] name = "async-h1" -version = "2.1.4" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9a5f3dbb5065856974e08c2ac24e6f81da6e39d2328de1c03a9a2b34ffb01" +checksum = "e5c68a75f812ff0f299e142c06dd0c34e3295a594d935e61eeb6c77041d1d4dc" dependencies = [ + "async-channel", + "async-dup", "async-std", "byte-pool", "futures-core", @@ -157,7 +159,7 @@ dependencies = [ "httparse", "lazy_static", "log", - "pin-project-lite 0.1.11", + "pin-project 1.0.2", ] [[package]] @@ -189,6 +191,22 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-process" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8cea09c1fb10a317d1b5af8024eeba256d6554763e85ecd90ff8df31c7bbda" +dependencies = [ + "async-io", + "blocking", + "cfg-if 0.1.10", + "event-listener", + "futures-lite", + "once_cell", + "signal-hook", + "winapi 0.3.9", +] + [[package]] name = "async-session" version = "2.0.1" @@ -226,13 +244,15 @@ dependencies = [ [[package]] name = "async-std" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e82538bc65a25dbdff70e4c5439d52f068048ab97cdea0acd73f131594caa1" +checksum = "8f9f84f1280a2b436a2c77c2582602732b6c2f4321d5494d6e799e6c367859a8" dependencies = [ + "async-channel", "async-global-executor", "async-io", "async-mutex", + "async-process", "blocking", "crossbeam-utils 0.8.1", "futures-channel", @@ -245,7 +265,7 @@ dependencies = [ "memchr", "num_cpus", "once_cell", - "pin-project-lite 0.1.11", + "pin-project-lite 0.2.0", "pin-utils", "slab", "wasm-bindgen-futures", @@ -259,9 +279,9 @@ checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" [[package]] name = "async-tls" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85a97c4a0ecce878efd3f945f119c78a646d8975340bca0398f9bb05c30cc52" +checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400" dependencies = [ "futures-core", "futures-io", @@ -366,9 +386,9 @@ dependencies = [ [[package]] name = "bit-vec" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0dc55f2d8a1a85650ac47858bb001b4c0dd73d79e3c455a842925e68d29cd3" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" @@ -376,17 +396,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - [[package]] name = "blake3" version = "0.3.7" @@ -515,9 +524,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" [[package]] name = "cfg-if" @@ -537,11 +546,13 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "js-sys", "libc", "num-integer", "num-traits", "serde", "time 0.1.44", + "wasm-bindgen", "winapi 0.3.9", ] @@ -565,22 +576,13 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "cloudabi" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" -dependencies = [ - "bitflags", -] - [[package]] name = "concread" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14fe52c39ed4e846fb3e6ad4bfe46224ef24db64ff7c5f496d2501c88c270b14" dependencies = [ - "ahash 0.4.6", + "ahash 0.4.7", "crossbeam", "crossbeam-epoch 0.8.2", "crossbeam-utils 0.7.2", @@ -599,21 +601,11 @@ dependencies = [ "cache-padded", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" -dependencies = [ - "cfg-if 0.1.10", - "wasm-bindgen", -] - [[package]] name = "const-random" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486d435a7351580347279f374cb8a3c16937485441db80181357b7c4d70f17ed" +checksum = "f590d95d011aa80b063ffe3253422ed5aa462af4e9867d43ce8337562bac77c4" dependencies = [ "const-random-macro", "proc-macro-hack", @@ -621,9 +613,9 @@ dependencies = [ [[package]] name = "const-random-macro" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a84d8ff70e3ec52311109b019c27672b4c1929e4cf7c18bcf0cd9fb5e230be" +checksum = "615f6e27d000a2bffbc7f2f6a8669179378fa27ee4d0a509e985dfc0a7defb40" dependencies = [ "getrandom 0.2.0", "lazy_static", @@ -633,9 +625,9 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" +checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826" [[package]] name = "constant_time_eq" @@ -698,6 +690,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + [[package]] name = "criterion" version = "0.3.3" @@ -709,7 +707,7 @@ dependencies = [ "clap", "criterion-plot", "csv", - "itertools 0.9.0", + "itertools", "lazy_static", "num-traits", "oorandom", @@ -731,7 +729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" dependencies = [ "cast", - "itertools 0.9.0", + "itertools", ] [[package]] @@ -1004,20 +1002,20 @@ dependencies = [ ] [[package]] -name = "dirs" -version = "2.0.2" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 0.1.10", - "dirs-sys", + "cfg-if 1.0.0", + "dirs-sys-next", ] [[package]] -name = "dirs-sys" -version = "0.3.5" +name = "dirs-sys-next" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "99de365f605554ae33f115102a02057d4fc18b01f3284d6870be0938743cfe7d" dependencies = [ "libc", "redox_users", @@ -1093,9 +1091,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fancy-regex" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae91abf6555234338687bb47913978d275539235fcb77ba9863b779090b42b14" +checksum = "36996e5f56f32ca51a937f325094fa450b32df871af1a89be331b7145b931bfc" dependencies = [ "bit-set", "regex", @@ -1129,7 +1127,7 @@ dependencies = [ [[package]] name = "fernet" version = "0.1.3" -source = "git+https://github.com/mozilla-services/fernet-rs.git#401fde478c63e868f126ff7c92abad1f96107ca4" +source = "git+https://github.com/mozilla-services/fernet-rs.git#ec7f9091e0761c0dfe92ad77321ca161b929dbe4" dependencies = [ "base64 0.12.3", "byteorder", @@ -1235,16 +1233,16 @@ checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" [[package]] name = "futures-lite" -version = "1.11.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6c079abfac3ab269e2927ec048dabc89d009ebfdda6b8ee86624f30c689658" +checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb" dependencies = [ "fastrand", "futures-core", "futures-io", "memchr", "parking", - "pin-project-lite 0.1.11", + "pin-project-lite 0.2.0", "waker-fn", ] @@ -1402,9 +1400,9 @@ checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "heck" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" dependencies = [ "unicode-segmentation", ] @@ -1450,9 +1448,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" dependencies = [ "bytes", "fnv", @@ -1482,9 +1480,9 @@ dependencies = [ [[package]] name = "http-types" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f316f6a06306570e899238d3b85375f350cfceda60ec47807c4164d6e169e58" +checksum = "f2ab8d0085fb82859c9adf050bd53992297ecdd03a665a230dfa50c8c964bf3d" dependencies = [ "anyhow", "async-channel", @@ -1520,7 +1518,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" dependencies = [ - "quick-error", + "quick-error 1.2.3", ] [[package]] @@ -1591,9 +1589,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg", "hashbrown 0.9.1", @@ -1629,15 +1627,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" -[[package]] -name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.9.0" @@ -1848,9 +1837,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" [[package]] name = "libnss" @@ -1979,9 +1968,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ "cfg-if 0.1.10", "fuchsia-zircon", @@ -2071,9 +2060,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cf75f38f16cb05ea017784dc6dbfd354f76c223dba37701734c4f5a9337d02" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2229,12 +2218,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.30" +version = "0.10.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" dependencies = [ "bitflags", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "foreign-types", "lazy_static", "libc", @@ -2249,9 +2238,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" -version = "0.9.58" +version = "0.9.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" dependencies = [ "autocfg", "cc", @@ -2289,12 +2278,11 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -2418,11 +2406,11 @@ dependencies = [ [[package]] name = "polyval" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3fd900a291ceb8b99799cc8cd3d1d3403a51721e015bc533528b2ceafcc443c" +checksum = "b4fd92d8e0c06d08525d2e2643cc2b5c80c69ae8eb12c18272d501cd7079ccc0" dependencies = [ - "cfg-if 1.0.0", + "cpuid-bool 0.2.0", "universal-hash", ] @@ -2497,10 +2485,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] -name = "quote" -version = "1.0.7" +name = "quick-error" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda" + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2", ] @@ -2606,7 +2600,6 @@ checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ "getrandom 0.1.15", "redox_syscall", - "rust-argon2", ] [[package]] @@ -2647,9 +2640,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.10.9" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb15d6255c792356a0f578d8a645c677904dc02e862bebe2ecc18e0c01b9a0ce" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" dependencies = [ "base64 0.13.0", "bytes", @@ -2680,16 +2673,15 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", "web-sys", "winreg", ] [[package]] name = "ring" -version = "0.16.18" +version = "0.16.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70017ed5c555d79ee3538fc63ca09c70ad8f317dcadc1adc2c496b60c22bb24f" +checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" dependencies = [ "cc", "libc", @@ -2738,18 +2730,6 @@ dependencies = [ "time 0.1.44", ] -[[package]] -name = "rust-argon2" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" -dependencies = [ - "base64 0.13.0", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils 0.8.1", -] - [[package]] name = "rustc_version" version = "0.2.3" @@ -2761,11 +2741,11 @@ dependencies = [ [[package]] name = "rustls" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" dependencies = [ - "base64 0.12.3", + "base64 0.13.0", "log", "ring", "sct", @@ -2806,12 +2786,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - [[package]] name = "scopeguard" version = "1.1.0" @@ -2868,9 +2842,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" dependencies = [ "serde_derive", ] @@ -2896,9 +2870,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ "proc-macro2", "quote", @@ -2907,9 +2881,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" dependencies = [ "itoa", "ryu", @@ -2918,9 +2892,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9408a61dabe404c76cec504ec510f7d92f41dc0a9362a0db8ab73d141cfbf93f" +checksum = "5af82de3c6549b001bec34961ff2d6a54339a87bab37ce901b693401f27de6cb" dependencies = [ "data-encoding", "percent-encoding", @@ -2966,18 +2940,28 @@ checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpuid-bool", + "cpuid-bool 0.1.2", "digest 0.9.0", "opaque-debug 0.3.0", ] [[package]] name = "shellexpand" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2b22262a9aaf9464d356f656fea420634f78c881c5eebd5ef5e66d8b9bc603" +checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" dependencies = [ - "dirs", + "dirs-next", +] + +[[package]] +name = "signal-hook" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" +dependencies = [ + "libc", + "signal-hook-registry", ] [[package]] @@ -3006,22 +2990,21 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" +checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", "winapi 0.3.9", ] @@ -3138,15 +3121,15 @@ dependencies = [ [[package]] name = "subtle" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343f3f510c2915908f155e94f17220b19ccfacf2a64a2a5d8004f2c3e311e7fd" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.53" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" +checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a" dependencies = [ "proc-macro2", "quote", @@ -3251,9 +3234,9 @@ dependencies = [ [[package]] name = "tide-rustls" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b34fba7cb60c3c465c82ff5d215f341774f8f11b0e45691a7134af4238e395a" +checksum = "8b2faeed43463ab96a5362256554787c10752f1173c9ffaf7b553842ef12b6c5" dependencies = [ "async-dup", "async-h1", @@ -3349,9 +3332,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" dependencies = [ "bytes", "fnv", @@ -3418,9 +3401,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] @@ -3570,9 +3553,9 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" [[package]] name = "vec-arena" @@ -3699,30 +3682,6 @@ version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" -[[package]] -name = "wasm-bindgen-test" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0355fa0c1f9b792a09b6dcb6a8be24d51e71e6d74972f9eb4a44c4c004d24a25" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "scoped-tls", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e07b46b98024c2ba2f9e83a10c2ef0515f057f2da299c1762a2017de80438b" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "web-sys" version = "0.3.46" @@ -3770,9 +3729,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", "untrusted", @@ -3780,9 +3739,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" dependencies = [ "webpki", ] @@ -3860,9 +3819,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f33972566adbd2d3588b0491eb94b98b43695c4ef897903470ede4f3f5a28a" +checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" dependencies = [ "zeroize_derive", ] @@ -3881,16 +3840,16 @@ dependencies = [ [[package]] name = "zxcvbn" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b69cd8a6484379ef04457ba1c00aaadad166c693b1b6a625b01bcc694b212b" +checksum = "a5f9db3a05b2ee81dcda4602487314ab654eca316f517be2e2e64175658f0dd0" dependencies = [ "chrono", "derive_builder", "fancy-regex", - "itertools 0.8.2", + "itertools", "lazy_static", - "quick-error", + "quick-error 2.0.0", "regex", "serde", "serde_derive", diff --git a/designs/auth_proto_rewrite_late_2020.rst b/designs/auth_proto_rewrite_late_2020.rst index 2fecf4505..aa794c987 100644 --- a/designs/auth_proto_rewrite_late_2020.rst +++ b/designs/auth_proto_rewrite_late_2020.rst @@ -47,8 +47,8 @@ New Design (late 2020 - future) =============================== A clearer configuration of credentials and how they function for the account is needed. This means -changing how the current credential memory representation works. The database format does *not* need -to change, but *may* be extended. +changing how the current credential memory representation works. The database format does need to +change. Currently Credentials can have *any* combination of factors. @@ -75,5 +75,125 @@ This will simplify the state machines in authsession, as well as allowing better UI decisions in clients for how we want to interact with possible credentials on the client system. +Credentials In Memory +--------------------- +To support this the representation of credentials in memory must change. The current struct (2020-12) +is: + + pub struct Credential { + pub(crate) password: Option, + pub(crate) webauthn: Option>, + pub(crate) totp: Option, + pub(crate) claims: Vec, + pub(crate) uuid: Uuid, + } + +There are numerous issues with this design. It does not express clearly the combination +of credentials that are valid in the credential, nor their capabilities and usage. A situation +such as `Password && (TOTP || Webauthn no verification)` OR `Password && Webauthn no verification` +are now ambiguous with this representation. It also does not clearly allow us to see where is the +correct entry/starting point for the authentication session. + +An improved design will have Credential become an enum representing the valid authentication methods + + pub enum Credential { + Anonymous, + Password(), + GeneratedPassword(), + PasswordMFA(), + PasswordWebauthn(), + Webauthn(), + WebauthnVerified(), + PasswordWebauthnVerified(), + } + +This allows a clearer set of logic flows in the credential and it's handling, as well as defined +transforms between credential states and type level guarantees of the consistency of the credential. + +Database Credentials +-------------------- + +Database Credentials are currently stored as: + + #[derive(Serialize, Deserialize, Debug)] + pub struct DbCredV1 { + pub password: Option, + pub webauthn: Option>, + pub totp: Option, + pub claims: Vec, + pub uuid: Uuid, + } + +This will be extended with an enum to represent the correct type/policy to deserialise into: + + pub struct DbCredV1 { + pub type_: DbCredTypeV1, + } + +An in place upgrade will be required to add this type to all existing credentials in the +database. + +Protocol +-------- + +The current design of the step/response is as follows. + + pub enum AuthStep { + Init(String), + Creds(Vec), + } + + pub enum AuthState { + Success(String), + Denied(String), + Continue(Vec), + } + +This will be extended to include the selection criteria to choose which method to use and be presented +with. Additionally, only one AuthCredential can be presented by the server at a time, and the server +may respond with many choices for the next step. This allows the server to propose TOTP *OR* Webauthn +but the client must choose which (It can not supply both, creating an AND situation or ambiguity around +the correct way to handle these). + + pub enum AuthMech { + Anonymous, + Password, + // This covers PasswordWebauthn as well. + PasswordMFA, + Webauthn, + WebauthnVerified, + PasswordWebauthnVerified, + } + + pub enum AuthStep { + Init(String), // server responds with AuthState::Choose|Denied + Begin(AuthMech), // server responds with AuthState::Continue|Success|Denied + Cred(AuthCredential), // server response with AuthState::Continue|Success|Denied + } + + pub enum AuthState { + Choose(Vec), + Continue(Vec), + Success(String), + Denied(String), + } + +A key reason to have this "Choose" step is related to an issue in the design and construction of +Webauthn Challenges. For more details see: https://fy.blackhats.net.au/blog/html/2020/11/21/webauthn_userverificationpolicy_curiosities.html + +AuthSession +----------- + +Once these other changes are made, AuthSession will need to be simplified but it's core state machines +will remain mostly unchanged. This set of changes will likely result in the AuthSession being much +clearer due to the enforcement of credential presentation order instead of the current design that +may allow "all in one" submissions. + +Other Benefits +============== + +* During an MFA authentication, the Password if incorrect can be re-prompted for a number of times subsequent to the TOTP/Webauthn having been found valid, improving user experience. +* Allows Webauthn Verified credentials to be used with clearer expression of the claims of the device associated to the credential. +* Policy will be simpler to enforce on credentials due to them more clearly stating their design and layouts. diff --git a/kanidm_client/src/asynchronous.rs b/kanidm_client/src/asynchronous.rs index 4c458f896..8a84c0b5e 100644 --- a/kanidm_client/src/asynchronous.rs +++ b/kanidm_client/src/asynchronous.rs @@ -2,6 +2,7 @@ use crate::{ClientError, KanidmClientBuilder, APPLICATION_JSON, KOPID}; use reqwest::header::CONTENT_TYPE; use serde::de::DeserializeOwned; use serde::Serialize; +use std::collections::BTreeSet as Set; use kanidm_proto::v1::*; @@ -189,13 +190,39 @@ impl KanidmAsyncClient { .map_err(|e| ClientError::JSONDecode(e, opid)) } - pub async fn auth_step_init(&self, ident: &str) -> Result { + pub async fn auth_step_init(&self, ident: &str) -> Result, ClientError> { let auth_init = AuthRequest { step: AuthStep::Init(ident.to_string()), }; let r: Result = self.perform_post_request("/v1/auth", auth_init).await; - r.map(|v| v.state) + r.map(|v| { + debug!("Authentication Session ID -> {:?}", v.sessionid); + v.state + }) + .and_then(|state| match state { + AuthState::Choose(mechs) => Ok(mechs), + _ => Err(ClientError::AuthenticationFailed), + }) + .map(|mechs| mechs.into_iter().collect()) + } + + pub async fn auth_step_begin(&self, mech: AuthMech) -> Result, ClientError> { + let auth_begin = AuthRequest { + step: AuthStep::Begin(mech), + }; + + let r: Result = self.perform_post_request("/v1/auth", auth_begin).await; + r.map(|v| { + debug!("Authentication Session ID -> {:?}", v.sessionid); + v.state + }) + .and_then(|state| match state { + AuthState::Continue(allowed) => Ok(allowed), + _ => Err(ClientError::AuthenticationFailed), + }) + // For converting to a Set + // .map(|allowed| allowed.into_iter().collect()) } pub async fn auth_simple_password( @@ -203,13 +230,23 @@ impl KanidmAsyncClient { ident: &str, password: &str, ) -> Result<(), ClientError> { - let _state = match self.auth_step_init(ident).await { + let mechs = match self.auth_step_init(ident).await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::Password) { + debug!("Password mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let _state = match self.auth_step_begin(AuthMech::Password).await { Ok(s) => s, Err(e) => return Err(e), }; let auth_req = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Password(password.to_string())]), + step: AuthStep::Cred(AuthCredential::Password(password.to_string())), }; let r: Result = self.perform_post_request("/v1/auth", auth_req).await; @@ -225,15 +262,23 @@ impl KanidmAsyncClient { } pub async fn auth_anonymous(&mut self) -> Result<(), ClientError> { - // TODO #251: Check state for auth continue contains anonymous. - // #251 will remove the need for this check. - let _state = match self.auth_step_init("anonymous").await { + let mechs = match self.auth_step_init("anonymous").await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::Anonymous) { + debug!("Anonymous mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let _state = match self.auth_step_begin(AuthMech::Anonymous).await { Ok(s) => s, Err(e) => return Err(e), }; let auth_anon = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Anonymous]), + step: AuthStep::Cred(AuthCredential::Anonymous), }; let r: Result = self.perform_post_request("/v1/auth", auth_anon).await; diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index 67cf54fe0..dc566d549 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -17,6 +17,7 @@ use serde::Serialize; use serde_derive::Deserialize; use serde_json::error::Error as SerdeJsonError; use std::collections::BTreeMap; +use std::collections::BTreeSet as Set; use std::fs::{metadata, File, Metadata}; use std::io::Read; use std::os::unix::fs::MetadataExt; @@ -32,11 +33,11 @@ use webauthn_rs::proto::{ // use users::{get_current_uid, get_effective_uid}; use kanidm_proto::v1::{ - AccountUnixExtend, AuthAllowed, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, - CreateRequest, DeleteRequest, Entry, Filter, GroupUnixExtend, ModifyList, ModifyRequest, - OperationError, OperationResponse, RadiusAuthToken, SearchRequest, SearchResponse, - SetCredentialRequest, SetCredentialResponse, SingleStringRequest, TOTPSecret, UnixGroupToken, - UnixUserToken, UserAuthToken, WhoamiResponse, + AccountUnixExtend, AuthAllowed, AuthCredential, AuthMech, AuthRequest, AuthResponse, AuthState, + AuthStep, CreateRequest, DeleteRequest, Entry, Filter, GroupUnixExtend, ModifyList, + ModifyRequest, OperationError, OperationResponse, RadiusAuthToken, SearchRequest, + SearchResponse, SetCredentialRequest, SetCredentialResponse, SingleStringRequest, TOTPSecret, + UnixGroupToken, UnixUserToken, UserAuthToken, WhoamiResponse, }; pub mod asynchronous; @@ -555,14 +556,23 @@ impl KanidmClient { // auth pub fn auth_anonymous(&mut self) -> Result<(), ClientError> { - // TODO #251: Check state for auth continue contains anonymous. - let _state = match self.auth_step_init("anonymous") { + let mechs = match self.auth_step_init("anonymous") { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::Anonymous) { + debug!("Anonymous mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let _state = match self.auth_step_begin(AuthMech::Anonymous) { Ok(s) => s, Err(e) => return Err(e), }; let auth_anon = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Anonymous]), + step: AuthStep::Cred(AuthCredential::Anonymous), }; let r: Result = self.perform_post_request("/v1/auth", auth_anon); @@ -579,13 +589,23 @@ impl KanidmClient { } pub fn auth_simple_password(&mut self, ident: &str, password: &str) -> Result<(), ClientError> { - let _state = match self.auth_step_init(ident) { + let mechs = match self.auth_step_init(ident) { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::Password) { + debug!("Password mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let _state = match self.auth_step_begin(AuthMech::Password) { Ok(s) => s, Err(e) => return Err(e), }; let auth_req = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Password(password.to_string())]), + step: AuthStep::Cred(AuthCredential::Password(password.to_string())), }; let r: Result = self.perform_post_request("/v1/auth", auth_req); @@ -607,19 +627,52 @@ impl KanidmClient { password: &str, totp: u32, ) -> Result<(), ClientError> { - let _state = match self.auth_step_init(ident) { + let mechs = match self.auth_step_init(ident) { Ok(s) => s, Err(e) => return Err(e), }; - let auth_req = AuthRequest { - step: AuthStep::Creds(vec![ - AuthCredential::TOTP(totp), - AuthCredential::Password(password.to_string()), - ]), - }; - let r: Result = self.perform_post_request("/v1/auth", auth_req); + if !mechs.contains(&AuthMech::PasswordMFA) { + debug!("PasswordMFA mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + let state = match self.auth_step_begin(AuthMech::PasswordMFA) { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !state.contains(&AuthAllowed::TOTP) { + debug!("TOTP step not offered."); + return Err(ClientError::AuthenticationFailed); + } + + let auth_req = AuthRequest { + step: AuthStep::Cred(AuthCredential::TOTP(totp)), + }; + + let r: Result = self.perform_post_request("/v1/auth", auth_req); + let r = r?; + + // Should need to continue. + match r.state { + AuthState::Continue(allowed) => { + if !allowed.contains(&AuthAllowed::Password) { + debug!("Password step not offered."); + return Err(ClientError::AuthenticationFailed); + } + } + _ => { + debug!("Invalid AuthState presented."); + return Err(ClientError::AuthenticationFailed); + } + }; + + let auth_req = AuthRequest { + step: AuthStep::Cred(AuthCredential::Password(password.to_string())), + }; + + let r: Result = self.perform_post_request("/v1/auth", auth_req); let r = r?; match r.state { @@ -636,27 +689,31 @@ impl KanidmClient { &mut self, ident: &str, ) -> Result { - let state = match self.auth_step_init(ident) { + let mechs = match self.auth_step_init(ident) { Ok(s) => s, Err(e) => return Err(e), }; - match state { - AuthState::Continue(mut proc) => { - // get the webauthn chal out of the state. - let chal = proc.pop(); - match chal { - Some(AuthAllowed::Webauthn(r)) => Ok(r), - _ => Err(ClientError::AuthenticationFailed), - } - } + if !mechs.contains(&AuthMech::Webauthn) { + debug!("Webauthn mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let mut state = match self.auth_step_begin(AuthMech::Webauthn) { + Ok(s) => s, + Err(e) => return Err(e), + }; + + // State is now a set of auth continues. + match state.pop() { + Some(AuthAllowed::Webauthn(r)) => Ok(r), _ => Err(ClientError::AuthenticationFailed), } } pub fn auth_webauthn_complete(&mut self, pkc: PublicKeyCredential) -> Result<(), ClientError> { let auth_req = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Webauthn(pkc)]), + step: AuthStep::Cred(AuthCredential::Webauthn(pkc)), }; let r: Result = self.perform_post_request("/v1/auth", auth_req); @@ -709,13 +766,39 @@ impl KanidmClient { r.map(|_| true) } - pub fn auth_step_init(&self, ident: &str) -> Result { + pub fn auth_step_init(&self, ident: &str) -> Result, ClientError> { let auth_init = AuthRequest { step: AuthStep::Init(ident.to_string()), }; let r: Result = self.perform_post_request("/v1/auth", auth_init); - r.map(|v| v.state) + r.map(|v| { + debug!("Authentication Session ID -> {:?}", v.sessionid); + v.state + }) + .and_then(|state| match state { + AuthState::Choose(mechs) => Ok(mechs), + _ => Err(ClientError::AuthenticationFailed), + }) + .map(|mechs| mechs.into_iter().collect()) + } + + pub fn auth_step_begin(&self, mech: AuthMech) -> Result, ClientError> { + let auth_begin = AuthRequest { + step: AuthStep::Begin(mech), + }; + + let r: Result = self.perform_post_request("/v1/auth", auth_begin); + r.map(|v| { + debug!("Authentication Session ID -> {:?}", v.sessionid); + v.state + }) + .and_then(|state| match state { + AuthState::Continue(allowed) => Ok(allowed), + _ => Err(ClientError::AuthenticationFailed), + }) + // For converting to a Set + // .map(|allowed| allowed.into_iter().collect()) } // ===== GROUPS diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index 4109b34e4..8eca6cc94 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::fmt; use uuid::Uuid; // use zxcvbn::feedback; +use std::cmp::Ordering; use webauthn_rs::proto::{ CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, @@ -426,6 +427,7 @@ impl ModifyRequest { // On loginSuccess, we send a cookie, and that allows the token to be // generated. The cookie can be shared between servers. #[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum AuthCredential { Anonymous, Password(String), @@ -444,17 +446,32 @@ impl fmt::Debug for AuthCredential { } } +#[derive(Debug, Serialize, Deserialize, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] +pub enum AuthMech { + Anonymous, + Password, + PasswordMFA, + Webauthn, + // WebauthnVerified, + // PasswordWebauthnVerified +} + +impl PartialEq for AuthMech { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum AuthStep { // name Init(String), - /* - Step( - Type(params ....) - ), - */ - Creds(Vec), + // We want to talk to you like this. + Begin(AuthMech), + // Step + Cred(AuthCredential), // Should we have a "finalise" type to attempt to finish based on // what we have given? } @@ -482,17 +499,47 @@ impl PartialEq for AuthAllowed { } } +impl Eq for AuthAllowed {} + +impl Ord for AuthAllowed { + fn cmp(&self, other: &Self) -> Ordering { + if self.eq(other) { + Ordering::Equal + } else { + // Relies on the fact that match is executed in order! + match (self, other) { + (AuthAllowed::Anonymous, _) => Ordering::Less, + (_, AuthAllowed::Anonymous) => Ordering::Greater, + (AuthAllowed::Password, _) => Ordering::Less, + (_, AuthAllowed::Password) => Ordering::Greater, + (AuthAllowed::TOTP, _) => Ordering::Less, + (_, AuthAllowed::TOTP) => Ordering::Greater, + (AuthAllowed::Webauthn(_), _) => Ordering::Less, + // Unreachable + // (_, AuthAllowed::Webauthn(_)) => Ordering::Greater, + } + } + } +} + +impl PartialOrd for AuthAllowed { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum AuthState { - // Everything is good, your bearer header has been issued and is within - // the result. - // Success(UserAuthToken), - Success(String), + // You need to select how you want to talk to me. + Choose(Vec), + // Continue to auth, allowed mechanisms/challenges listed. + Continue(Vec), // Something was bad, your session is terminated and no cookie. Denied(String), - // Continue to auth, allowed mechanisms listed. - Continue(Vec), + // Everything is good, your bearer header has been issued and is within + // the result. + Success(String), } #[derive(Debug, Serialize, Deserialize)] diff --git a/kanidm_rlm_python/README.md b/kanidm_rlm_python/README.md new file mode 100644 index 000000000..c22980d44 --- /dev/null +++ b/kanidm_rlm_python/README.md @@ -0,0 +1,27 @@ +Testing Process +=============== + + cd kanidmd + cargo run -- recover_account -c ./server.toml -n admin + cargo run -- server -c ./server.toml + + + + cd kanidm_tools + cargo run -- login -D admin + cargo run -- account list -D admin + cargo run -- account create -D admin radius_service_account radius_service_account + cargo run -- group add_members -D admin idm_radius_servers radius_service_account + cargo run -- account credential set_password radius_service_account -D admin + cargo run -- account radius generate_secret admin -D admin + + + + cd kanidm_rlm_python/ + KANIDM_RLM_CONFIG=./test_data/config.ini python3 kanidmradius.py test + KANIDM_RLM_CONFIG=./test_data/config.ini python3 kanidmradius.py admin + + + + + diff --git a/kanidm_rlm_python/kanidmradius.py b/kanidm_rlm_python/kanidmradius.py index 57f249a0e..f4439937f 100644 --- a/kanidm_rlm_python/kanidmradius.py +++ b/kanidm_rlm_python/kanidmradius.py @@ -16,9 +16,10 @@ else: # Setup the config too print(os.getcwd()) +CONFIG_PATH = os.environ.get('KANIDM_RLM_CONFIG', '/data/config.ini') + CONFIG = configparser.ConfigParser() -CONFIG.read('/data/config.ini') -# CONFIG.read('/tmp/config.ini') +CONFIG.read(CONFIG_PATH) GROUPS = [ { @@ -50,7 +51,19 @@ def _authenticate(s, acct, pw): print(r.json()) raise Exception("AuthInitFailed") - cred_auth = {"step": { "creds": [{"Password": pw}]}} + # {'sessionid': '00000000-5fe5-46e1-06b6-b830dd035a10', 'state': {'choose': ['password']}} + if 'password' not in r.json().get('state', {'choose': None}).get('choose', None): + print("invalid auth mech presented %s" % r.json()) + raise Exception("AuthMechUnknown") + + begin_auth = {"step": {"begin": "password"}} + + r = s.post(AUTH_URL, json=begin_auth, verify=CA, timeout=TIMEOUT) + if r.status_code != 200: + print(r.json()) + raise Exception("AuthBeginFailed") + + cred_auth = {"step": { "cred": {"password": pw}}} r = s.post(AUTH_URL, json=cred_auth, verify=CA, timeout=TIMEOUT) response = r.json() if r.status_code != 200: diff --git a/kanidm_rlm_python/test_data/config.ini b/kanidm_rlm_python/test_data/config.ini index 41bd7a230..a2511f333 100644 --- a/kanidm_rlm_python/test_data/config.ini +++ b/kanidm_rlm_python/test_data/config.ini @@ -1,5 +1,5 @@ [kanidm_client] -url = https://172.17.0.2:8080 +url = https://localhost:8443 strict = false ca = /data/ca.crt user = radius_service_account diff --git a/kanidmd/src/lib/be/dbvalue.rs b/kanidmd/src/lib/be/dbvalue.rs index 414ad0def..e6d115597 100644 --- a/kanidmd/src/lib/be/dbvalue.rs +++ b/kanidmd/src/lib/be/dbvalue.rs @@ -39,10 +39,30 @@ pub struct DbWebauthnV1 { pub v: bool, } +#[derive(Serialize, Deserialize, Debug)] +pub enum DbCredTypeV1 { + Pw, + GPw, + PwMfa, + // PwWn, + Wn, + // WnVer, + // PwWnVer, +} + +fn dbcred_type_default_pw() -> DbCredTypeV1 { + DbCredTypeV1::Pw +} + #[derive(Serialize, Deserialize, Debug)] pub struct DbCredV1 { + #[serde(default = "dbcred_type_default_pw")] + pub type_: DbCredTypeV1, + #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub webauthn: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub totp: Option, pub claims: Vec, pub uuid: Uuid, diff --git a/kanidmd/src/lib/core/https.rs b/kanidmd/src/lib/core/https.rs index 41796aeaa..2f51b78be 100644 --- a/kanidmd/src/lib/core/https.rs +++ b/kanidmd/src/lib/core/https.rs @@ -1009,6 +1009,30 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { } // Do some response/state management. match state { + AuthState::Choose(allowed) => { + debug!("🧩 -> AuthState::Choose"); + let msession = req.session_mut(); + // Force a new cookie session. + // msession.regenerate(); + // Ensure the auth-session-id is set + msession.remove("auth-session-id"); + msession + .insert("auth-session-id", sessionid) + .map(|_| ProtoAuthState::Choose(allowed)) + .map_err(|_| OperationError::InvalidSessionState) + } + AuthState::Continue(allowed) => { + debug!("🧩 -> AuthState::Continue"); + let msession = req.session_mut(); + // Force a new cookie session. + // msession.regenerate(); + // Ensure the auth-session-id is set + msession.remove("auth-session-id"); + msession + .insert("auth-session-id", sessionid) + .map(|_| ProtoAuthState::Continue(allowed)) + .map_err(|_| OperationError::InvalidSessionState) + } AuthState::Success(uat) => { debug!("🧩 -> AuthState::Success"); // Remove the auth-session-id @@ -1030,18 +1054,6 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { msession.remove("auth-session-id"); Err(OperationError::AccessDenied) } - AuthState::Continue(allowed) => { - debug!("🧩 -> AuthState::Continue"); - let msession = req.session_mut(); - // Force a new cookie session. - // msession.regenerate(); - // Ensure the auth-session-id is set - msession.remove("auth-session-id"); - msession - .insert("auth-session-id", sessionid) - .map(|_| ProtoAuthState::Continue(allowed)) - .map_err(|_| OperationError::InvalidSessionState) - } } .map(|state| AuthResponse { state, sessionid }) } diff --git a/kanidmd/src/lib/credential/mod.rs b/kanidmd/src/lib/credential/mod.rs index a5a93e1ce..3e2e21d85 100644 --- a/kanidmd/src/lib/credential/mod.rs +++ b/kanidmd/src/lib/credential/mod.rs @@ -1,4 +1,4 @@ -use crate::be::dbvalue::{DbCredV1, DbPasswordV1, DbWebauthnV1}; +use crate::be::dbvalue::{DbCredTypeV1, DbCredV1, DbPasswordV1, DbWebauthnV1}; use hashbrown::HashMap as Map; use kanidm_proto::v1::OperationError; use openssl::hash::MessageDigest; @@ -225,15 +225,10 @@ impl Password { /// B requires both the password and otp to be valid. /// /// In this way, each Credential provides it's own password requirements and policy, and requires -/// some metadata to support this such as it's source and strength etc. Some of these details are -/// to be resolved ... +/// some metadata to support this such as it's source and strength etc. pub struct Credential { - // Source (machine, user, ....). Strength? // policy: Policy, - pub(crate) password: Option, - pub(crate) webauthn: Option>, - // totp: Option> - pub(crate) totp: Option, + pub(crate) type_: CredentialType, pub(crate) claims: Vec, // Uuid of Credential, used by auth session to lock this specific credential // if required. @@ -242,12 +237,25 @@ pub struct Credential { // locked: bool } +#[derive(Clone, Debug)] +pub enum CredentialType { + // Anonymous, + Password(Password), + GeneratedPassword(Password), + Webauthn(Map), + PasswordMFA(Password, Option, Map), + // PasswordWebauthn(Password, Map), + // WebauthnVerified(Map), + // PasswordWebauthnVerified(Password, Map), +} + impl TryFrom for Credential { type Error = (); fn try_from(value: DbCredV1) -> Result { // Work out what the policy is? let DbCredV1 { + type_, password, webauthn, totp, @@ -284,10 +292,21 @@ impl TryFrom for Credential { None => None, }; + let type_ = match type_ { + DbCredTypeV1::Pw => v_password.map(CredentialType::Password), + DbCredTypeV1::GPw => v_password.map(CredentialType::GeneratedPassword), + // In the future this could use .zip + DbCredTypeV1::PwMfa => match (v_password, v_webauthn) { + (Some(pw), Some(wn)) => Some(CredentialType::PasswordMFA(pw, v_totp, wn)), + _ => None, + }, + DbCredTypeV1::Wn => v_webauthn.map(CredentialType::Webauthn), + } + .filter(|v| v.is_valid()) + .ok_or(())?; + Ok(Credential { - password: v_password, - webauthn: v_webauthn, - totp: v_totp, + type_, claims, uuid, }) @@ -306,9 +325,7 @@ impl Credential { let mut webauthn_map = Map::new(); webauthn_map.insert(label, cred); Credential { - password: None, - webauthn: Some(webauthn_map), - totp: None, + type_: CredentialType::Webauthn(webauthn_map), claims: Vec::new(), uuid: Uuid::new_v4(), } @@ -319,13 +336,7 @@ impl Credential { policy: &CryptoPolicy, cleartext: &str, ) -> Result { - Password::new(policy, cleartext).map(|pw| Credential { - password: Some(pw), - webauthn: self.webauthn.clone(), - totp: self.totp.clone(), - claims: self.claims.clone(), - uuid: self.uuid, - }) + Password::new(policy, cleartext).map(|pw| self.update_password(pw)) } pub fn append_webauthn( @@ -333,30 +344,37 @@ impl Credential { label: String, cred: WebauthnCredential, ) -> Result { - let webauthn_map = match &self.webauthn { - Some(map) => { - let mut nmap = map.clone(); - match nmap.insert(label.clone(), cred) { - Some(_) => { - return Err(OperationError::InvalidAttribute(format!( - "Webauthn label '{:?}' already exists", - label - ))); - } - None => nmap, - } + let type_ = match &self.type_ { + CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => { + let mut wan = Map::new(); + wan.insert(label, cred); + CredentialType::PasswordMFA(pw.clone(), None, wan) } - None => { - let mut map = Map::new(); - map.insert(label, cred); - map + CredentialType::PasswordMFA(pw, totp, map) => { + let mut nmap = map.clone(); + if let Some(_) = nmap.insert(label.clone(), cred) { + return Err(OperationError::InvalidAttribute(format!( + "Webauthn label '{:?}' already exists", + label + ))); + } + CredentialType::PasswordMFA(pw.clone(), totp.clone(), nmap) + } + CredentialType::Webauthn(map) => { + let mut nmap = map.clone(); + if let Some(_) = nmap.insert(label.clone(), cred) { + return Err(OperationError::InvalidAttribute(format!( + "Webauthn label '{:?}' already exists", + label + ))); + } + CredentialType::Webauthn(nmap) } }; + // Check stuff Ok(Credential { - password: self.password.clone(), - webauthn: Some(webauthn_map), - totp: self.totp.clone(), + type_, claims: self.claims.clone(), uuid: self.uuid, }) @@ -367,69 +385,155 @@ impl Credential { cid: &CredentialID, counter: Counter, ) -> Result, OperationError> { - let opt_label = self.webauthn.as_ref().and_then(|m| { - m.iter().fold(None, |acc, (k, v)| { - if acc.is_none() && &v.cred_id == cid && v.counter < counter { - Some(k) - } else { - acc - } - }) - }); + let nmap = match &self.type_ { + CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => { + // No action required + return Ok(None); + } + CredentialType::PasswordMFA(_, _, map) | CredentialType::Webauthn(map) => map + .iter() + .fold(None, |acc, (k, v)| { + if acc.is_none() && &v.cred_id == cid && v.counter < counter { + Some(k) + } else { + acc + } + }) + .map(|label| { + let mut webauthn_map = map.clone(); - if let Some(label) = opt_label { - let mut webauthn_map = self.webauthn.clone(); + webauthn_map + .get_mut(label) + .map(|cred| cred.counter = counter); + webauthn_map + }), + }; - webauthn_map - .as_mut() - .and_then(|m| m.get_mut(label)) - .map(|cred| cred.counter = counter); + let map = match nmap { + Some(map) => map, + None => { + // No action needed. + return Ok(None); + } + }; - Ok(Some(Credential { - password: self.password.clone(), - webauthn: webauthn_map, - totp: self.totp.clone(), - claims: self.claims.clone(), - uuid: self.uuid, - })) - } else { - Ok(None) + let type_ = match &self.type_ { + CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => { + // Should not be possible! + unreachable!(); + } + CredentialType::Webauthn(_) => CredentialType::Webauthn(map), + CredentialType::PasswordMFA(pw, totp, _) => { + CredentialType::PasswordMFA(pw.clone(), totp.clone(), map) + } + }; + + Ok(Some(Credential { + type_, + claims: self.claims.clone(), + uuid: self.uuid, + })) + } + + pub fn webauthn_ref(&self) -> Result<&Map, OperationError> { + match &self.type_ { + CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => Err( + OperationError::InvalidAccountState("non-webauthn cred type?".to_string()), + ), + CredentialType::PasswordMFA(_, _, map) | CredentialType::Webauthn(map) => Ok(map), + } + } + + pub fn password_ref(&self) -> Result<&Password, OperationError> { + match &self.type_ { + CredentialType::Password(pw) + | CredentialType::GeneratedPassword(pw) + | CredentialType::PasswordMFA(pw, _, _) => Ok(pw), + CredentialType::Webauthn(_) => Err(OperationError::InvalidAccountState( + "non-password cred type?".to_string(), + )), } } #[cfg(test)] - pub fn verify_password(&self, cleartext: &str) -> bool { - match &self.password { - Some(pw) => pw.verify(cleartext).unwrap_or(false), - None => false, - } + pub fn verify_password(&self, cleartext: &str) -> Result { + self.password_ref().and_then(|pw| pw.verify(cleartext)) } pub fn to_db_valuev1(&self) -> DbCredV1 { - DbCredV1 { - password: self.password.as_ref().map(|pw| pw.to_dbpasswordv1()), - webauthn: self.webauthn.as_ref().map(|map| { - map.iter() - .map(|(k, v)| DbWebauthnV1 { - l: k.clone(), - i: v.cred_id.clone(), - c: v.cred.clone(), - t: v.counter, - v: v.verified, - }) - .collect() - }), - totp: self.totp.as_ref().map(|t| t.to_dbtotpv1()), - claims: self.claims.clone(), - uuid: self.uuid, + let claims = self.claims.clone(); + let uuid = self.uuid; + match &self.type_ { + CredentialType::Password(pw) => DbCredV1 { + type_: DbCredTypeV1::Pw, + password: Some(pw.to_dbpasswordv1()), + webauthn: None, + totp: None, + claims, + uuid, + }, + CredentialType::GeneratedPassword(pw) => DbCredV1 { + type_: DbCredTypeV1::GPw, + password: Some(pw.to_dbpasswordv1()), + webauthn: None, + totp: None, + claims, + uuid, + }, + CredentialType::PasswordMFA(pw, totp, map) => DbCredV1 { + type_: DbCredTypeV1::PwMfa, + password: Some(pw.to_dbpasswordv1()), + webauthn: Some( + map.iter() + .map(|(k, v)| DbWebauthnV1 { + l: k.clone(), + i: v.cred_id.clone(), + c: v.cred.clone(), + t: v.counter, + v: v.verified, + }) + .collect(), + ), + totp: totp.as_ref().map(|t| t.to_dbtotpv1()), + claims, + uuid, + }, + CredentialType::Webauthn(map) => DbCredV1 { + type_: DbCredTypeV1::Wn, + password: None, + webauthn: Some( + map.iter() + .map(|(k, v)| DbWebauthnV1 { + l: k.clone(), + i: v.cred_id.clone(), + c: v.cred.clone(), + t: v.counter, + v: v.verified, + }) + .collect(), + ), + totp: None, + claims, + uuid, + }, } } pub(crate) fn update_password(&self, pw: Password) -> Self { + let type_ = match &self.type_ { + CredentialType::Password(_) => CredentialType::Password(pw), + CredentialType::GeneratedPassword(_) => CredentialType::GeneratedPassword(pw), + CredentialType::PasswordMFA(_, totp, wan) => { + CredentialType::PasswordMFA(pw, totp.clone(), wan.clone()) + } + CredentialType::Webauthn(wan) => { + // Or should this become PasswordWebauthn? + debug_assert!(false); + CredentialType::Webauthn(wan.clone()) + } + }; Credential { - password: Some(pw), - webauthn: self.webauthn.clone(), - totp: self.totp.clone(), + type_, claims: self.claims.clone(), uuid: self.uuid, } @@ -437,10 +541,20 @@ impl Credential { // We don't make totp accessible from outside the crate for now. pub(crate) fn update_totp(&self, totp: TOTP) -> Self { + let type_ = match &self.type_ { + CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => { + CredentialType::PasswordMFA(pw.clone(), Some(totp), Map::new()) + } + CredentialType::PasswordMFA(pw, _, wan) => { + CredentialType::PasswordMFA(pw.clone(), Some(totp), wan.clone()) + } + CredentialType::Webauthn(wan) => { + debug_assert!(false); + CredentialType::Webauthn(wan.clone()) + } + }; Credential { - password: self.password.clone(), - webauthn: self.webauthn.clone(), - totp: Some(totp), + type_, claims: self.claims.clone(), uuid: self.uuid, } @@ -448,24 +562,27 @@ impl Credential { pub(crate) fn new_from_password(pw: Password) -> Self { Credential { - password: Some(pw), - webauthn: None, - totp: None, + type_: CredentialType::Password(pw), claims: Vec::new(), uuid: Uuid::new_v4(), } } pub(crate) fn softlock_policy(&self) -> Option { - match (&self.webauthn, &self.totp, &self.password) { - // Has any kind of Webauthn .... - (Some(_webauthn), _, _) => Some(CredSoftLockPolicy::Webauthn), - // Has any kind of totp. - (None, Some(totp), _) => Some(CredSoftLockPolicy::TOTP(totp.step)), - // No totp, pw - (None, None, Some(_)) => Some(CredSoftLockPolicy::Password), - // Indeterminate - _ => None, + match &self.type_ { + CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => { + Some(CredSoftLockPolicy::Password) + } + CredentialType::PasswordMFA(_pw, totp, wan) => { + if let Some(r_totp) = totp { + Some(CredSoftLockPolicy::TOTP(r_totp.step)) + } else if wan.len() > 0 { + Some(CredSoftLockPolicy::Webauthn) + } else { + None + } + } + CredentialType::Webauthn(_wan) => Some(CredSoftLockPolicy::Webauthn), } } @@ -490,6 +607,18 @@ impl Credential { */ } +impl CredentialType { + fn is_valid(&self) -> bool { + match self { + CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => true, + CredentialType::PasswordMFA(_, m_totp, webauthn) => { + m_totp.is_some() || !webauthn.is_empty() + } + CredentialType::Webauthn(webauthn) => !webauthn.is_empty(), + } + } +} + #[cfg(test)] mod tests { use crate::credential::policy::CryptoPolicy; @@ -500,11 +629,11 @@ mod tests { fn test_credential_simple() { let p = CryptoPolicy::minimum(); let c = Credential::new_password_only(&p, "password").unwrap(); - assert!(c.verify_password("password")); - assert!(!c.verify_password("password1")); - assert!(!c.verify_password("Password1")); - assert!(!c.verify_password("It Works!")); - assert!(!c.verify_password("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + assert!(c.verify_password("password").unwrap()); + assert!(!c.verify_password("password1").unwrap()); + assert!(!c.verify_password("Password1").unwrap()); + assert!(!c.verify_password("It Works!").unwrap()); + assert!(!c.verify_password("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap()); } #[test] diff --git a/kanidmd/src/lib/event.rs b/kanidmd/src/lib/event.rs index a18c583d5..0aba574ca 100644 --- a/kanidmd/src/lib/event.rs +++ b/kanidmd/src/lib/event.rs @@ -6,7 +6,9 @@ use crate::schema::SchemaTransaction; use crate::value::PartialValue; use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::ModifyList as ProtoModifyList; -use kanidm_proto::v1::{AuthCredential, AuthStep, SearchResponse, UserAuthToken, WhoamiResponse}; +use kanidm_proto::v1::{ + AuthCredential, AuthMech, AuthStep, SearchResponse, UserAuthToken, WhoamiResponse, +}; // use error::OperationError; use crate::modify::{ModifyInvalid, ModifyList, ModifyValid}; use crate::server::{ @@ -925,15 +927,22 @@ pub struct AuthEventStepInit { } #[derive(Debug)] -pub struct AuthEventStepCreds { +pub struct AuthEventStepCred { pub sessionid: Uuid, - pub creds: Vec, + pub cred: AuthCredential, +} + +#[derive(Debug)] +pub struct AuthEventStepMech { + pub sessionid: Uuid, + pub mech: AuthMech, } #[derive(Debug)] pub enum AuthEventStep { Init(AuthEventStepInit), - Creds(AuthEventStepCreds), + Begin(AuthEventStepMech), + Cred(AuthEventStepCred), } impl AuthEventStep { @@ -948,10 +957,19 @@ impl AuthEventStep { Ok(AuthEventStep::Init(AuthEventStepInit { name, appid: None })) } } - AuthStep::Creds(creds) => match sid { - Some(ssid) => Ok(AuthEventStep::Creds(AuthEventStepCreds { + AuthStep::Begin(mech) => match sid { + Some(ssid) => Ok(AuthEventStep::Begin(AuthEventStepMech { sessionid: ssid, - creds, + mech, + })), + None => Err(OperationError::InvalidAuthState( + "session id not present in cred".to_string(), + )), + }, + AuthStep::Cred(cred) => match sid { + Some(ssid) => Ok(AuthEventStep::Cred(AuthEventStepCred { + sessionid: ssid, + cred, })), None => Err(OperationError::InvalidAuthState( "session id not present in cred".to_string(), @@ -976,19 +994,24 @@ impl AuthEventStep { }) } + #[cfg(test)] + pub fn begin_mech(sessionid: Uuid, mech: AuthMech) -> Self { + AuthEventStep::Begin(AuthEventStepMech { sessionid, mech }) + } + #[cfg(test)] pub fn cred_step_anonymous(sid: Uuid) -> Self { - AuthEventStep::Creds(AuthEventStepCreds { + AuthEventStep::Cred(AuthEventStepCred { sessionid: sid, - creds: vec![AuthCredential::Anonymous], + cred: AuthCredential::Anonymous, }) } #[cfg(test)] pub fn cred_step_password(sid: Uuid, pw: &str) -> Self { - AuthEventStep::Creds(AuthEventStepCreds { + AuthEventStep::Cred(AuthEventStepCred { sessionid: sid, - creds: vec![AuthCredential::Password(pw.to_string())], + cred: AuthCredential::Password(pw.to_string()), }) } } @@ -1024,6 +1047,14 @@ impl AuthEvent { } } + #[cfg(test)] + pub fn begin_mech(sessionid: Uuid, mech: AuthMech) -> Self { + AuthEvent { + event: None, + step: AuthEventStep::begin_mech(sessionid, mech), + } + } + #[cfg(test)] pub fn cred_step_anonymous(sid: Uuid) -> Self { AuthEvent { diff --git a/kanidmd/src/lib/idm/account.rs b/kanidmd/src/lib/idm/account.rs index c7bc14d6d..c82bfecad 100644 --- a/kanidmd/src/lib/idm/account.rs +++ b/kanidmd/src/lib/idm/account.rs @@ -289,17 +289,11 @@ impl Account { ) -> Result { match appid { Some(_) => Err(OperationError::InvalidState), - None => { - match &self.primary { - // Check the cred's associated pw. - Some(ref primary) => primary - .password - .as_ref() - .ok_or(OperationError::InvalidState) - .and_then(|pw| pw.verify(cleartext)), - None => Err(OperationError::InvalidState), - } - } // no appid + None => self + .primary + .as_ref() + .ok_or(OperationError::InvalidState) + .and_then(|cred| cred.password_ref().and_then(|pw| pw.verify(cleartext))), } } diff --git a/kanidmd/src/lib/idm/authsession.rs b/kanidmd/src/lib/idm/authsession.rs index d1a4c5a74..ed878708d 100644 --- a/kanidmd/src/lib/idm/authsession.rs +++ b/kanidmd/src/lib/idm/authsession.rs @@ -3,9 +3,9 @@ use crate::idm::account::Account; use crate::idm::claim::Claim; use crate::idm::AuthState; use kanidm_proto::v1::OperationError; -use kanidm_proto::v1::{AuthAllowed, AuthCredential}; +use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthMech}; -use crate::credential::{totp::TOTP, Credential, Password}; +use crate::credential::{totp::TOTP, Credential, CredentialType, Password}; use crate::idm::delayed::{DelayedAction, PasswordUpgrade, WebauthnCounterIncrement}; // use crossbeam::channel::Sender; @@ -60,11 +60,10 @@ struct CredWebauthn { #[derive(Clone, Debug)] enum CredHandler { - Denied(&'static str), Anonymous, // AppPassword (?) Password(Password), - TOTPPassword(CredTotpPw), + PasswordMFA(CredTotpPw), Webauthn(CredWebauthn), // Webauthn + Password } @@ -76,15 +75,20 @@ impl CredHandler { c: &Credential, webauthn: &Webauthn, ) -> Result { - match (c.password.as_ref(), c.totp.as_ref(), c.webauthn.as_ref()) { - (Some(pw), None, None) => Ok(CredHandler::Password(pw.clone())), - (Some(pw), Some(totp), None) => Ok(CredHandler::TOTPPassword(CredTotpPw { - pw: pw.clone(), - pw_state: CredVerifyState::Init, - totp: totp.clone(), - totp_state: CredVerifyState::Init, - })), - (None, None, Some(wan)) => webauthn + match &c.type_ { + CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => { + Ok(CredHandler::Password(pw.clone())) + } + CredentialType::PasswordMFA(pw, Some(totp), _) => { + Ok(CredHandler::PasswordMFA(CredTotpPw { + pw: pw.clone(), + pw_state: CredVerifyState::Init, + totp: totp.clone(), + totp_state: CredVerifyState::Init, + })) + } + CredentialType::PasswordMFA(_, None, _) => Err(()), + CredentialType::Webauthn(wan) => webauthn .generate_challenge_authenticate(wan.values().map(|c| c.clone()).collect()) .map(|(chal, wan_state)| { CredHandler::Webauthn(CredWebauthn { @@ -101,8 +105,6 @@ impl CredHandler { ); () }), - // Must be an invalid set of credentials. WTF? - _ => Err(()), } } } @@ -126,189 +128,118 @@ impl CredHandler { } } - fn validate_anonymous(au: &mut AuditScope, creds: &[AuthCredential]) -> CredState { - creds.iter().fold( - CredState::Continue(vec![AuthAllowed::Anonymous]), - |acc, cred| { - // There is no "continuation" from this type - we only set it at - // the start assuming there is no values in the iter so we can tell - // the session to continue up to some timelimit. - match acc { - // If denied, continue returning denied. - CredState::Denied(_) => { - lsecurity!(au, "Handler::Anonymous -> Result::Denied - already denied"); - acc - } - // We have a continue or success, it's important we keep checking here - // after the success, because if they sent "multiple" anonymous or - // they sent anon + password, we need to handle both cases. Double anon - // is okay, but anything else is instant failure, even if we already - // had a success. - _ => { - match cred { - AuthCredential::Anonymous => { - // For anonymous, no claims will ever be issued. - lsecurity!(au, "Handler::Anonymous -> Result::Success"); - CredState::Success(Vec::new()) - } - _ => { - lsecurity!(au, "Handler::Anonymous -> Result::Denied - invalid cred type for handler"); - CredState::Denied(BAD_AUTH_TYPE_MSG) - } - } - } - } // end match acc - }, - ) + fn validate_anonymous(au: &mut AuditScope, cred: &AuthCredential) -> CredState { + match cred { + AuthCredential::Anonymous => { + // For anonymous, no claims will ever be issued. + lsecurity!(au, "Handler::Anonymous -> Result::Success"); + CredState::Success(Vec::new()) + } + _ => { + lsecurity!( + au, + "Handler::Anonymous -> Result::Denied - invalid cred type for handler" + ); + CredState::Denied(BAD_AUTH_TYPE_MSG) + } + } } fn validate_password( au: &mut AuditScope, - creds: &[AuthCredential], + cred: &AuthCredential, pw: &mut Password, who: Uuid, async_tx: &Sender, ) -> CredState { - creds.iter().fold( - // If no creds, remind that we want pw ... - CredState::Continue(vec![AuthAllowed::Password]), - |acc, cred| { - match acc { - // If failed, continue to fail. - CredState::Denied(_) => { - lsecurity!(au, "Handler::Password -> Result::Denied - already denied"); - acc - } - _ => { - match cred { - AuthCredential::Password(cleartext) => { - if pw.verify(cleartext.as_str()).unwrap_or(false) { - lsecurity!(au, "Handler::Password -> Result::Success"); - Self::maybe_pw_upgrade(au, pw, who, cleartext.as_str(), async_tx); - CredState::Success(Vec::new()) - } else { - lsecurity!(au, "Handler::Password -> Result::Denied - incorrect password"); - CredState::Denied(BAD_PASSWORD_MSG) - } - } - // All other cases fail. - _ => { - lsecurity!(au, "Handler::Anonymous -> Result::Denied - invalid cred type for handler"); - CredState::Denied(BAD_AUTH_TYPE_MSG) - } - } - } - } // end match acc - }, - ) + match cred { + AuthCredential::Password(cleartext) => { + if pw.verify(cleartext.as_str()).unwrap_or(false) { + lsecurity!(au, "Handler::Password -> Result::Success"); + Self::maybe_pw_upgrade(au, pw, who, cleartext.as_str(), async_tx); + CredState::Success(Vec::new()) + } else { + lsecurity!( + au, + "Handler::Password -> Result::Denied - incorrect password" + ); + CredState::Denied(BAD_PASSWORD_MSG) + } + } + // All other cases fail. + _ => { + lsecurity!( + au, + "Handler::Anonymous -> Result::Denied - invalid cred type for handler" + ); + CredState::Denied(BAD_AUTH_TYPE_MSG) + } + } } fn validate_totp_password( au: &mut AuditScope, - creds: &[AuthCredential], + cred: &AuthCredential, ts: &Duration, pw_totp: &mut CredTotpPw, who: Uuid, async_tx: &Sender, ) -> CredState { - // Set the default reminder to both pw + totp - creds.iter().fold( - // If no creds, remind that we want pw ... - CredState::Continue(vec![AuthAllowed::TOTP, AuthAllowed::Password]), - |acc, cred| { - match acc { - CredState::Denied(_) => { - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - already denied"); - acc - } - _ => { - match cred { - AuthCredential::Password(cleartext) => { - // if pw -> check - if pw_totp.pw.verify(cleartext.as_str()).unwrap_or(false) { - pw_totp.pw_state = CredVerifyState::Success; - Self::maybe_pw_upgrade(au, &pw_totp.pw, who, cleartext.as_str(), async_tx); - match pw_totp.totp_state { - CredVerifyState::Init => { - // TOTP hasn't been run yet, we need it before - // we indicate the pw status. - lsecurity!(au, "Handler::TOTPPassword -> Result::Continue - TOTP -, password OK"); - CredState::Continue(vec![AuthAllowed::TOTP]) - } - CredVerifyState::Success => { - // The totp is success, and password good, let's go! - lsecurity!(au, "Handler::TOTPPassword -> Result::Success - TOTP OK, password OK"); - CredState::Success(Vec::new()) - } - CredVerifyState::Fail => { - // The totp already failed, send that message now. - // Should be impossible state. - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - TOTP Fail, password OK"); - CredState::Denied(BAD_TOTP_MSG) - } - } - } else { - pw_totp.pw_state = CredVerifyState::Fail; - match pw_totp.totp_state { - CredVerifyState::Init => { - // TOTP hasn't been run yet, we need it before - // we indicate the pw status. - lsecurity!(au, "Handler::TOTPPassword -> Result::Continue - TOTP -, password Fail"); - CredState::Continue(vec![AuthAllowed::TOTP]) - } - CredVerifyState::Success => { - // The totp is success, but password bad. - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - TOTP OK, password Fail"); - CredState::Denied(BAD_PASSWORD_MSG) - } - CredVerifyState::Fail => { - // The totp already failed, remind. - // this should be an impossible state. - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - TOTP Fail, password Fail"); - CredState::Denied(BAD_TOTP_MSG) - } - } - } - } - AuthCredential::TOTP(totp_chal) => { - // if totp -> check - if pw_totp.totp.verify(*totp_chal, ts) { - pw_totp.totp_state = CredVerifyState::Success; - match pw_totp.pw_state { - CredVerifyState::Init => { - lsecurity!(au, "Handler::TOTPPassword -> Result::Continue - TOTP OK, password -"); - CredState::Continue(vec![AuthAllowed::Password]) - } - CredVerifyState::Success => { - lsecurity!(au, "Handler::TOTPPassword -> Result::Success - TOTP OK, password OK"); - CredState::Success(Vec::new()) - } - CredVerifyState::Fail => { - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - TOTP OK, password Fail"); - CredState::Denied(BAD_PASSWORD_MSG) - } - } - } else { - pw_totp.totp_state = CredVerifyState::Fail; - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - TOTP Fail, password -"); - CredState::Denied(BAD_TOTP_MSG) - } - } - // All other cases fail. - _ => { - lsecurity!(au, "Handler::TOTPPassword -> Result::Denied - invalid cred type for handler"); - CredState::Denied(BAD_AUTH_TYPE_MSG) - } - } // end match cred - } - } // end match acc - }, - ) // end fold - } // end CredHandler::TOTPPassword + match (cred, &pw_totp.totp_state, &pw_totp.pw_state) { + // Must be done first. + (AuthCredential::TOTP(totp_chal), CredVerifyState::Init, CredVerifyState::Init) => { + if pw_totp.totp.verify(*totp_chal, ts) { + pw_totp.totp_state = CredVerifyState::Success; + lsecurity!( + au, + "Handler::PasswordMFA -> Result::Continue - TOTP OK, password -" + ); + CredState::Continue(vec![AuthAllowed::Password]) + } else { + pw_totp.totp_state = CredVerifyState::Fail; + lsecurity!( + au, + "Handler::PasswordMFA -> Result::Denied - TOTP Fail, password -" + ); + CredState::Denied(BAD_TOTP_MSG) + } + } + // Must only proceed if totp was success. + ( + AuthCredential::Password(cleartext), + CredVerifyState::Success, + CredVerifyState::Init, + ) => { + if pw_totp.pw.verify(cleartext.as_str()).unwrap_or(false) { + pw_totp.pw_state = CredVerifyState::Success; + lsecurity!( + au, + "Handler::PasswordMFA -> Result::Success - TOTP OK, password OK" + ); + Self::maybe_pw_upgrade(au, &pw_totp.pw, who, cleartext.as_str(), async_tx); + CredState::Success(Vec::new()) + } else { + pw_totp.pw_state = CredVerifyState::Fail; + lsecurity!( + au, + "Handler::PasswordMFA -> Result::Denied - TOTP OK, password Fail" + ); + CredState::Denied(BAD_PASSWORD_MSG) + } + } + _ => { + lsecurity!( + au, + "Handler::PasswordMFA -> Result::Denied - invalid cred type for handler" + ); + CredState::Denied(BAD_AUTH_TYPE_MSG) + } + } + } // end CredHandler::PasswordMFA pub fn validate_webauthn( au: &mut AuditScope, - creds: &[AuthCredential], + cred: &AuthCredential, wan_cred: &mut CredWebauthn, webauthn: &Webauthn, who: Uuid, @@ -322,97 +253,116 @@ impl CredHandler { return CredState::Denied(BAD_WEBAUTHN_MSG); } - creds.iter().fold( - CredState::Continue(vec![]), - |acc, cred| { - match acc { - // If denied, continue returning denied. - CredState::Denied(_) => { - lsecurity!(au, "Handler::Webauthn -> Result::Denied - already denied"); - acc - } - _ => { - match cred { - AuthCredential::Webauthn(resp) => { - // lets see how we go. - webauthn.authenticate_credential(&resp, wan_cred.wan_state.clone()) - .map(|r| { - wan_cred.state = CredVerifyState::Success; - // Success. Determine if we need to update the counter - // async from r. - if let Some((cid, counter)) = r { - // Do async - if let Err(_e) = async_tx.send(DelayedAction::WebauthnCounterIncrement(WebauthnCounterIncrement { - target_uuid: who, - cid, - counter, - })) { - ladmin_warning!(au, "unable to queue delayed webauthn counter increment, continuing ... "); - }; - }; - CredState::Success(Vec::new()) - }) - .unwrap_or_else(|e| { - wan_cred.state = CredVerifyState::Fail; - // Denied. - lsecurity!(au, "Handler::Webauthn -> Result::Denied - webauthn error {:?}", e); - CredState::Denied(BAD_WEBAUTHN_MSG) - }) - } - _ => { - lsecurity!(au, "Handler::Webauthn -> Result::Denied - invalid cred type for handler"); - CredState::Denied(BAD_AUTH_TYPE_MSG) - } - } - } - } // end match acc + match cred { + AuthCredential::Webauthn(resp) => { + // lets see how we go. + webauthn.authenticate_credential(&resp, wan_cred.wan_state.clone()) + .map(|r| { + wan_cred.state = CredVerifyState::Success; + // Success. Determine if we need to update the counter + // async from r. + if let Some((cid, counter)) = r { + // Do async + if let Err(_e) = async_tx.send(DelayedAction::WebauthnCounterIncrement(WebauthnCounterIncrement { + target_uuid: who, + cid, + counter, + })) { + ladmin_warning!(au, "unable to queue delayed webauthn counter increment, continuing ... "); + }; + }; + CredState::Success(Vec::new()) + }) + .unwrap_or_else(|e| { + wan_cred.state = CredVerifyState::Fail; + // Denied. + lsecurity!(au, "Handler::Webauthn -> Result::Denied - webauthn error {:?}", e); + CredState::Denied(BAD_WEBAUTHN_MSG) + }) } - ) // end fold + _ => { + lsecurity!( + au, + "Handler::Webauthn -> Result::Denied - invalid cred type for handler" + ); + CredState::Denied(BAD_AUTH_TYPE_MSG) + } + } } pub fn validate( &mut self, au: &mut AuditScope, - creds: &[AuthCredential], + cred: &AuthCredential, ts: &Duration, who: Uuid, async_tx: &Sender, webauthn: &Webauthn, ) -> CredState { match self { - CredHandler::Denied(reason) => { - // Sad trombone. - lsecurity!(au, "Handler::Denied -> Result::Denied"); - CredState::Denied(reason) - } - CredHandler::Anonymous => Self::validate_anonymous(au, creds), + CredHandler::Anonymous => Self::validate_anonymous(au, cred), CredHandler::Password(ref mut pw) => { - Self::validate_password(au, creds, pw, who, async_tx) + Self::validate_password(au, cred, pw, who, async_tx) } - CredHandler::TOTPPassword(ref mut pw_totp) => { - Self::validate_totp_password(au, creds, ts, pw_totp, who, async_tx) + CredHandler::PasswordMFA(ref mut pw_totp) => { + Self::validate_totp_password(au, cred, ts, pw_totp, who, async_tx) } CredHandler::Webauthn(ref mut wan_cred) => { - Self::validate_webauthn(au, creds, wan_cred, webauthn, who, async_tx) + Self::validate_webauthn(au, cred, wan_cred, webauthn, who, async_tx) } } } - pub fn valid_auth_mechs(&self) -> Vec { + pub fn next_auth_allowed(&self) -> Vec { match &self { - CredHandler::Denied(_) => Vec::new(), CredHandler::Anonymous => vec![AuthAllowed::Anonymous], CredHandler::Password(_) => vec![AuthAllowed::Password], // webauth // mfa - CredHandler::TOTPPassword(_) => vec![AuthAllowed::Password, AuthAllowed::TOTP], + CredHandler::PasswordMFA(_) => vec![AuthAllowed::Password, AuthAllowed::TOTP], CredHandler::Webauthn(webauthn) => vec![AuthAllowed::Webauthn(webauthn.chal.clone())], } } - pub(crate) fn is_denied(&self) -> Option<&'static str> { + fn can_proceed(&self, mech: &AuthMech) -> bool { + match (self, mech) { + (CredHandler::Anonymous, AuthMech::Anonymous) + | (CredHandler::Password(_), AuthMech::Password) + | (CredHandler::PasswordMFA(_), AuthMech::PasswordMFA) + | (CredHandler::Webauthn(_), AuthMech::Webauthn) => true, + (_, _) => false, + } + } + + fn allows_mech(&self) -> AuthMech { + match self { + CredHandler::Anonymous => AuthMech::Anonymous, + CredHandler::Password(_) => AuthMech::Password, + CredHandler::PasswordMFA(_) => AuthMech::PasswordMFA, + CredHandler::Webauthn(_) => AuthMech::Webauthn, + } + } +} + +#[derive(Clone)] +/// This interleaves with the client auth step. The client sends an "init" +/// and we go to the init state, sending back the list of what can proceed. +/// The client then sends a "begin" with the chosen mech that moves to +/// "InProgress", "Success" or "Denied". From there the CredHandler +/// is interacted with until we move to either "Success" or "Denied". +enum AuthSessionState { + Init(Vec), + // Stop! Don't make this a vec - make the credhandler able to hold multiple + // internal copies of it's type and check against them all. + InProgress(CredHandler), + Success, + Denied(&'static str), +} + +impl AuthSessionState { + fn is_denied(&self) -> Option<&'static str> { match &self { - CredHandler::Denied(x) => Some(x), + AuthSessionState::Denied(x) => Some(x), _ => None, } } @@ -428,81 +378,67 @@ pub(crate) struct AuthSession { // we want the primary-interaction credentials. // // This handler will then handle the mfa and stepping up through to generate the auth states - handler: CredHandler, - // The identity of the credential that uniquely identifies it. - // cred_uuid: Uuid, - // Store any related appid we are processing for. - appid: Option, - // Store claims related to the handler - // need to store state somehow? - finished: bool, + state: AuthSessionState, } impl AuthSession { pub fn new( au: &mut AuditScope, account: Account, - appid: Option, + _appid: Option, webauthn: &Webauthn, ct: Duration, ) -> (Option, AuthState) { // During this setup, determine the credential handler that we'll be using // for this session. This is currently based on presentation of an application // id. - let handler = if account.is_within_valid_time(ct) { - match appid { - Some(_) => CredHandler::Denied("authentication denied"), - None => { - // We want the primary handler - this is where we make a decision - // based on the anonymous ... in theory this could be cleaner - // and interact with the account more? - if account.is_anonymous() { - CredHandler::Anonymous - } else { - // Now we see if they have one ... - match &account.primary { - Some(cred) => { - // Probably means new authsession has to be failable - CredHandler::try_from(au, cred, webauthn).unwrap_or_else(|_| { - lsecurity_critical!( - au, - "corrupt credentials, unable to start credhandler" - ); - CredHandler::Denied("invalid credential state") - }) - } - None => { - lsecurity!(au, "account has no primary credentials"); - CredHandler::Denied("invalid credential state") - } - } + let state = if account.is_within_valid_time(ct) { + // We want the primary handler - this is where we make a decision + // based on the anonymous ... in theory this could be cleaner + // and interact with the account more? + if account.is_anonymous() { + AuthSessionState::Init(vec![CredHandler::Anonymous]) + } else { + // Now we see if they have one ... + match &account.primary { + Some(cred) => { + // TODO: Make it possible to have multiple creds. + // Probably means new authsession has to be failable + CredHandler::try_from(au, cred, webauthn) + .map(|ch| AuthSessionState::Init(vec![ch])) + .unwrap_or_else(|_| { + lsecurity_critical!( + au, + "corrupt credentials, unable to start credhandler" + ); + AuthSessionState::Denied("invalid credential state") + }) + } + None => { + lsecurity!(au, "account has no primary credentials"); + AuthSessionState::Denied("invalid credential state") } } } } else { lsecurity!(au, "account expired"); - CredHandler::Denied(ACCOUNT_EXPIRED) + AuthSessionState::Denied(ACCOUNT_EXPIRED) }; // if credhandler == deny, finish = true. - if let Some(reason) = handler.is_denied() { + if let Some(reason) = state.is_denied() { // Already denied, lets send that result (None, AuthState::Denied(reason.to_string())) } else { // We can proceed - let auth_session = AuthSession { - account, - handler, - appid, - finished: false, - }; + let auth_session = AuthSession { account, state }; // Get the set of mechanisms that can proceed. This is tied // to the session so that it can mutate state and have progression // of what's next, or ordering. - let next_mech = auth_session.valid_auth_mechs(); + let valid_mechs = auth_session.valid_auth_mechs(); - let state = AuthState::Continue(next_mech); - (Some(auth_session), state) + let as_state = AuthState::Choose(valid_mechs); + (Some(auth_session), as_state) } } @@ -510,61 +446,113 @@ impl AuthSession { &self.account } - pub fn end_session(&mut self, reason: String) -> Result { - self.finished = true; - Ok(AuthState::Denied(reason)) + pub fn start_session( + &mut self, + _au: &mut AuditScope, + mech: &AuthMech, + // time: &Duration, + // webauthn: &Webauthn, + ) -> Result { + // Given some auth mech, select which credential(s) are apropriate + // and attempt to use them. + + // Today we only select one, but later we could have *multiple* that + // match the selector. + let (next_state, response) = match &mut self.state { + AuthSessionState::Success + | AuthSessionState::Denied(_) + | AuthSessionState::InProgress(_) => ( + None, + Err(OperationError::InvalidAuthState( + "session already finalised!".to_string(), + )), + ), + AuthSessionState::Init(handlers) => { + // Which handlers are relevant? + let mut allowed_handlers: Vec<_> = handlers + .iter() + .filter(|ch| ch.can_proceed(mech)) + .cloned() + .collect(); + + if let Some(allowed_handler) = allowed_handlers.pop() { + let allowed: Vec<_> = allowed_handler.next_auth_allowed(); + + if allowed.is_empty() { + ( + None, + Err(OperationError::InvalidAuthState( + "unable to negotitate credentials".to_string(), + )), + ) + } else { + ( + Some(AuthSessionState::InProgress(allowed_handler)), + Ok(AuthState::Continue(allowed)), + ) + } + } else { + ( + Some(AuthSessionState::Denied(BAD_CREDENTIALS)), + Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())), + ) + } + } + }; + + if let Some(mut next_state) = next_state { + std::mem::swap(&mut self.state, &mut next_state); + }; + + response } // This should return a AuthResult or similar state of checking? pub fn validate_creds( &mut self, au: &mut AuditScope, - creds: &[AuthCredential], + cred: &AuthCredential, time: &Duration, async_tx: &Sender, webauthn: &Webauthn, ) -> Result { - if self.finished { - return Err(OperationError::InvalidAuthState( - "session already finalised!".to_string(), - )); - } - - if creds.len() > 4 { - lsecurity!( - au, - "Credentials denied: potential flood/dos/bruteforce attempt. {} creds were sent.", - creds.len() - ); - self.finished = true; - return Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())); - } - - match self - .handler - .validate(au, creds, time, self.account.uuid, async_tx, webauthn) - { - CredState::Success(claims) => { - lsecurity!(au, "Successful cred handling"); - self.finished = true; - let uat = self - .account - .to_userauthtoken(&claims) - .ok_or(OperationError::InvalidState)?; - - // Now encrypt and prepare the token for return to the client. - Ok(AuthState::Success(uat)) + let (next_state, response) = match &mut self.state { + AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => { + return Err(OperationError::InvalidAuthState( + "session already finalised!".to_string(), + )); } - CredState::Continue(allowed) => { - lsecurity!(au, "Request credential continuation: {:?}", allowed); - Ok(AuthState::Continue(allowed)) + AuthSessionState::InProgress(ref mut handler) => { + match handler.validate(au, cred, time, self.account.uuid, async_tx, webauthn) { + CredState::Success(claims) => { + lsecurity!(au, "Successful cred handling"); + let uat = self + .account + .to_userauthtoken(&claims) + .ok_or(OperationError::InvalidState)?; + + // Now encrypt and prepare the token for return to the client. + (Some(AuthSessionState::Success), Ok(AuthState::Success(uat))) + } + CredState::Continue(allowed) => { + lsecurity!(au, "Request credential continuation: {:?}", allowed); + (None, Ok(AuthState::Continue(allowed))) + } + CredState::Denied(reason) => { + lsecurity!(au, "Credentials denied: {}", reason); + ( + Some(AuthSessionState::Denied(reason)), + Ok(AuthState::Denied(reason.to_string())), + ) + } + } } - CredState::Denied(reason) => { - self.finished = true; - lsecurity!(au, "Credentials denied: {}", reason); - Ok(AuthState::Denied(reason.to_string())) - } - } + }; + + if let Some(mut next_state) = next_state { + std::mem::swap(&mut self.state, &mut next_state); + }; + // Also send an async message to self to log the auth as provided. // Alternately, open a write, and commit the needed security metadata here // now rather than async (probably better for lock-outs etc) @@ -576,13 +564,26 @@ impl AuthSession { // If this suceeds audit? // If success, to authtoken? + + response } - fn valid_auth_mechs(&self) -> Vec { - if self.finished { - Vec::new() - } else { - self.handler.valid_auth_mechs() + pub fn end_session(&mut self, reason: &'static str) -> Result { + let mut next_state = AuthSessionState::Denied(reason); + std::mem::swap(&mut self.state, &mut next_state); + Ok(AuthState::Denied(reason.to_string())) + } + + fn valid_auth_mechs(&self) -> Vec { + match &self.state { + AuthSessionState::Success + | AuthSessionState::Denied(_) + | AuthSessionState::InProgress(_) => Vec::new(), + AuthSessionState::Init(handlers) => { + // Iterate over the handlers into what mechs they are + // and filter to unique? + handlers.iter().map(|h| h.allows_mech()).collect() + } } } } @@ -596,13 +597,12 @@ mod tests { use crate::credential::webauthn::WebauthnDomainConfig; use crate::credential::Credential; use crate::idm::authsession::{ - AuthSession, BAD_AUTH_TYPE_MSG, BAD_CREDENTIALS, BAD_PASSWORD_MSG, BAD_TOTP_MSG, - BAD_WEBAUTHN_MSG, + AuthSession, BAD_AUTH_TYPE_MSG, BAD_PASSWORD_MSG, BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, }; use crate::idm::delayed::DelayedAction; use crate::idm::AuthState; use crate::utils::duration_from_epoch_now; - use kanidm_proto::v1::{AuthAllowed, AuthCredential}; + use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthMech}; use std::time::Duration; use webauthn_rs::proto::UserVerificationPolicy; use webauthn_rs::Webauthn; @@ -629,7 +629,7 @@ mod tests { let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1); - let (_session, state) = AuthSession::new( + let (session, state) = AuthSession::new( &mut audit, anon_account, None, @@ -637,6 +637,22 @@ mod tests { duration_from_epoch_now(), ); + if let AuthState::Choose(auth_mechs) = state { + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthMech::Anonymous => true, + _ => acc, + }) + ); + } else { + panic!("Invalid auth state") + } + + let state = session + .expect("Missing auth session?") + .start_session(&mut audit, &AuthMech::Anonymous) + .expect("Failed to select anonymous mech."); + if let AuthState::Continue(auth_mechs) = state { assert!( true == auth_mechs.iter().fold(false, |acc, x| match x { @@ -649,50 +665,7 @@ mod tests { } } - #[test] - fn test_idm_authsession_floodcheck_mech() { - let mut audit = AuditScope::new( - "test_idm_authsession_floodcheck_mech", - uuid::Uuid::new_v4(), - None, - ); - let webauthn = create_webauthn(); - let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1); - let (session, _) = AuthSession::new( - &mut audit, - anon_account, - None, - &webauthn, - duration_from_epoch_now(), - ); - let (async_tx, mut async_rx) = unbounded(); - - // Will be some. - let mut session = session.unwrap(); - - let attempt = vec![ - AuthCredential::Anonymous, - AuthCredential::Anonymous, - AuthCredential::Anonymous, - AuthCredential::Anonymous, - AuthCredential::Anonymous, - ]; - match session.validate_creds( - &mut audit, - &attempt, - &Duration::from_secs(0), - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => { - assert!(msg == BAD_CREDENTIALS); - } - _ => panic!(), - }; - assert!(async_rx.try_recv().is_err()); - audit.write_log(); - } - + // Deprecated, will remove later. #[test] fn test_idm_authsession_missing_appid() { let webauthn = create_webauthn(); @@ -711,15 +684,61 @@ mod tests { duration_from_epoch_now(), ); - assert!(session.is_none()); + // We now ignore appids. + assert!(session.is_some()); - if let AuthState::Denied(_) = state { + if let AuthState::Choose(_) = state { // Pass } else { panic!(); } } + macro_rules! start_password_session { + ( + $audit:expr, + $account:expr, + $webauthn:expr + ) => {{ + let (session, state) = AuthSession::new( + $audit, + $account.clone(), + None, + $webauthn, + duration_from_epoch_now(), + ); + let mut session = session.unwrap(); + + if let AuthState::Choose(auth_mechs) = state { + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthMech::Password => true, + _ => acc, + }) + ); + } else { + panic!(); + } + + let state = session + .start_session($audit, &AuthMech::Password) + .expect("Failed to select anonymous mech."); + + if let AuthState::Continue(auth_mechs) = state { + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthAllowed::Password => true, + _ => acc, + }) + ); + } else { + panic!("Invalid auth state") + } + + session + }}; + } + #[test] fn test_idm_authsession_simple_password_mech() { let mut audit = AuditScope::new( @@ -735,28 +754,12 @@ mod tests { let cred = Credential::new_password_only(&p, "test_password").unwrap(); account.primary = Some(cred); - // now check - let (session, state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); let (async_tx, mut async_rx) = unbounded(); - if let AuthState::Continue(auth_mechs) = state { - assert!( - true == auth_mechs.iter().fold(false, |acc, x| match x { - AuthAllowed::Password => true, - _ => acc, - }) - ); - } else { - panic!(); - } - let attempt = vec![AuthCredential::Password("bad_password".to_string())]; + // now check + let mut session = start_password_session!(&mut audit, account, &webauthn); + + let attempt = AuthCredential::Password("bad_password".to_string()); match session.validate_creds( &mut audit, &attempt, @@ -768,15 +771,11 @@ mod tests { _ => panic!(), }; - let (session, _state) = AuthSession::new( - &mut audit, - account, - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - let attempt = vec![AuthCredential::Password("test_password".to_string())]; + // === Now begin a new session, and use a good pw. + + let mut session = start_password_session!(&mut audit, account, &webauthn); + + let attempt = AuthCredential::Password("test_password".to_string()); match session.validate_creds( &mut audit, &attempt, @@ -792,6 +791,52 @@ mod tests { audit.write_log(); } + macro_rules! start_password_mfa_session { + ( + $audit:expr, + $account:expr, + $webauthn:expr + ) => {{ + let (session, state) = AuthSession::new( + $audit, + $account.clone(), + None, + $webauthn, + duration_from_epoch_now(), + ); + let mut session = session.unwrap(); + + if let AuthState::Choose(auth_mechs) = state { + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthMech::PasswordMFA => true, + _ => acc, + }) + ); + } else { + panic!(); + } + + let state = session + .start_session($audit, &AuthMech::PasswordMFA) + .expect("Failed to select anonymous mech."); + + if let AuthState::Continue(auth_mechs) = state { + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + // TODO: How to return webauthn chal? + AuthAllowed::TOTP => true, + _ => acc, + }) + ); + } else { + panic!("Invalid auth state") + } + + session + }}; + } + #[test] fn test_idm_authsession_totp_password_mech() { let mut audit = AuditScope::new( @@ -827,40 +872,17 @@ mod tests { // add totp also account.primary = Some(cred); - // now check - let (_session, state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); let (async_tx, mut async_rx) = unbounded(); - if let AuthState::Continue(auth_mechs) = state { - assert!(auth_mechs.iter().fold(true, |acc, x| match x { - AuthAllowed::Password => acc, - AuthAllowed::TOTP => acc, - _ => false, - })); - } else { - panic!(); - } - // Rest of test go here + // now check // check send anon (fail) { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); + let mut session = start_password_mfa_session!(&mut audit, account, &webauthn); + match session.validate_creds( &mut audit, - &vec![AuthCredential::Anonymous], + &AuthCredential::Anonymous, &ts, &async_tx, &webauthn, @@ -872,150 +894,28 @@ mod tests { // == two step checks - // check send bad pw, should get continue (even though denied set) - // then send good totp, should fail. + // Sending a PW first is an immediate fail. { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![AuthCredential::Password(pw_bad.to_string())], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::TOTP]), - _ => panic!(), - }; - match session.validate_creds( - &mut audit, - &vec![AuthCredential::TOTP(totp_good)], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), - _ => panic!(), - }; - } - // check send bad pw, should get continue (even though denied set) - // then send bad totp, should fail TOTP - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![AuthCredential::Password(pw_bad.to_string())], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::TOTP]), - _ => panic!(), - }; - match session.validate_creds( - &mut audit, - &vec![AuthCredential::TOTP(totp_bad)], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), - _ => panic!(), - }; - } + let mut session = start_password_mfa_session!(&mut audit, account, &webauthn); - // check send good pw, should get continue - // then send good totp, success - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); match session.validate_creds( &mut audit, - &vec![AuthCredential::Password(pw_good.to_string())], + &AuthCredential::Password(pw_bad.to_string()), &ts, &async_tx, &webauthn, ) { - Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::TOTP]), - _ => panic!(), - }; - match session.validate_creds( - &mut audit, - &vec![AuthCredential::TOTP(totp_good)], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Success(_)) => {} + Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; } - - // check send good pw, should get continue - // then send bad totp, fail otp - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![AuthCredential::Password(pw_good.to_string())], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::TOTP]), - _ => panic!(), - }; - match session.validate_creds( - &mut audit, - &vec![AuthCredential::TOTP(totp_bad)], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), - _ => panic!(), - }; - } - // check send bad totp, should fail immediate { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); + let mut session = start_password_mfa_session!(&mut audit, account, &webauthn); + match session.validate_creds( &mut audit, - &vec![AuthCredential::TOTP(totp_bad)], + &AuthCredential::TOTP(totp_bad), &ts, &async_tx, &webauthn, @@ -1028,17 +928,11 @@ mod tests { // check send good totp, should continue // then bad pw, fail pw { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); + let mut session = start_password_mfa_session!(&mut audit, account, &webauthn); + match session.validate_creds( &mut audit, - &vec![AuthCredential::TOTP(totp_good)], + &AuthCredential::TOTP(totp_good), &ts, &async_tx, &webauthn, @@ -1048,7 +942,7 @@ mod tests { }; match session.validate_creds( &mut audit, - &vec![AuthCredential::Password(pw_bad.to_string())], + &AuthCredential::Password(pw_bad.to_string()), &ts, &async_tx, &webauthn, @@ -1061,17 +955,11 @@ mod tests { // check send good totp, should continue // then good pw, success { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); + let mut session = start_password_mfa_session!(&mut audit, account, &webauthn); + match session.validate_creds( &mut audit, - &vec![AuthCredential::TOTP(totp_good)], + &AuthCredential::TOTP(totp_good), &ts, &async_tx, &webauthn, @@ -1081,106 +969,7 @@ mod tests { }; match session.validate_creds( &mut audit, - &vec![AuthCredential::Password(pw_good.to_string())], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Success(_)) => {} - _ => panic!(), - }; - } - - // == one step checks - - // check bad totp, bad pw, fail totp. - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![ - AuthCredential::Password(pw_bad.to_string()), - AuthCredential::TOTP(totp_bad), - ], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), - _ => panic!(), - }; - } - // check send bad pw, good totp fail password - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![ - AuthCredential::TOTP(totp_good), - AuthCredential::Password(pw_bad.to_string()), - ], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), - _ => panic!(), - }; - } - // check send good pw, bad totp fail totp. - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![ - AuthCredential::TOTP(totp_bad), - AuthCredential::Password(pw_good.to_string()), - ], - &ts, - &async_tx, - &webauthn, - ) { - Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), - _ => panic!(), - }; - } - // check good pw, good totp, success - { - let (session, _state) = AuthSession::new( - &mut audit, - account.clone(), - None, - &webauthn, - duration_from_epoch_now(), - ); - let mut session = session.unwrap(); - match session.validate_creds( - &mut audit, - &vec![ - AuthCredential::TOTP(totp_good), - AuthCredential::Password(pw_good.to_string()), - ], + &AuthCredential::Password(pw_good.to_string()), &ts, &async_tx, &webauthn, @@ -1194,6 +983,53 @@ mod tests { audit.write_log(); } + macro_rules! start_webauthn_only_session { + ( + $audit:expr, + $account:expr, + $webauthn:expr + ) => {{ + let (session, state) = AuthSession::new( + $audit, + $account.clone(), + None, + $webauthn, + duration_from_epoch_now(), + ); + let mut session = session.unwrap(); + + if let AuthState::Choose(auth_mechs) = state { + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthMech::Webauthn => true, + _ => acc, + }) + ); + } else { + panic!(); + } + + let state = session + .start_session($audit, &AuthMech::Webauthn) + .expect("Failed to select Webauthn mech."); + + let wan_chal = if let AuthState::Continue(auth_mechs) = state { + assert!(auth_mechs.len() == 1); + auth_mechs + .into_iter() + .fold(None, |_acc, x| match x { + AuthAllowed::Webauthn(chal) => Some(chal), + _ => None, + }) + .expect("No webauthn challenge found.") + } else { + panic!(); + }; + + (session, wan_chal) + }}; + } + #[test] fn test_idm_authsession_webauthn_only_mech() { let mut audit = AuditScope::new( @@ -1228,28 +1064,16 @@ mod tests { // now check correct mech was offered. we stash this challenge for later // to help generate a failure. - let (_session, state) = AuthSession::new(&mut audit, account.clone(), None, &webauthn, ts); - let inv_chal = if let AuthState::Continue(auth_mechs) = state { - assert!(auth_mechs.len() == 1); - auth_mechs - .into_iter() - .fold(None, |_acc, x| match x { - AuthAllowed::Webauthn(chal) => Some(chal), - _ => None, - }) - .expect("No webauthn challenge found.") - } else { - panic!(); - }; + let (_session, inv_chal) = start_webauthn_only_session!(&mut audit, account, &webauthn); // check send anon (fail) { - let (session, _state) = - AuthSession::new(&mut audit, account.clone(), None, &webauthn, ts); - let mut session = session.unwrap(); + let (mut session, _inv_chal) = + start_webauthn_only_session!(&mut audit, account, &webauthn); + match session.validate_creds( &mut audit, - &vec![AuthCredential::Anonymous], + &AuthCredential::Anonymous, &ts, &async_tx, &webauthn, @@ -1261,26 +1085,15 @@ mod tests { // Check good challenge { - let (session, state) = - AuthSession::new(&mut audit, account.clone(), None, &webauthn, ts); + let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn); - let resp = if let AuthState::Continue(mut auth_mechs) = state { - match auth_mechs.pop() { - Some(AuthAllowed::Webauthn(chal)) => wa - .do_authentication("https://idm.example.com", chal) - .expect("failed to use softtoken to authenticate"), - _ => { - panic!(); - } - } - } else { - panic!(); - }; + let resp = wa + .do_authentication("https://idm.example.com", chal) + .expect("failed to use softtoken to authenticate"); - let mut session = session.unwrap(); match session.validate_creds( &mut audit, - &vec![AuthCredential::Webauthn(resp)], + &AuthCredential::Webauthn(resp), &ts, &async_tx, &webauthn, @@ -1298,28 +1111,16 @@ mod tests { // Check bad challenge. { - let (session, state) = - AuthSession::new(&mut audit, account.clone(), None, &webauthn, ts); + let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn); - let resp = if let AuthState::Continue(mut auth_mechs) = state { - match auth_mechs.pop() { - Some(AuthAllowed::Webauthn(_chal)) => { - // HERE -> we use inv_chal instead. - wa.do_authentication("https://idm.example.com", inv_chal) - .expect("failed to use softtoken to authenticate") - } - _ => { - panic!(); - } - } - } else { - panic!(); - }; + let resp = wa + // HERE -> we use inv_chal instead. + .do_authentication("https://idm.example.com", inv_chal) + .expect("failed to use softtoken to authenticate"); - let mut session = session.unwrap(); match session.validate_creds( &mut audit, - &vec![AuthCredential::Webauthn(resp)], + &AuthCredential::Webauthn(resp), &ts, &async_tx, &webauthn, @@ -1328,8 +1129,8 @@ mod tests { _ => panic!(), }; } - // Use an incorrect softtoken. + // Use an incorrect softtoken. { let mut inv_wa = WebauthnAuthenticator::new(U2FSoft::new()); let (chal, reg_state) = webauthn @@ -1357,16 +1158,13 @@ mod tests { .do_authentication("https://idm.example.com", chal) .expect("Failed to use softtoken for response."); - let (session, _state) = - AuthSession::new(&mut audit, account.clone(), None, &webauthn, ts); - + let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn); // Ignore the real cred, use the diff cred. Normally this shouldn't even // get this far, because the client should identify that the cred id's are // not inline. - let mut session = session.unwrap(); match session.validate_creds( &mut audit, - &vec![AuthCredential::Webauthn(resp)], + &AuthCredential::Webauthn(resp), &ts, &async_tx, &webauthn, diff --git a/kanidmd/src/lib/idm/mod.rs b/kanidmd/src/lib/idm/mod.rs index 35e468e83..c6dc824db 100644 --- a/kanidmd/src/lib/idm/mod.rs +++ b/kanidmd/src/lib/idm/mod.rs @@ -10,11 +10,12 @@ pub(crate) mod server; pub(crate) mod unix; // mod identity; -use kanidm_proto::v1::{AuthAllowed, UserAuthToken}; +use kanidm_proto::v1::{AuthAllowed, AuthMech, UserAuthToken}; #[derive(Debug)] pub enum AuthState { - Success(UserAuthToken), - Denied(String), + Choose(Vec), Continue(Vec), + Denied(String), + Success(UserAuthToken), } diff --git a/kanidmd/src/lib/idm/server.rs b/kanidmd/src/lib/idm/server.rs index 30f9c1b7f..275b617d3 100644 --- a/kanidmd/src/lib/idm/server.rs +++ b/kanidmd/src/lib/idm/server.rs @@ -418,9 +418,58 @@ impl<'a> IdmServerWriteTransaction<'a> { state, delay, }) - // }) - } - AuthEventStep::Creds(creds) => { + } // AuthEventStep::Init + AuthEventStep::Begin(mech) => { + // lperf_segment!(au, "idm::server::auth", || { + let _session_ticket = self.session_ticket.acquire().await; + let _softlock_ticket = self.softlock_ticket.acquire().await; + + let mut session_write = self.sessions.write(); + // Do we have a session? + let auth_session = session_write + // Why is the session missing? + .get_mut(&mech.sessionid) + .ok_or_else(|| { + ladmin_error!(au, "Invalid Session State (no present session uuid)"); + OperationError::InvalidSessionState + })?; + + // From the auth_session, determine if the current account + // credential that we are using has become softlocked or not. + let mut softlock_write = self.softlocks.write(); + + let cred_uuid = auth_session.get_account().primary_cred_uuid(); + + let is_valid = softlock_write + .get_mut(&cred_uuid) + .map(|slock| { + // Apply the current time. + slock.apply_time_step(ct); + // Now check the results + slock.is_valid() + }) + .unwrap_or(true); + + let r = if is_valid { + // Indicate to the session which auth mech we now want to proceed with. + auth_session.start_session(au, &mech.mech) + } else { + // Fail the session + auth_session.end_session("Account is temporarily locked") + } + .map(|aus| { + let delay = None; + AuthResult { + sessionid: mech.sessionid, + state: aus, + delay, + } + }); + softlock_write.commit(); + session_write.commit(); + r + } // End AuthEventStep::Mech + AuthEventStep::Cred(creds) => { // lperf_segment!(au, "idm::server::auth", || { let _session_ticket = self.session_ticket.acquire().await; let _softlock_ticket = self.softlock_ticket.acquire().await; @@ -456,7 +505,7 @@ impl<'a> IdmServerWriteTransaction<'a> { // Basically throw them at the auth_session and see what // falls out. auth_session - .validate_creds(au, &creds.creds, &ct, &self.async_tx, self.webauthn) + .validate_creds(au, &creds.cred, &ct, &self.async_tx, self.webauthn) .map(|aus| { // Inspect the result: // if it was a failure, we need to inc the softlock. @@ -479,7 +528,7 @@ impl<'a> IdmServerWriteTransaction<'a> { }) } else { // Fail the session - auth_session.end_session("Account is temporarily locked".to_string()) + auth_session.end_session("Account is temporarily locked") } .map(|aus| { // TODO: Change this william! @@ -495,8 +544,7 @@ impl<'a> IdmServerWriteTransaction<'a> { softlock_write.commit(); session_write.commit(); r - // }) - } + } // End AuthEventStep::Cred } } @@ -1382,9 +1430,9 @@ mod tests { use crate::idm::AuthState; use crate::modify::{Modify, ModifyList}; use crate::value::{PartialValue, Value}; - use kanidm_proto::v1::AuthAllowed; use kanidm_proto::v1::OperationError; use kanidm_proto::v1::SetCredentialResponse; + use kanidm_proto::v1::{AuthAllowed, AuthMech}; use crate::audit::AuditScope; use crate::idm::server::IdmServer; @@ -1430,12 +1478,12 @@ mod tests { } = ar; debug_assert!(delay.is_none()); match state { - AuthState::Continue(mut conts) => { + AuthState::Choose(mut conts) => { // Should only be one auth mech assert!(conts.len() == 1); // And it should be anonymous let m = conts.pop().expect("Should not fail"); - assert!(m == AuthAllowed::Anonymous); + assert!(m == AuthMech::Anonymous); } _ => { error!( @@ -1460,6 +1508,49 @@ mod tests { sid }; + { + let mut idms_write = idms.write(); + let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous); + + let r2 = task::block_on(idms_write.auth( + au, + &anon_begin, + Duration::from_secs(TEST_CURRENT_TIME), + )); + debug!("r2 ==> {:?}", r2); + + match r2 { + Ok(ar) => { + let AuthResult { + sessionid: _, + state, + delay, + } = ar; + + debug_assert!(delay.is_none()); + match state { + AuthState::Continue(allowed) => { + // Check the uat. + assert!(allowed.len() == 1); + assert!(allowed.first() == Some(&AuthAllowed::Anonymous)); + } + _ => { + error!( + "A critical error has occured! We have a non-continue result!" + ); + panic!(); + } + } + } + Err(e) => { + error!("A critical error has occured! {:?}", e); + // Should not occur! + panic!(); + } + }; + + idms_write.commit(au).expect("Must not fail"); + }; { let mut idms_write = idms.write(); // Now send the anonymous request, given the session id. @@ -1566,9 +1657,14 @@ mod tests { qs_write.commit(au) } - fn init_admin_authsession_sid(idms: &IdmServer, au: &mut AuditScope, ct: Duration) -> Uuid { + fn init_admin_authsession_sid( + idms: &IdmServer, + au: &mut AuditScope, + ct: Duration, + name: &str, + ) -> Uuid { let mut idms_write = idms.write(); - let admin_init = AuthEvent::named_init("admin"); + let admin_init = AuthEvent::named_init(name); let r1 = task::block_on(idms_write.auth(au, &admin_init, ct)); let ar = r1.unwrap(); @@ -1578,6 +1674,26 @@ mod tests { delay, } = ar; + debug_assert!(delay.is_none()); + match state { + AuthState::Choose(_) => {} + _ => { + error!("Sessions was not initialised"); + panic!(); + } + }; + + // Now push that we want the Password Mech. + let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); + + let r2 = task::block_on(idms_write.auth(au, &admin_begin, ct)); + let ar = r2.unwrap(); + let AuthResult { + sessionid, + state, + delay, + } = ar; + debug_assert!(delay.is_none()); match state { @@ -1594,7 +1710,8 @@ mod tests { } fn check_admin_password(idms: &IdmServer, au: &mut AuditScope, pw: &str) { - let sid = init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME)); + let sid = + init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME), "admin"); let mut idms_write = idms.write(); let anon_step = AuthEvent::cred_step_password(sid, pw); @@ -1652,33 +1769,13 @@ mod tests { _idms_delayed: &IdmServerDelayed, au: &mut AuditScope| { init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); - let mut idms_write = idms.write(); - let admin_init = AuthEvent::named_init("admin@example.com"); - let r1 = task::block_on(idms_write.auth( + let sid = init_admin_authsession_sid( + idms, au, - &admin_init, Duration::from_secs(TEST_CURRENT_TIME), - )); - let ar = r1.unwrap(); - let AuthResult { - sessionid, - state, - delay, - } = ar; - - debug_assert!(delay.is_none()); - match state { - AuthState::Continue(_) => {} - _ => { - error!("Sessions was not initialised"); - panic!(); - } - }; - - idms_write.commit(au).expect("Must not fail"); - - let sid = sessionid; + "admin@example.com", + ); let mut idms_write = idms.write(); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD); @@ -1727,7 +1824,12 @@ mod tests { _idms_delayed: &IdmServerDelayed, au: &mut AuditScope| { init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); - let sid = init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME)); + let sid = init_admin_authsession_sid( + idms, + au, + Duration::from_secs(TEST_CURRENT_TIME), + "admin", + ); let mut idms_write = idms.write(); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); @@ -1804,7 +1906,12 @@ mod tests { _idms_delayed: &IdmServerDelayed, au: &mut AuditScope| { init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); - let sid = init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME)); + let sid = init_admin_authsession_sid( + idms, + au, + Duration::from_secs(TEST_CURRENT_TIME), + "admin", + ); let mut idms_write = idms.write(); assert!(idms_write.is_sessionid_present(&sid)); // Expire like we are currently "now". Should not affect our session. @@ -2503,7 +2610,12 @@ mod tests { init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); // Auth invalid, no softlock present. - let sid = init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME)); + let sid = init_admin_authsession_sid( + idms, + au, + Duration::from_secs(TEST_CURRENT_TIME), + "admin", + ); let mut idms_write = idms.write(); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); @@ -2574,8 +2686,12 @@ mod tests { // Tested in the softlock state machine. // Auth valid once softlock pass, valid. Count remains. - let sid = - init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME + 2)); + let sid = init_admin_authsession_sid( + idms, + au, + Duration::from_secs(TEST_CURRENT_TIME + 2), + "admin", + ); let mut idms_write = idms.write(); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD); @@ -2632,12 +2748,20 @@ mod tests { init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); // Start an *early* auth session. - let sid_early = - init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME)); + let sid_early = init_admin_authsession_sid( + idms, + au, + Duration::from_secs(TEST_CURRENT_TIME), + "admin", + ); // Start a second auth session - let sid_later = - init_admin_authsession_sid(idms, au, Duration::from_secs(TEST_CURRENT_TIME)); + let sid_later = init_admin_authsession_sid( + idms, + au, + Duration::from_secs(TEST_CURRENT_TIME), + "admin", + ); // Get the detail wrong in sid_later. let mut idms_write = idms.write(); let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC); @@ -2822,7 +2946,7 @@ mod tests { let cred = account.primary.expect("Must exist."); let wcred = cred - .webauthn + .webauthn_ref() .expect("must have webauthn") .values() .next() diff --git a/kanidmd/src/lib/idm/unix.rs b/kanidmd/src/lib/idm/unix.rs index 2c6e6aa51..3d6bf9c93 100644 --- a/kanidmd/src/lib/idm/unix.rs +++ b/kanidmd/src/lib/idm/unix.rs @@ -228,39 +228,33 @@ impl UnixUserAccount { // is the cred some or none? match &self.cred { Some(cred) => { - match &cred.password { - Some(pw) => { - if pw.verify(cleartext)? { - lsecurity!(au, "Successful unix cred handling"); - if pw.requires_upgrade() { - async_tx.send( - DelayedAction::UnixPwUpgrade(UnixPasswordUpgrade { + cred.password_ref().and_then(|pw| { + if pw.verify(cleartext)? { + lsecurity!(au, "Successful unix cred handling"); + if pw.requires_upgrade() { + async_tx + .send(DelayedAction::UnixPwUpgrade(UnixPasswordUpgrade { target_uuid: self.uuid, existing_password: cleartext.to_string(), - }) - ).map_err(|_| { - ladmin_error!(au, "failed to queue delayed action - unix password upgrade"); - OperationError::InvalidState - })?; - } - - // Technically this means we check the times twice, but that doesn't - // seem like a big deal when we want to short cut return on invalid. - Some(self.to_unixusertoken(ct)).transpose() - } else { - // Failed to auth - lsecurity!(au, "Failed unix cred handling (denied)"); - Ok(None) + })) + .map_err(|_| { + ladmin_error!( + au, + "failed to queue delayed action - unix password upgrade" + ); + OperationError::InvalidState + })?; } + + // Technically this means we check the times twice, but that doesn't + // seem like a big deal when we want to short cut return on invalid. + Some(self.to_unixusertoken(ct)).transpose() + } else { + // Failed to auth + lsecurity!(au, "Failed unix cred handling (denied)"); + Ok(None) } - // We have a cred but it's not a password, that's weird - None => { - lsecurity!(au, "Invalid unix cred request"); - Err(OperationError::InvalidAccountState( - "non-password cred type?".to_string(), - )) - } - } + }) } // They don't have a unix cred, fail the auth. None => { @@ -272,10 +266,7 @@ impl UnixUserAccount { pub(crate) fn check_existing_pw(&self, cleartext: &str) -> Result { match &self.cred { - Some(cred) => match &cred.password { - Some(pw) => pw.verify(cleartext), - None => Err(OperationError::InvalidState), - }, + Some(cred) => cred.password_ref().and_then(|pw| pw.verify(cleartext)), None => Err(OperationError::InvalidState), } } diff --git a/kanidmd/src/lib/plugins/password_import.rs b/kanidmd/src/lib/plugins/password_import.rs index a0a971f6f..0f5ec8106 100644 --- a/kanidmd/src/lib/plugins/password_import.rs +++ b/kanidmd/src/lib/plugins/password_import.rs @@ -128,7 +128,7 @@ impl Plugin for PasswordImport { mod tests { use crate::credential::policy::CryptoPolicy; use crate::credential::totp::{TOTP, TOTP_DEFAULT_STEP}; - use crate::credential::Credential; + use crate::credential::{Credential, CredentialType}; use crate::entry::{Entry, EntryInit, EntryNew}; use crate::modify::{Modify, ModifyList}; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; @@ -268,8 +268,13 @@ mod tests { let c = e .get_ava_single_credential("primary_credential") .expect("failed to get primary cred."); - assert!(c.totp.is_some()); - assert!(c.password.is_some()); + match &c.type_ { + CredentialType::PasswordMFA(_pw, totp, webauthn) => { + assert!(totp.is_some()); + assert!(webauthn.is_empty()); + } + _ => assert!(false), + }; } ); } diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index 8b5f1f2ca..f4eb70237 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -3556,7 +3556,7 @@ mod tests { .get_ava_single_credential("primary_credential") .expect("Failed"); // do a pw check. - assert!(cred_ref.verify_password("test_password")); + assert!(cred_ref.verify_password("test_password").unwrap()); }) }