diff --git a/Cargo.lock b/Cargo.lock index 6e501ed50..4b7ba8b3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", "version_check", ] @@ -152,18 +152,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "asn1-rs" version = "0.3.1" @@ -279,7 +267,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg", - "cfg-if 1.0.0", + "cfg-if", "concurrent-queue", "futures-lite", "log", @@ -311,27 +299,6 @@ dependencies = [ "syn 2.0.23", ] -[[package]] -name = "async-session" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630" -dependencies = [ - "anyhow", - "async-lock", - "async-trait", - "base64 0.13.1", - "bincode", - "blake3", - "chrono", - "hmac 0.11.0", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "sha2 0.9.9", -] - [[package]] name = "async-std" version = "1.12.0" @@ -400,7 +367,7 @@ checksum = "d06c690e5e2800f70c0cf8773a9fe7680d66e719dae9b4cabedd13ef4885d056" dependencies = [ "base64 0.13.1", "bitflags 1.3.2", - "cfg-if 1.0.0", + "cfg-if", "core-foundation", "devd-rs", "libc", @@ -503,29 +470,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "axum-extra" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "febf23ab04509bd7672e6abe76bd8277af31b679e89fa5ffc6087dc289a448a3" -dependencies = [ - "axum", - "axum-core", - "bytes", - "cookie 0.17.0", - "futures-util", - "http", - "http-body", - "mime", - "pin-project-lite", - "serde", - "tokio", - "tower", - "tower-http", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-macros" version = "0.3.7" @@ -556,22 +500,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum-sessions" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714cad544cd87d8da821cda715bb9aaa5d4d1adbdb64c549b18138e3cbf93c44" -dependencies = [ - "async-session", - "axum", - "axum-extra", - "futures", - "http-body", - "tokio", - "tower", - "tracing", -] - [[package]] name = "backtrace" version = "0.3.68" @@ -580,7 +508,7 @@ checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", "object", @@ -708,21 +636,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "blake3" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac 0.8.0", - "digest 0.9.0", -] - [[package]] name = "block-buffer" version = "0.7.3" @@ -837,12 +750,6 @@ dependencies = [ "nom", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -1034,7 +941,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen", ] @@ -1044,12 +951,6 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "cookie" version = "0.14.4" @@ -1059,7 +960,7 @@ dependencies = [ "aes-gcm", "base64 0.13.1", "hkdf", - "hmac 0.10.1", + "hmac", "percent-encoding", "rand 0.8.5", "sha2 0.9.9", @@ -1078,22 +979,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "cookie" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" -dependencies = [ - "base64 0.21.2", - "hmac 0.12.1", - "percent-encoding", - "rand 0.8.5", - "sha2 0.10.7", - "subtle", - "time 0.3.22", - "version_check", -] - [[package]] name = "cookie_store" version = "0.16.2" @@ -1148,7 +1033,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1204,7 +1089,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", @@ -1218,7 +1103,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] @@ -1228,7 +1113,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] @@ -1240,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", "memoffset 0.9.0", "scopeguard", @@ -1252,7 +1137,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] @@ -1262,7 +1147,7 @@ version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1275,16 +1160,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array 0.14.7", - "subtle", -] - [[package]] name = "crypto-mac" version = "0.10.1" @@ -1295,16 +1170,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array 0.14.7", - "subtle", -] - [[package]] name = "csv" version = "1.2.2" @@ -1561,7 +1426,7 @@ version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1682,7 +1547,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall 0.2.16", "windows-sys 0.48.0", @@ -1887,7 +1752,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -1898,7 +1763,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2224,7 +2089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ "digest 0.9.0", - "hmac 0.10.1", + "hmac", ] [[package]] @@ -2233,29 +2098,10 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ - "crypto-mac 0.10.1", + "crypto-mac", "digest 0.9.0", ] -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac 0.11.1", - "digest 0.9.0", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hostname-validator" version = "1.1.1" @@ -2507,7 +2353,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -2755,7 +2601,6 @@ dependencies = [ "axum-csp", "axum-macros", "axum-server", - "axum-sessions", "chrono", "compact_jwt", "cron", @@ -3022,7 +2867,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "winapi", ] @@ -3520,7 +3365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ "bitflags 1.3.2", - "cfg-if 1.0.0", + "cfg-if", "foreign-types", "libc", "once_cell", @@ -3628,7 +3473,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall 0.3.5", "smallvec", @@ -3840,7 +3685,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", - "cfg-if 1.0.0", + "cfg-if", "concurrent-queue", "libc", "log", @@ -4596,7 +4441,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.7", ] @@ -4626,7 +4471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", @@ -4638,7 +4483,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.7", ] @@ -4894,7 +4739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ "autocfg", - "cfg-if 1.0.0", + "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix 0.37.22", @@ -4951,7 +4796,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] @@ -5252,7 +5097,7 @@ version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -5549,7 +5394,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "serde", "serde_json", "wasm-bindgen-macro", @@ -5576,7 +5421,7 @@ version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", diff --git a/Makefile b/Makefile index 8227de892..adff20b16 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,7 @@ codespell: -L 'crate,unexpect,Pres,pres,ACI,aci,te,ue,unx,aNULL' \ --skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache,./pykanidm/poetry.lock' \ --skip='./book/book/*' \ + --skip='./book/src/images/*' \ --skip='./docs/*,./.git' \ --skip='./rlm_python/mods-available/eap' \ --skip='./server/web_ui/static/external,./server/web_ui/pkg/external' \ diff --git a/book/src/integrations/oauth2.md b/book/src/integrations/oauth2.md index 0db717f2d..0be22b2fd 100644 --- a/book/src/integrations/oauth2.md +++ b/book/src/integrations/oauth2.md @@ -216,7 +216,7 @@ title=WARNING text=Changing these settings MAY have serious consequences on the -To disable PKCE for a resource server: +To disable PKCE for a confidential resource server: ```bash kanidm system oauth2 warning-insecure-client-disable-pkce @@ -228,6 +228,32 @@ To enable legacy cryptograhy (RSA PKCS1-5 SHA256): kanidm system oauth2 warning-enable-legacy-crypto ``` +## Public Client Configuration + +Some applications are unable to provide client authentication. A common example is single page web +applications that act as the OAuth2 client and its corresponding webserver that is the resource +server. In this case the SPA is unable to act as a confidential client since the basic secret would +need to be embedded in every client. + +Public clients for this reason require PKCE to bind a specific browser session to its OAuth2 +exchange. PKCE can not be disabled for public clients for this reason. + + + +{{#template ../templates/kani-warning.md +imagepath=../images +title=WARNING text=Public clients have many limitations compared to confidential clients. You should avoid them if possible. +}} + + + +To create an OAuth2 public resource server: + +```bash +kanidm system oauth2 create-public +kanidm system oauth2 create mywebapp "My Web App" https://webapp.example.com +``` + ## Example Integrations ### Apache mod\_auth\_openidc @@ -239,7 +265,7 @@ with an appropriate include. OIDCRedirectURI /protected/redirect_uri OIDCCryptoPassphrase OIDCProviderMetadataURL https://kanidm.example.com/oauth2/openid//.well-known/openid-configuration -OIDCScope "openid" +OIDCScope "openid" OIDCUserInfoTokenMethod authz_header OIDCClientID OIDCClientSecret diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 44d003836..1ceec9307 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -37,6 +37,7 @@ use webauthn_rs_proto::{ PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, }; +mod oauth; mod person; mod scim; mod service_account; @@ -1795,228 +1796,6 @@ impl KanidmClient { .await } - // ==== Oauth2 resource server configuration - #[instrument(level = "debug")] - pub async fn idm_oauth2_rs_list(&self) -> Result, ClientError> { - self.perform_get_request("/v1/oauth2").await - } - - pub async fn idm_oauth2_rs_basic_create( - &self, - name: &str, - displayname: &str, - origin: &str, - ) -> Result<(), ClientError> { - let mut new_oauth2_rs = Entry::default(); - new_oauth2_rs - .attrs - .insert("oauth2_rs_name".to_string(), vec![name.to_string()]); - new_oauth2_rs - .attrs - .insert("displayname".to_string(), vec![displayname.to_string()]); - new_oauth2_rs - .attrs - .insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]); - self.perform_post_request("/v1/oauth2/_basic", new_oauth2_rs) - .await - } - - // TODO: the "id" here is actually the *name* not the uuid of the entry... - pub async fn idm_oauth2_rs_get(&self, id: &str) -> Result, ClientError> { - self.perform_get_request(format!("/v1/oauth2/{}", id).as_str()) - .await - } - - pub async fn idm_oauth2_rs_get_basic_secret( - &self, - id: &str, - ) -> Result, ClientError> { - self.perform_get_request(format!("/v1/oauth2/{}/_basic_secret", id).as_str()) - .await - } - - #[allow(clippy::too_many_arguments)] - pub async fn idm_oauth2_rs_update( - &self, - id: &str, - name: Option<&str>, - displayname: Option<&str>, - origin: Option<&str>, - landing: Option<&str>, - reset_secret: bool, - reset_token_key: bool, - reset_sign_key: bool, - ) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - - if let Some(newname) = name { - update_oauth2_rs - .attrs - .insert("oauth2_rs_name".to_string(), vec![newname.to_string()]); - } - if let Some(newdisplayname) = displayname { - update_oauth2_rs - .attrs - .insert("displayname".to_string(), vec![newdisplayname.to_string()]); - } - if let Some(neworigin) = origin { - update_oauth2_rs - .attrs - .insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]); - } - if let Some(newlanding) = landing { - update_oauth2_rs.attrs.insert( - "oauth2_rs_origin_landing".to_string(), - vec![newlanding.to_string()], - ); - } - if reset_secret { - update_oauth2_rs - .attrs - .insert("oauth2_rs_basic_secret".to_string(), Vec::new()); - } - if reset_token_key { - update_oauth2_rs - .attrs - .insert("oauth2_rs_token_key".to_string(), Vec::new()); - } - if reset_sign_key { - update_oauth2_rs - .attrs - .insert("es256_private_key_der".to_string(), Vec::new()); - update_oauth2_rs - .attrs - .insert("rs256_private_key_der".to_string(), Vec::new()); - } - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - - pub async fn idm_oauth2_rs_update_scope_map( - &self, - id: &str, - group: &str, - scopes: Vec<&str>, - ) -> Result<(), ClientError> { - let scopes: Vec = scopes.into_iter().map(str::to_string).collect(); - self.perform_post_request( - format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str(), - scopes, - ) - .await - } - - pub async fn idm_oauth2_rs_delete_scope_map( - &self, - id: &str, - group: &str, - ) -> Result<(), ClientError> { - self.perform_delete_request(format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str()) - .await - } - - pub async fn idm_oauth2_rs_update_sup_scope_map( - &self, - id: &str, - group: &str, - scopes: Vec<&str>, - ) -> Result<(), ClientError> { - let scopes: Vec = scopes.into_iter().map(str::to_string).collect(); - self.perform_post_request( - format!("/v1/oauth2/{}/_sup_scopemap/{}", id, group).as_str(), - scopes, - ) - .await - } - - pub async fn idm_oauth2_rs_delete_sup_scope_map( - &self, - id: &str, - group: &str, - ) -> Result<(), ClientError> { - self.perform_delete_request(format!("/v1/oauth2/{}/_sup_scopemap/{}", id, group).as_str()) - .await - } - - pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> { - self.perform_delete_request(["/v1/oauth2/", id].concat().as_str()) - .await - } - - pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - update_oauth2_rs.attrs.insert( - "oauth2_allow_insecure_client_disable_pkce".to_string(), - Vec::new(), - ); - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - - pub async fn idm_oauth2_rs_disable_pkce(&self, id: &str) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - update_oauth2_rs.attrs.insert( - "oauth2_allow_insecure_client_disable_pkce".to_string(), - vec!["true".to_string()], - ); - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - - pub async fn idm_oauth2_rs_enable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - update_oauth2_rs.attrs.insert( - "oauth2_jwt_legacy_crypto_enable".to_string(), - vec!["true".to_string()], - ); - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - - pub async fn idm_oauth2_rs_disable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - update_oauth2_rs.attrs.insert( - "oauth2_jwt_legacy_crypto_enable".to_string(), - vec!["false".to_string()], - ); - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - - pub async fn idm_oauth2_rs_prefer_short_username(&self, id: &str) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - update_oauth2_rs.attrs.insert( - "oauth2_prefer_short_username".to_string(), - vec!["true".to_string()], - ); - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - - pub async fn idm_oauth2_rs_prefer_spn_username(&self, id: &str) -> Result<(), ClientError> { - let mut update_oauth2_rs = Entry { - attrs: BTreeMap::new(), - }; - update_oauth2_rs.attrs.insert( - "oauth2_prefer_short_username".to_string(), - vec!["false".to_string()], - ); - self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) - .await - } - // ==== recycle bin pub async fn recycle_bin_list(&self) -> Result, ClientError> { self.perform_get_request("/v1/recycle_bin").await diff --git a/libs/client/src/oauth.rs b/libs/client/src/oauth.rs new file mode 100644 index 000000000..b87059a97 --- /dev/null +++ b/libs/client/src/oauth.rs @@ -0,0 +1,247 @@ +use crate::{ClientError, KanidmClient}; +use kanidm_proto::v1::Entry; +use std::collections::BTreeMap; + +impl KanidmClient { + // ==== Oauth2 resource server configuration + #[instrument(level = "debug")] + pub async fn idm_oauth2_rs_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/oauth2").await + } + + pub async fn idm_oauth2_rs_basic_create( + &self, + name: &str, + displayname: &str, + origin: &str, + ) -> Result<(), ClientError> { + let mut new_oauth2_rs = Entry::default(); + new_oauth2_rs + .attrs + .insert("oauth2_rs_name".to_string(), vec![name.to_string()]); + new_oauth2_rs + .attrs + .insert("displayname".to_string(), vec![displayname.to_string()]); + new_oauth2_rs + .attrs + .insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]); + self.perform_post_request("/v1/oauth2/_basic", new_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_public_create( + &self, + name: &str, + displayname: &str, + origin: &str, + ) -> Result<(), ClientError> { + let mut new_oauth2_rs = Entry::default(); + new_oauth2_rs + .attrs + .insert("oauth2_rs_name".to_string(), vec![name.to_string()]); + new_oauth2_rs + .attrs + .insert("displayname".to_string(), vec![displayname.to_string()]); + new_oauth2_rs + .attrs + .insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]); + self.perform_post_request("/v1/oauth2/_public", new_oauth2_rs) + .await + } + + // TODO: the "id" here is actually the *name* not the uuid of the entry... + pub async fn idm_oauth2_rs_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/oauth2/{}", id).as_str()) + .await + } + + pub async fn idm_oauth2_rs_get_basic_secret( + &self, + id: &str, + ) -> Result, ClientError> { + self.perform_get_request(format!("/v1/oauth2/{}/_basic_secret", id).as_str()) + .await + } + + #[allow(clippy::too_many_arguments)] + pub async fn idm_oauth2_rs_update( + &self, + id: &str, + name: Option<&str>, + displayname: Option<&str>, + origin: Option<&str>, + landing: Option<&str>, + reset_secret: bool, + reset_token_key: bool, + reset_sign_key: bool, + ) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + + if let Some(newname) = name { + update_oauth2_rs + .attrs + .insert("oauth2_rs_name".to_string(), vec![newname.to_string()]); + } + if let Some(newdisplayname) = displayname { + update_oauth2_rs + .attrs + .insert("displayname".to_string(), vec![newdisplayname.to_string()]); + } + if let Some(neworigin) = origin { + update_oauth2_rs + .attrs + .insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]); + } + if let Some(newlanding) = landing { + update_oauth2_rs.attrs.insert( + "oauth2_rs_origin_landing".to_string(), + vec![newlanding.to_string()], + ); + } + if reset_secret { + update_oauth2_rs + .attrs + .insert("oauth2_rs_basic_secret".to_string(), Vec::new()); + } + if reset_token_key { + update_oauth2_rs + .attrs + .insert("oauth2_rs_token_key".to_string(), Vec::new()); + } + if reset_sign_key { + update_oauth2_rs + .attrs + .insert("es256_private_key_der".to_string(), Vec::new()); + update_oauth2_rs + .attrs + .insert("rs256_private_key_der".to_string(), Vec::new()); + } + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_update_scope_map( + &self, + id: &str, + group: &str, + scopes: Vec<&str>, + ) -> Result<(), ClientError> { + let scopes: Vec = scopes.into_iter().map(str::to_string).collect(); + self.perform_post_request( + format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str(), + scopes, + ) + .await + } + + pub async fn idm_oauth2_rs_delete_scope_map( + &self, + id: &str, + group: &str, + ) -> Result<(), ClientError> { + self.perform_delete_request(format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str()) + .await + } + + pub async fn idm_oauth2_rs_update_sup_scope_map( + &self, + id: &str, + group: &str, + scopes: Vec<&str>, + ) -> Result<(), ClientError> { + let scopes: Vec = scopes.into_iter().map(str::to_string).collect(); + self.perform_post_request( + format!("/v1/oauth2/{}/_sup_scopemap/{}", id, group).as_str(), + scopes, + ) + .await + } + + pub async fn idm_oauth2_rs_delete_sup_scope_map( + &self, + id: &str, + group: &str, + ) -> Result<(), ClientError> { + self.perform_delete_request(format!("/v1/oauth2/{}/_sup_scopemap/{}", id, group).as_str()) + .await + } + + pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> { + self.perform_delete_request(["/v1/oauth2/", id].concat().as_str()) + .await + } + + pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + update_oauth2_rs.attrs.insert( + "oauth2_allow_insecure_client_disable_pkce".to_string(), + Vec::new(), + ); + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_disable_pkce(&self, id: &str) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + update_oauth2_rs.attrs.insert( + "oauth2_allow_insecure_client_disable_pkce".to_string(), + vec!["true".to_string()], + ); + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_enable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + update_oauth2_rs.attrs.insert( + "oauth2_jwt_legacy_crypto_enable".to_string(), + vec!["true".to_string()], + ); + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_disable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + update_oauth2_rs.attrs.insert( + "oauth2_jwt_legacy_crypto_enable".to_string(), + vec!["false".to_string()], + ); + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_prefer_short_username(&self, id: &str) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + update_oauth2_rs.attrs.insert( + "oauth2_prefer_short_username".to_string(), + vec!["true".to_string()], + ); + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } + + pub async fn idm_oauth2_rs_prefer_spn_username(&self, id: &str) -> Result<(), ClientError> { + let mut update_oauth2_rs = Entry { + attrs: BTreeMap::new(), + }; + update_oauth2_rs.attrs.insert( + "oauth2_prefer_short_username".to_string(), + vec!["false".to_string()], + ); + self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) + .await + } +} diff --git a/libs/client/src/sync_account.rs b/libs/client/src/sync_account.rs index 4a9a8805a..d7adc9939 100644 --- a/libs/client/src/sync_account.rs +++ b/libs/client/src/sync_account.rs @@ -39,7 +39,7 @@ impl KanidmClient { format!("/v1/sync_account/{}/_attr/sync_credential_portal", id).as_str(), ) .await - .map(|values: Vec| values.get(0).map(|u| u.clone())) + .map(|values: Vec| values.get(0).cloned()) } pub async fn idm_sync_account_create( diff --git a/proto/src/v1.rs b/proto/src/v1.rs index 516964b0d..9ec4f6b1f 100644 --- a/proto/src/v1.rs +++ b/proto/src/v1.rs @@ -906,7 +906,6 @@ impl fmt::Display for AuthMech { #[serde(rename_all = "lowercase")] pub enum AuthIssueSession { Token, - Cookie, } #[derive(Debug, Serialize, Deserialize)] @@ -1011,7 +1010,9 @@ pub enum AuthState { // the result. Success(String), // Everything is good, your cookie has been issued. - SuccessCookie, + // Cookies no longer supported. Left as a comment as an example of alternate + // issue types. + // SuccessCookie, } #[derive(Debug, Serialize, Deserialize)] diff --git a/server/core/Cargo.toml b/server/core/Cargo.toml index 775b435c8..342c806ec 100644 --- a/server/core/Cargo.toml +++ b/server/core/Cargo.toml @@ -19,7 +19,6 @@ axum-auth = "0.4.0" axum-csp = { workspace = true } axum-macros = "0.3.7" axum-server = { version = "0.5.1", features = ["tls-openssl"] } -axum-sessions = "0.5.0" chrono = { workspace = true } compact_jwt = { workspace = true } cron = { workspace = true } diff --git a/server/core/src/https/middleware/mod.rs b/server/core/src/https/middleware/mod.rs index 7313fcf7b..2bbb7454c 100644 --- a/server/core/src/https/middleware/mod.rs +++ b/server/core/src/https/middleware/mod.rs @@ -5,7 +5,6 @@ use axum::{ response::Response, TypedHeader, }; -use axum_sessions::SessionHandle; use http::HeaderValue; use uuid::Uuid; @@ -41,20 +40,8 @@ pub async fn kopid_middleware( // generate the event ID let eventid = sketching::tracing_forest::id(); - // get the bearer token from the headers or the session - let uat = match auth { - Some(bearer) => Some(bearer.token().to_string()), - None => { - // no headers, let's try the cookies - match request.extensions().get::() { - Some(sess) => { - // we have a session! - sess.read().await.get::("bearer") - } - None => None, - } - } - }; + // get the bearer token from the headers if present. + let uat = auth.map(|bearer| bearer.token().to_string()); // insert the extension so we can pull it out later request.extensions_mut().insert(KOpId { eventid, uat }); diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index 28d13f86e..23cc0a171 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -19,8 +19,6 @@ use axum::routing::*; use axum::Router; use axum_csp::{CspDirectiveType, CspValue}; use axum_macros::FromRef; -use axum_sessions::extractors::WritableSession; -use axum_sessions::{async_session, SameSite, SessionLayer}; use compact_jwt::{Jws, JwsSigner, JwsUnverified}; use generic::*; use http::{HeaderMap, HeaderValue}; @@ -76,11 +74,7 @@ impl ServerState { } } - fn get_current_auth_session_id( - &self, - headers: &HeaderMap, - session: &WritableSession, - ) -> Option { + fn get_current_auth_session_id(&self, headers: &HeaderMap) -> Option { // We see if there is a signed header copy first. headers .get("X-KANIDM-AUTH-SESSION-ID") @@ -89,8 +83,6 @@ impl ServerState { hv.to_str().ok() }) .and_then(|s| Some(self.reinflate_uuid_from_bytes(s)).unwrap_or(None)) - // If not there, get from the cookie instead. - .or_else(|| session.get::("auth-session-id")) } } @@ -134,7 +126,6 @@ pub fn get_js_files(role: ServerRole) -> Vec { pub async fn create_https_server( config: Configuration, - cookie_key: [u8; 64], jws_signer: JwsSigner, status_ref: &'static StatusActor, qe_w_ref: &'static QueryServerWriteV1, @@ -185,15 +176,6 @@ pub async fn create_https_server( vec![CspValue::SelfSite, CspValue::SchemeData], ); - let store = async_session::CookieStore::new(); - - let session_layer = SessionLayer::new(store, &cookie_key) - .with_cookie_name("kanidm-session") - .with_session_ttl(None) - .with_cookie_domain(config.domain) - .with_same_site_policy(SameSite::Strict) - .with_secure(true); - let trust_x_forward_for = config.trust_x_forward_for; let state = ServerState { @@ -209,13 +191,18 @@ pub async fn create_https_server( let static_routes = match config.role { ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => { + // Create a spa router that captures everything at ui without key extraction. + let spa_router = Router::new() + .route("/", get(crate::https::ui::ui_handler)) + .fallback(crate::https::ui::ui_handler); + Router::new() - // direct users to the login page - .route("/", get(|| async { Redirect::temporary("/ui/login") })) - .route("/ui/", get(crate::https::ui::ui_handler)) - // matches /ui/* but adds a path var `key` if you really wanted to capture it later. - .route("/ui/*key", get(crate::https::ui::ui_handler)) + // direct users to the base app page. If a login is required, + // then views will take care of redirection. We shouldn't redir + // to login because that force clears previous sessions! + .route("/", get(|| async { Redirect::temporary("/ui") })) .route("/manifest.webmanifest", get(manifest::manifest)) + .nest("/ui", spa_router) .layer(middleware::compression::new()) // TODO: this needs to be configured properly } ServerRole::WriteReplicaNoUI => Router::new(), @@ -250,7 +237,6 @@ pub async fn create_https_server( middleware::csp_headers::cspheaders_layer, )) .layer(from_fn(middleware::version_middleware)) - .layer(session_layer) .layer(TraceLayer::new_for_http()) // This must be the LAST middleware. // This is because the last middleware here is the first to be entered and the last diff --git a/server/core/src/https/oauth2.rs b/server/core/src/https/oauth2.rs index 15453bce3..88eae89c6 100644 --- a/server/core/src/https/oauth2.rs +++ b/server/core/src/https/oauth2.rs @@ -47,6 +47,19 @@ pub async fn oauth2_basic_post( json_rest_event_post(state, classes, obj, kopid).await } +pub async fn oauth2_public_post( + State(state): State, + Extension(kopid): Extension, + Json(obj): Json, +) -> impl IntoResponse { + let classes = vec![ + "oauth2_resource_server".to_string(), + "oauth2_resource_server_public".to_string(), + "object".to_string(), + ]; + json_rest_event_post(state, classes, obj, kopid).await +} + fn oauth2_id(rs_name: &str) -> Filter { filter_all!(f_and!([ f_eq("class", PartialValue::new_class("oauth2_resource_server")), @@ -517,25 +530,12 @@ pub async fn oauth2_token_post( // This is called directly by the resource server, where we then issue // the token to the caller. - // Get the authz header (if present). In the future depending on the - // type of exchanges we support, this could become an Option type. - let client_authz = match headers + // Get the authz header (if present). Not all exchange types require this. + let client_authz = headers .get("authorization") .and_then(|hv| hv.to_str().ok()) .and_then(|h| h.split(' ').last()) - .map(str::to_string) - { - Some(val) => val, - None => { - error!("Basic Authentication Not Provided"); - #[allow(clippy::unwrap_used)] - return Response::builder() - .status(StatusCode::UNAUTHORIZED) - .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .body(Body::from("Invalid Basic Authorisation")) - .unwrap(); - } - }; + .map(str::to_string); // Do we change the method/path we take here based on the type of requested // grant? Should we cease the delayed/async session update here and just opt @@ -543,7 +543,7 @@ pub async fn oauth2_token_post( let res = state .qe_w_ref - .handle_oauth2_token_exchange(Some(client_authz), tok_req, kopid.eventid) + .handle_oauth2_token_exchange(client_authz, tok_req, kopid.eventid) .await; match res { @@ -612,8 +612,7 @@ pub async fn oauth2_openid_userinfo_get( Extension(kopid): Extension, ) -> Response { // The token we want to inspect is in the authorisation header. - - let client_authz = match kopid.uat { + let client_token = match kopid.uat { Some(val) => val, None => { error!("Bearer Authentication Not Provided"); @@ -628,7 +627,7 @@ pub async fn oauth2_openid_userinfo_get( let res = state .qe_r_ref - .handle_oauth2_openid_userinfo(client_id, client_authz, kopid.eventid) + .handle_oauth2_openid_userinfo(client_id, client_token, kopid.eventid) .await; match res { diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index 095c07a8d..41f077f21 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -8,7 +8,6 @@ use axum::response::{IntoResponse, Response}; use axum::routing::{delete, get, post, put}; use axum::{Extension, Json, Router}; use axum_macros::debug_handler; -use axum_sessions::extractors::{ReadableSession, WritableSession}; use compact_jwt::Jws; use http::{HeaderMap, HeaderValue, StatusCode}; use hyper::Body; @@ -104,26 +103,18 @@ pub async fn whoami( pub async fn whoami_uat( State(state): State, Extension(kopid): Extension, - session: ReadableSession, ) -> impl IntoResponse { - let uat = match kopid.uat { - Some(val) => Some(val), - None => session.get("bearer"), - }; - let res = state.qe_r_ref.handle_whoami_uat(uat, kopid.eventid).await; + let res = state + .qe_r_ref + .handle_whoami_uat(kopid.uat, kopid.eventid) + .await; to_axum_response(res) } pub async fn logout( State(state): State, - mut msession: WritableSession, Extension(kopid): Extension, ) -> impl IntoResponse { - // Now lets nuke any cookies for the session. We do this before the handle_logout - // so that if any errors occur, the cookies are still removed. - msession.remove("auth-session-id"); - msession.remove("bearer"); - let res = state.qe_w_ref.handle_logout(kopid.uat, kopid.eventid).await; to_axum_response(res) @@ -1222,15 +1213,10 @@ pub async fn recycle_bin_revive_id_post( pub async fn applinks_get( State(state): State, Extension(kopid): Extension, - session: ReadableSession, ) -> impl IntoResponse { - let uat = match kopid.uat { - Some(val) => Some(val), - None => session.get("bearer"), - }; let res = state .qe_r_ref - .handle_list_applinks(uat, kopid.eventid) + .handle_list_applinks(kopid.uat, kopid.eventid) .await; to_axum_response(res) } @@ -1244,7 +1230,6 @@ pub async fn reauth( State(state): State, TrustedClientIp(ip_addr): TrustedClientIp, Extension(kopid): Extension, - session: WritableSession, Json(obj): Json, ) -> impl IntoResponse { // This may change in the future ... @@ -1253,13 +1238,12 @@ pub async fn reauth( .handle_reauth(kopid.uat, obj, kopid.eventid, ip_addr) .await; debug!("REAuth result: {:?}", inter); - auth_session_state_management(state, inter, session) + auth_session_state_management(state, inter) } pub async fn auth( State(state): State, TrustedClientIp(ip_addr): TrustedClientIp, - session: WritableSession, headers: HeaderMap, Extension(kopid): Extension, Json(obj): Json, @@ -1268,7 +1252,7 @@ pub async fn auth( // Do anything here first that's needed like getting the session details // out of the req cookie. - let maybe_sessionid = state.get_current_auth_session_id(&headers, &session); + let maybe_sessionid = state.get_current_auth_session_id(&headers); debug!("Session ID: {:?}", maybe_sessionid); // We probably need to know if we allocate the cookie, that this is a // new session, and in that case, anything *except* authrequest init is @@ -1280,14 +1264,13 @@ pub async fn auth( debug!("Auth result: {:?}", inter); - auth_session_state_management(state, inter, session) + auth_session_state_management(state, inter) } #[instrument(skip(state))] fn auth_session_state_management( state: ServerState, inter: Result, - mut msession: WritableSession, ) -> impl IntoResponse { let mut auth_session_id_tok = None; @@ -1300,77 +1283,44 @@ fn auth_session_state_management( match auth_state { AuthState::Choose(allowed) => { debug!("🧩 -> AuthState::Choose"); // TODO: this should be ... less work - - // Ensure the auth-session-id is set - msession.remove("auth-session-id"); - msession - .insert("auth-session-id", sessionid) + // Ensure the auth-session-id is set + let kref = &state.jws_signer; + let jws = Jws::new(SessionId { sessionid }); + // Get the header token ready. + jws.sign(kref) + .map(|jwss| { + auth_session_id_tok = Some(jwss.to_string()); + }) .map_err(|e| { error!(?e); OperationError::InvalidSessionState }) - .and_then(|_| { - let kref = &state.jws_signer; - let jws = Jws::new(SessionId { sessionid }); - // Get the header token ready. - jws.sign(kref) - .map(|jwss| { - auth_session_id_tok = Some(jwss.to_string()); - }) - .map_err(|e| { - error!(?e); - OperationError::InvalidSessionState - }) - }) .map(|_| ProtoAuthState::Choose(allowed)) } AuthState::Continue(allowed) => { debug!("🧩 -> AuthState::Continue"); - - // Ensure the auth-session-id is set - msession.remove("auth-session-id"); - trace!(?sessionid, "🔥 🔥 "); - msession - .insert("auth-session-id", sessionid) + let kref = &state.jws_signer; + // Get the header token ready. + let jws = Jws::new(SessionId { sessionid }); + jws.sign(kref) + .map(|jwss| { + auth_session_id_tok = Some(jwss.to_string()); + }) .map_err(|e| { error!(?e); OperationError::InvalidSessionState }) - .and_then(|_| { - let kref = &state.jws_signer; - // Get the header token ready. - let jws = Jws::new(SessionId { sessionid }); - jws.sign(kref) - .map(|jwss| { - auth_session_id_tok = Some(jwss.to_string()); - }) - .map_err(|e| { - error!(?e); - OperationError::InvalidSessionState - }) - }) .map(|_| ProtoAuthState::Continue(allowed)) } AuthState::Success(token, issue) => { debug!("🧩 -> AuthState::Success"); - // Remove the auth-session-id - - msession.remove("auth-session-id"); - // Create a session cookie? - msession.remove("bearer"); match issue { - AuthIssueSession::Cookie => msession - .insert("bearer", token) - .map_err(|_| OperationError::InvalidSessionState) - .map(|_| ProtoAuthState::SuccessCookie), AuthIssueSession::Token => Ok(ProtoAuthState::Success(token)), } } AuthState::Denied(reason) => { debug!("🧩 -> AuthState::Denied"); - // Remove the auth-session-id - msession.remove("auth-session-id"); Ok(ProtoAuthState::Denied(reason)) } } @@ -1398,13 +1348,11 @@ fn auth_session_state_management( pub async fn auth_valid( State(state): State, Extension(kopid): Extension, - session: ReadableSession, ) -> impl IntoResponse { - let uat = match kopid.uat { - Some(val) => Some(val), - None => session.get("bearer"), - }; - let res = state.qe_r_ref.handle_auth_valid(uat, kopid.eventid).await; + let res = state + .qe_r_ref + .handle_auth_valid(kopid.uat, kopid.eventid) + .await; to_axum_response(res) } @@ -1412,6 +1360,11 @@ pub async fn auth_valid( pub fn router(state: ServerState) -> Router { Router::new() .route("/v1/oauth2", get(super::oauth2::oauth2_get)) + .route("/v1/oauth2/_basic", post(super::oauth2::oauth2_basic_post)) + .route( + "/v1/oauth2/_public", + post(super::oauth2::oauth2_public_post), + ) .route( "/v1/oauth2/:rs_name", get(super::oauth2::oauth2_id_get) @@ -1422,7 +1375,6 @@ pub fn router(state: ServerState) -> Router { "/v1/oauth2/:rs_name/_basic_secret", get(super::oauth2::oauth2_id_get_basic_secret), ) - .route("/v1/oauth2/_basic", post(super::oauth2::oauth2_basic_post)) .route( "/v1/oauth2/:rs_name/_scopemap/:group", post(super::oauth2::oauth2_id_scopemap_post) diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 0adda609e..7a00e170d 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -762,8 +762,6 @@ pub async fn create_server_core( } }; - let cookie_key: [u8; 64] = idms.get_cookie_key(); - // Any pre-start tasks here. match &config.integration_test_config { Some(itc) => { @@ -913,7 +911,6 @@ pub async fn create_server_core( } else { let h: tokio::task::JoinHandle<()> = match https::create_https_server( config.clone(), - cookie_key, jws_signer, status_ref, server_write_ref, diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index 10d2bf6be..820de36f7 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -1536,7 +1536,8 @@ lazy_static! { ("acp_create_class", Value::new_iutf8("object")), ("acp_create_class", Value::new_iutf8("oauth2_resource_server")), - ("acp_create_class", Value::new_iutf8("oauth2_resource_server_basic")) + ("acp_create_class", Value::new_iutf8("oauth2_resource_server_basic")), + ("acp_create_class", Value::new_iutf8("oauth2_resource_server_public")) ); } diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 58f623fe9..27046556e 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -1867,7 +1867,6 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#" "description", "oauth2_rs_scope_map", "oauth2_rs_sup_scope_map", - "oauth2_allow_insecure_client_disable_pkce", "rs256_private_key_der", "oauth2_jwt_legacy_crypto_enable", "oauth2_prefer_short_username", @@ -1887,27 +1886,34 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#" } "#; -pub const JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = r#" - { - "attrs": { - "class": [ - "object", - "system", - "classtype" - ], - "description": [ - "The class representing a configured Oauth2 Resource Server authenticated with http basic" - ], - "classname": [ - "oauth2_resource_server_basic" - ], - "systemmay": [], - "systemmust": [ - "oauth2_rs_basic_secret" - ], - "uuid": [ - "00000000-0000-0000-0000-ffff00000086" - ] - } - } -"#; +lazy_static! { + pub static ref E_SCHEMA_CLASS_OAUTH2_RS_BASIC: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("class", CLASS_SYSTEM.clone()), + ("class", CLASS_CLASSTYPE.clone()), + ( + "description", + Value::new_utf8s( + "The class representing a configured Oauth2 Resource Server authenticated with http basic authentication"), + ), + ("classname", Value::new_iutf8("oauth2_resource_server_basic")), + ("systemmay", Value::new_iutf8("oauth2_allow_insecure_client_disable_pkce")), + ("systemmust", Value::new_iutf8("oauth2_rs_basic_secret")), + ("systemexcludes", Value::new_iutf8("oauth2_resource_server_public")), + ("uuid", Value::Uuid(UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC)) + ); + + pub static ref E_SCHEMA_CLASS_OAUTH2_RS_PUBLIC: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("class", CLASS_SYSTEM.clone()), + ("class", CLASS_CLASSTYPE.clone()), + ( + "description", + Value::new_utf8s( + "The class representing a configured Oauth2 Resource Server with public clients and pkce verification"), + ), + ("classname", Value::new_iutf8("oauth2_resource_server_public")), + ("systemexcludes", Value::new_iutf8("oauth2_resource_server_basic")), + ("uuid", Value::Uuid(UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC)) + ); +} diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index a20776422..b27c9f313 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -231,6 +231,7 @@ pub const UUID_SCHEMA_ATTR_NAME_HISTORY: Uuid = uuid!("00000000-0000-0000-0000-f pub const UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000136"); +pub const UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC: Uuid = uuid!("00000000-0000-0000-0000-ffff00000137"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/server/lib/src/constants/values.rs b/server/lib/src/constants/values.rs index 7dc6e6ded..6813d79c6 100644 --- a/server/lib/src/constants/values.rs +++ b/server/lib/src/constants/values.rs @@ -24,6 +24,8 @@ lazy_static! { PartialValue::new_class("oauth2_resource_server"); pub static ref PVCLASS_OAUTH2_BASIC: PartialValue = PartialValue::new_class("oauth2_resource_server_basic"); + pub static ref PVCLASS_OAUTH2_PUBLIC: PartialValue = + PartialValue::new_class("oauth2_resource_server_public"); pub static ref PVCLASS_PERSON: PartialValue = PartialValue::new_class("person"); pub static ref PVCLASS_POSIXACCOUNT: PartialValue = PartialValue::new_class("posixaccount"); pub static ref PVCLASS_POSIXGROUP: PartialValue = PartialValue::new_class("posixgroup"); @@ -47,6 +49,7 @@ lazy_static! { pub static ref CLASS_ACCOUNT: Value = Value::new_class("account"); pub static ref CLASS_ATTRIBUTETYPE: Value = Value::new_class("attributetype"); pub static ref CLASS_CLASS: Value = Value::new_class("class"); + pub static ref CLASS_CLASSTYPE: Value = Value::new_class("classtype"); pub static ref CLASS_DOMAIN_INFO: Value = Value::new_class("domain_info"); pub static ref CLASS_DYNGROUP: Value = Value::new_class("dyngroup"); pub static ref CLASS_GROUP: Value = Value::new_class("group"); diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs index 9889a5c5e..df9e1f2f2 100644 --- a/server/lib/src/idm/oauth2.rs +++ b/server/lib/src/idm/oauth2.rs @@ -187,6 +187,29 @@ pub struct AuthorisePermitSuccess { pub code: String, } +#[derive(Clone)] +enum OauthRSType { + Basic { + authz_secret: String, + enable_pkce: bool, + }, + // Public clients must have pkce. + Public, +} + +impl std::fmt::Debug for OauthRSType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut ds = f.debug_struct("Oauth2RSType"); + match self { + OauthRSType::Basic { enable_pkce, .. } => { + ds.field("type", &"basic").field("pkce", enable_pkce) + } + OauthRSType::Public => ds.field("type", &"public"), + }; + ds.finish() + } +} + #[derive(Clone)] pub struct Oauth2RS { name: String, @@ -196,15 +219,10 @@ pub struct Oauth2RS { origin_https: bool, scope_maps: BTreeMap>, sup_scope_maps: BTreeMap>, - // Client Auth Type (basic is all we support for now. - authz_secret: String, // Our internal exchange encryption material for this rs. token_fernet: Fernet, jws_signer: JwsSigner, // jws_validator: JwsValidator, - // Some clients, especially openid ones don't do pkce. SIGH. - // Can we enforce nonce in this case? - enable_pkce: bool, // For oidc we also need our issuer url. iss: Url, // For discovery we need to build and keep a number of values. @@ -214,6 +232,7 @@ pub struct Oauth2RS { jwks_uri: Url, scopes_supported: BTreeSet, prefer_short_username: bool, + type_: OauthRSType, } impl std::fmt::Debug for Oauth2RS { @@ -222,6 +241,7 @@ impl std::fmt::Debug for Oauth2RS { .field("name", &self.name) .field("displayname", &self.displayname) .field("uuid", &self.uuid) + .field("type", &self.type_) .field("origin", &self.origin) .field("scope_maps", &self.scope_maps) .field("sup_scope_maps", &self.sup_scope_maps) @@ -295,157 +315,153 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> { if !ent.attribute_equality("class", &PVCLASS_OAUTH2_RS) { admin_error!("Missing class oauth2_resource_server"); // Check we have oauth2_resource_server class - Err(OperationError::InvalidEntryState) - } else if ent.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) { - // If we have oauth2_resource_server_basic - // Now we know we can load the attrs. - trace!("name"); - let name = ent - .get_ava_single_iname("oauth2_rs_name") - .map(str::to_string) - .ok_or(OperationError::InvalidValueState)?; - trace!("displayname"); - let displayname = ent - .get_ava_single_utf8("displayname") - .map(str::to_string) - .ok_or(OperationError::InvalidValueState)?; - trace!("origin"); - let (origin, origin_https) = ent - .get_ava_single_url("oauth2_rs_origin") - .map(|url| (url.origin(), url.scheme() == "https")) - .ok_or(OperationError::InvalidValueState)?; + return Err(OperationError::InvalidEntryState); + } - let landing_valid = ent - .get_ava_single_url("oauth2_rs_origin_landing") - .map(|url| url.origin() == origin). - unwrap_or(true); - - if !landing_valid { - warn!("{} has a landing page that is not part of origin. May be invalid.", name); - } - - trace!("authz_secret"); + let type_ = if ent.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) { let authz_secret = ent .get_ava_single_secret("oauth2_rs_basic_secret") .map(str::to_string) .ok_or(OperationError::InvalidValueState)?; - trace!("token_key"); - let token_fernet = ent - .get_ava_single_secret("oauth2_rs_token_key") - .ok_or(OperationError::InvalidValueState) - .and_then(|key| { - Fernet::new(key).ok_or(OperationError::CryptographyError) - })?; - - trace!("scope_maps"); - let scope_maps = ent - .get_ava_as_oauthscopemaps("oauth2_rs_scope_map") - .cloned() - .unwrap_or_default(); - - trace!("sup_scope_maps"); - let sup_scope_maps = ent - .get_ava_as_oauthscopemaps("oauth2_rs_sup_scope_map") - .cloned() - .unwrap_or_default(); - - trace!("oauth2_jwt_legacy_crypto_enable"); - let jws_signer = if ent.get_ava_single_bool("oauth2_jwt_legacy_crypto_enable").unwrap_or(false) { - trace!("rs256_private_key_der"); - ent - .get_ava_single_private_binary("rs256_private_key_der") - .ok_or(OperationError::InvalidValueState) - .and_then(|key_der| { - JwsSigner::from_rs256_der(key_der).map_err(|e| { - admin_error!(err = ?e, "Unable to load Legacy RS256 JwsSigner from DER"); - OperationError::CryptographyError - }) - })? - } else { - trace!("es256_private_key_der"); - ent - .get_ava_single_private_binary("es256_private_key_der") - .ok_or(OperationError::InvalidValueState) - .and_then(|key_der| { - JwsSigner::from_es256_der(key_der).map_err(|e| { - admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER"); - OperationError::CryptographyError - }) - })? - }; - - /* - let jws_validator = jws_signer.get_validator().map_err(|e| { - admin_error!(err = ?e, "Unable to load JwsValidator from JwsSigner"); - OperationError::CryptographyError - })?; - */ let enable_pkce = ent .get_ava_single_bool("oauth2_allow_insecure_client_disable_pkce") .map(|e| !e) .unwrap_or(true); - let prefer_short_username = ent - .get_ava_single_bool("oauth2_prefer_short_username") - .unwrap_or(false); - - let mut authorization_endpoint = self.inner.origin.clone(); - authorization_endpoint.set_path("/ui/oauth2"); - - let mut token_endpoint = self.inner.origin.clone(); - token_endpoint.set_path("/oauth2/token"); - - let mut userinfo_endpoint = self.inner.origin.clone(); - userinfo_endpoint.set_path(&format!("/oauth2/openid/{name}/userinfo")); - - let mut jwks_uri = self.inner.origin.clone(); - jwks_uri.set_path(&format!("/oauth2/openid/{name}/public_key.jwk")); - - let mut iss = self.inner.origin.clone(); - iss.set_path(&format!("/oauth2/openid/{name}")); - - let scopes_supported: BTreeSet = - scope_maps - .values() - .flat_map(|bts| bts.iter()) - - .chain( - sup_scope_maps - .values() - .flat_map(|bts| bts.iter()) - ) - - .cloned() - .collect(); - - let client_id = name.clone(); - let rscfg = Oauth2RS { - name, - displayname, - uuid, - origin, - origin_https, - scope_maps, - sup_scope_maps, + OauthRSType::Basic { authz_secret, - token_fernet, - jws_signer, - // jws_validator, enable_pkce, - iss, - authorization_endpoint, - token_endpoint, - userinfo_endpoint, - jwks_uri, - scopes_supported, - prefer_short_username, - }; - - Ok((client_id, rscfg)) + } + } else if ent.attribute_equality("class", &PVCLASS_OAUTH2_PUBLIC) { + OauthRSType::Public } else { - Err(OperationError::InvalidEntryState) + error!("Missing class determining oauth2 rs type"); + return Err(OperationError::InvalidEntryState); + }; + + // Now we know we can load the shared attrs. + let name = ent + .get_ava_single_iname("oauth2_rs_name") + .map(str::to_string) + .ok_or(OperationError::InvalidValueState)?; + + let displayname = ent + .get_ava_single_utf8("displayname") + .map(str::to_string) + .ok_or(OperationError::InvalidValueState)?; + + let (origin, origin_https) = ent + .get_ava_single_url("oauth2_rs_origin") + .map(|url| (url.origin(), url.scheme() == "https")) + .ok_or(OperationError::InvalidValueState)?; + + let landing_valid = ent + .get_ava_single_url("oauth2_rs_origin_landing") + .map(|url| url.origin() == origin). + unwrap_or(true); + + if !landing_valid { + warn!("{} has a landing page that is not part of origin. May be invalid.", name); } + + let token_fernet = ent + .get_ava_single_secret("oauth2_rs_token_key") + .ok_or(OperationError::InvalidValueState) + .and_then(|key| { + Fernet::new(key).ok_or(OperationError::CryptographyError) + })?; + + let scope_maps = ent + .get_ava_as_oauthscopemaps("oauth2_rs_scope_map") + .cloned() + .unwrap_or_default(); + + let sup_scope_maps = ent + .get_ava_as_oauthscopemaps("oauth2_rs_sup_scope_map") + .cloned() + .unwrap_or_default(); + + trace!("oauth2_jwt_legacy_crypto_enable"); + let jws_signer = if ent.get_ava_single_bool("oauth2_jwt_legacy_crypto_enable").unwrap_or(false) { + trace!("rs256_private_key_der"); + ent + .get_ava_single_private_binary("rs256_private_key_der") + .ok_or(OperationError::InvalidValueState) + .and_then(|key_der| { + JwsSigner::from_rs256_der(key_der).map_err(|e| { + admin_error!(err = ?e, "Unable to load Legacy RS256 JwsSigner from DER"); + OperationError::CryptographyError + }) + })? + } else { + trace!("es256_private_key_der"); + ent + .get_ava_single_private_binary("es256_private_key_der") + .ok_or(OperationError::InvalidValueState) + .and_then(|key_der| { + JwsSigner::from_es256_der(key_der).map_err(|e| { + admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER"); + OperationError::CryptographyError + }) + })? + }; + + let prefer_short_username = ent + .get_ava_single_bool("oauth2_prefer_short_username") + .unwrap_or(false); + + let mut authorization_endpoint = self.inner.origin.clone(); + authorization_endpoint.set_path("/ui/oauth2"); + + let mut token_endpoint = self.inner.origin.clone(); + token_endpoint.set_path("/oauth2/token"); + + let mut userinfo_endpoint = self.inner.origin.clone(); + userinfo_endpoint.set_path(&format!("/oauth2/openid/{name}/userinfo")); + + let mut jwks_uri = self.inner.origin.clone(); + jwks_uri.set_path(&format!("/oauth2/openid/{name}/public_key.jwk")); + + let mut iss = self.inner.origin.clone(); + iss.set_path(&format!("/oauth2/openid/{name}")); + + let scopes_supported: BTreeSet = + scope_maps + .values() + .flat_map(|bts| bts.iter()) + + .chain( + sup_scope_maps + .values() + .flat_map(|bts| bts.iter()) + ) + + .cloned() + .collect(); + + let client_id = name.clone(); + let rscfg = Oauth2RS { + name, + displayname, + uuid, + origin, + origin_https, + scope_maps, + sup_scope_maps, + token_fernet, + jws_signer, + iss, + authorization_endpoint, + token_endpoint, + userinfo_endpoint, + jwks_uri, + scopes_supported, + prefer_short_username, + type_, + }; + + Ok((client_id, rscfg)) }) .collect(); @@ -478,10 +494,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { })?; // check the secret. - if o2rs.authz_secret != secret { - security_info!("Invalid oauth2 client_id secret"); - return Err(Oauth2Error::AuthenticationRequired); - } + match &o2rs.type_ { + OauthRSType::Basic { authz_secret, .. } => { + if authz_secret != &secret { + security_info!("Invalid oauth2 client_id secret"); + return Err(Oauth2Error::AuthenticationRequired); + } + } + // Relies on the token to be valid. + OauthRSType::Public => {} + }; + // We are authenticated! Yay! Now we can actually check things ... // Can we deserialise the token? @@ -550,14 +573,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { token_req: &AccessTokenRequest, ct: Duration, ) -> Result { + // Public clients will send the client_id via the ATR, so we need to handle this case. let (client_id, secret) = if let Some(client_authz) = client_authz { - parse_basic_authz(client_authz)? + let (client_id, secret) = parse_basic_authz(client_authz)?; + (client_id, Some(secret)) } else { match (&token_req.client_id, &token_req.client_secret) { - (Some(a), Some(b)) => (a.clone(), b.clone()), + (Some(a), b) => (a.clone(), b.clone()), _ => { + // We at least need the client_id, else we can't proceed! security_info!( - "Invalid oauth2 authentication - no basic auth or missing auth post data" + "Invalid oauth2 authentication - no basic auth or missing client_id in access token request" ); return Err(Oauth2Error::AuthenticationRequired); } @@ -579,10 +605,27 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { }; // check the secret. - if o2rs.authz_secret != secret { - security_info!("Invalid oauth2 client_id secret"); - return Err(Oauth2Error::AuthenticationRequired); - } + match &o2rs.type_ { + OauthRSType::Basic { authz_secret, .. } => { + match secret { + Some(secret) => { + if authz_secret != &secret { + security_info!("Invalid oauth2 client_id secret"); + return Err(Oauth2Error::AuthenticationRequired); + } + } + None => { + // We can only get here if we relied on the atr for the client_id and secret + security_info!( + "Invalid oauth2 authentication - no secret in access token request" + ); + return Err(Oauth2Error::AuthenticationRequired); + } + } + } + // Relies on the token to be valid - no further action needed. + OauthRSType::Public => {} + }; // We are authenticated! Yay! Now we can actually check things ... @@ -724,6 +767,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { }) })?; + let require_pkce = match &o2rs.type_ { + OauthRSType::Basic { enable_pkce, .. } => *enable_pkce, + OauthRSType::Public => true, + }; + // If we have a verifier present, we MUST assert that a code challenge is present! // It is worth noting here that code_xchg is *server issued* and encrypted, with // a short validity period. The client controlled value is in token_req.code_verifier @@ -744,7 +792,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ); return Err(Oauth2Error::InvalidRequest); } - } else if o2rs.enable_pkce { + } else if require_pkce { security_info!( "PKCE code verification failed - no code challenge present in PKCE enforced mode" ); @@ -1126,10 +1174,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { })?; // check the secret. - if o2rs.authz_secret != secret { - security_info!("Invalid oauth2 client_id secret"); - return Err(OperationError::InvalidSessionState); - } + match &o2rs.type_ { + OauthRSType::Basic { authz_secret, .. } => { + if authz_secret != &secret { + security_info!("Invalid oauth2 client_id secret"); + return Err(OperationError::InvalidSessionState); + } + } + // Relies on the token to be valid. + OauthRSType::Public => {} + }; o2rs.token_fernet .decrypt(token) @@ -1207,8 +1261,13 @@ impl<'a> IdmServerProxyReadTransaction<'a> { return Err(Oauth2Error::InvalidOrigin); } + let require_pkce = match &o2rs.type_ { + OauthRSType::Basic { enable_pkce, .. } => *enable_pkce, + OauthRSType::Public => true, + }; + let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request { - if !o2rs.enable_pkce { + if !require_pkce { security_info!(?o2rs.name, "Insecure rs configuration - pkce is not enforced, but rs is requesting it!"); } // CodeChallengeMethod must be S256 @@ -1217,7 +1276,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { return Err(Oauth2Error::InvalidRequest); } Some(pkce_request.code_challenge.clone()) - } else if o2rs.enable_pkce { + } else if require_pkce { security_error!(?o2rs.name, "No PKCE code challenge was provided with client in enforced PKCE mode."); return Err(Oauth2Error::InvalidRequest); } else { @@ -1498,10 +1557,17 @@ impl<'a> IdmServerProxyReadTransaction<'a> { })?; // check the secret. - if o2rs.authz_secret != secret { - security_info!("Invalid oauth2 client_id secret"); - return Err(Oauth2Error::AuthenticationRequired); - } + match &o2rs.type_ { + OauthRSType::Basic { authz_secret, .. } => { + if authz_secret != &secret { + security_info!("Invalid oauth2 client_id secret"); + return Err(Oauth2Error::AuthenticationRequired); + } + } + // Relies on the token to be valid. + OauthRSType::Public => {} + }; + // We are authenticated! Yay! Now we can actually check things ... let token: Oauth2TokenType = o2rs @@ -1590,7 +1656,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { pub fn oauth2_openid_userinfo( &mut self, client_id: &str, - client_authz: &str, + token_str: &str, ct: Duration, ) -> Result { // DANGER: Why do we have to do this? During the use of qs for internal search @@ -1611,7 +1677,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { let token: Oauth2TokenType = o2rs .token_fernet - .decrypt(client_authz) + .decrypt(token_str) .map_err(|_| { admin_error!("Failed to decrypt token introspection request"); Oauth2Error::InvalidRequest @@ -1954,7 +2020,7 @@ mod tests { } // setup an oauth2 instance. - async fn setup_oauth2_resource_server( + async fn setup_oauth2_resource_server_basic( idms: &IdmServer, ct: Duration, enable_pkce: bool, @@ -2077,6 +2143,105 @@ mod tests { (secret, uat, ident, uuid) } + async fn setup_oauth2_resource_server_public( + idms: &IdmServer, + ct: Duration, + ) -> (UserAuthToken, Identity, Uuid) { + let mut idms_prox_write = idms.proxy_write(ct).await; + + let uuid = Uuid::new_v4(); + + let e: Entry = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("oauth2_resource_server")), + ("class", Value::new_class("oauth2_resource_server_public")), + ("uuid", Value::Uuid(uuid)), + ("oauth2_rs_name", Value::new_iname("test_resource_server")), + ("displayname", Value::new_utf8s("test_resource_server")), + ( + "oauth2_rs_origin", + Value::new_url_s("https://demo.example.com").unwrap() + ), + // System admins + ( + "oauth2_rs_scope_map", + Value::new_oauthscopemap(UUID_SYSTEM_ADMINS, btreeset!["groups".to_string()]) + .expect("invalid oauthscope") + ), + ( + "oauth2_rs_scope_map", + Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["openid".to_string()]) + .expect("invalid oauthscope") + ), + ( + "oauth2_rs_sup_scope_map", + Value::new_oauthscopemap( + UUID_IDM_ALL_ACCOUNTS, + btreeset!["supplement".to_string()] + ) + .expect("invalid oauthscope") + ) + ); + let ce = CreateEvent::new_internal(vec![e]); + assert!(idms_prox_write.qs_write.create(&ce).is_ok()); + + // Setup the uat we'll be using - note for these tests they *require* + // the parent session to be valid and present! + + let session_id = uuid::Uuid::new_v4(); + + let account = idms_prox_write + .target_to_account(UUID_ADMIN) + .expect("account must exist"); + let uat = account + .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .expect("Unable to create uat"); + + // Need the uat first for expiry. + let expiry = uat.expiry; + + let p = CryptoPolicy::minimum(); + let cred = Credential::new_password_only(&p, "test_password").unwrap(); + let cred_id = cred.uuid; + + let session = Value::Session( + session_id, + crate::value::Session { + label: "label".to_string(), + expiry, + issued_at: time::OffsetDateTime::UNIX_EPOCH + ct, + issued_by: IdentityId::Internal, + cred_id, + scope: SessionScope::ReadWrite, + }, + ); + + // Mod the user + let modlist = ModifyList::new_list(vec![ + Modify::Present("user_auth_token_session".into(), session), + Modify::Present( + "primary_credential".into(), + Value::Cred("primary".to_string(), cred), + ), + ]); + + idms_prox_write + .qs_write + .internal_modify( + &filter!(f_eq("uuid", PartialValue::Uuid(UUID_ADMIN))), + &modlist, + ) + .expect("Failed to modify user"); + + let ident = idms_prox_write + .process_uat_to_identity(&uat, ct) + .expect("Unable to process uat"); + + idms_prox_write.commit().expect("failed to commit"); + + (uat, ident, uuid) + } + async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) { let mut idms_prox_write = idms.proxy_write(ct).await; let account = idms_prox_write @@ -2102,7 +2267,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let idms_prox_read = idms.proxy_read().await; @@ -2162,6 +2327,72 @@ mod tests { assert!(idms_prox_write.commit().is_ok()); } + #[idm_test] + async fn test_idm_oauth2_public_function( + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed, + ) { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + let (uat, ident, _) = setup_oauth2_resource_server_public(idms, ct).await; + + let idms_prox_read = idms.proxy_read().await; + + // Get an ident/uat for now. + + // == Setup the authorisation request + let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble"); + + let consent_request = good_authorisation_request!( + idms_prox_read, + &ident, + &uat, + ct, + code_challenge, + "openid".to_string() + ); + + // Should be in the consent phase; + let consent_token = + if let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request { + consent_token + } else { + unreachable!(); + }; + + // == Manually submit the consent token to the permit for the permit_success + drop(idms_prox_read); + let mut idms_prox_write = idms.proxy_write(ct).await; + + let permit_success = idms_prox_write + .check_oauth2_authorise_permit(&ident, &uat, &consent_token, ct) + .expect("Failed to perform oauth2 permit"); + + // Check we are reflecting the CSRF properly. + assert!(permit_success.state == "123"); + + // == Submit the token exchange code. + + let token_req = AccessTokenRequest { + grant_type: GrantTypeReq::AuthorizationCode { + code: permit_success.code, + redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), + // From the first step. + code_verifier, + }, + client_id: Some("test_resource_server".to_string()), + client_secret: None, + }; + + let token_response = idms_prox_write + .check_oauth2_token_exchange(None, &token_req, ct) + .expect("Failed to perform oauth2 token exchange"); + + // 🎉 We got a token! In the future we can then check introspection from this point. + assert!(token_response.token_type == "bearer"); + + assert!(idms_prox_write.commit().is_ok()); + } + #[idm_test] async fn test_idm_oauth2_invalid_authorisation_requests( idms: &IdmServer, @@ -2170,7 +2401,7 @@ mod tests { // Test invalid oauth2 authorisation states/requests. let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let (anon_uat, anon_ident) = setup_idm_admin(idms, ct).await; let (idm_admin_uat, idm_admin_ident) = setup_idm_admin(idms, ct).await; @@ -2334,7 +2565,7 @@ mod tests { // Test invalid oauth2 authorisation states/requests. let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let (uat2, ident2) = { let mut idms_prox_write = idms.proxy_write(ct).await; @@ -2417,7 +2648,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, mut uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; // ⚠️ We set the uat expiry time to 5 seconds from TEST_CURRENT_TIME. This // allows all our other tests to pass, but it means when we specifically put the @@ -2593,7 +2824,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); @@ -2691,7 +2922,7 @@ mod tests { // First, setup to get a token. let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); @@ -2837,7 +3068,7 @@ mod tests { // First, setup to get a token. let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); @@ -2938,7 +3169,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let (uat2, ident2) = { let mut idms_prox_write = idms.proxy_write(ct).await; @@ -3022,7 +3253,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, _uat, _ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let idms_prox_read = idms.proxy_read().await; @@ -3162,7 +3393,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); @@ -3289,7 +3520,7 @@ mod tests { // but change the preferred_username setting on the RS let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, true).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, true).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); @@ -3375,7 +3606,7 @@ mod tests { // but change the preferred_username setting on the RS let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, true).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, true).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); @@ -3469,7 +3700,7 @@ mod tests { async fn test_idm_oauth2_insecure_pkce(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, false, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, false, false, false).await; let idms_prox_read = idms.proxy_read().await; @@ -3511,7 +3742,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, false, true, false).await; + setup_oauth2_resource_server_basic(idms, ct, false, true, false).await; let idms_prox_read = idms.proxy_read().await; // The public key url should offer an rs key // discovery should offer RS256 @@ -3611,7 +3842,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let idms_prox_read = idms.proxy_read().await; @@ -3811,7 +4042,7 @@ mod tests { ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let (_secret, uat, ident, o2rs_uuid) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; // Assert there are no consent maps yet. assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none()); @@ -3899,7 +4130,7 @@ mod tests { let ct = Duration::from_secs(TEST_CURRENT_TIME); // Enable pkce is set to FALSE let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, false, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, false, false, false).await; let idms_prox_read = idms.proxy_read().await; @@ -3976,7 +4207,7 @@ mod tests { let ct = Duration::from_secs(TEST_CURRENT_TIME); // Enable pkce is set to FALSE let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, false, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, false, false, false).await; let idms_prox_read = idms.proxy_read().await; @@ -4064,7 +4295,7 @@ mod tests { ) -> (AccessTokenResponse, Option) { // First, setup to get a token. let (secret, uat, ident, _) = - setup_oauth2_resource_server(idms, ct, true, false, false).await; + setup_oauth2_resource_server_basic(idms, ct, true, false, false).await; let client_authz = Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); diff --git a/server/lib/src/plugins/jwskeygen.rs b/server/lib/src/plugins/jwskeygen.rs index 31517c61a..1e9965ad4 100644 --- a/server/lib/src/plugins/jwskeygen.rs +++ b/server/lib/src/plugins/jwskeygen.rs @@ -46,12 +46,14 @@ impl Plugin for JwsKeygen { impl JwsKeygen { fn modify_inner(cand: &mut [Entry]) -> Result<(), OperationError> { cand.iter_mut().try_for_each(|e| { - if e.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) { - if !e.attribute_pres("oauth2_rs_basic_secret") { + if e.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) && + !e.attribute_pres("oauth2_rs_basic_secret") { security_info!("regenerating oauth2 basic secret"); let v = Value::SecretValue(password_from_random()); e.add_ava("oauth2_rs_basic_secret", v); - } + } + + if e.attribute_equality("class", &PVCLASS_OAUTH2_RS) { if !e.attribute_pres("oauth2_rs_token_key") { security_info!("regenerating oauth2 token key"); let k = fernet::Fernet::generate_key(); diff --git a/server/lib/src/plugins/refint.rs b/server/lib/src/plugins/refint.rs index b8515233f..601181e91 100644 --- a/server/lib/src/plugins/refint.rs +++ b/server/lib/src/plugins/refint.rs @@ -731,7 +731,7 @@ mod tests { let ea: Entry = entry_init!( ("class", Value::new_class("object")), ("class", Value::new_class("oauth2_resource_server")), - ("class", Value::new_class("oauth2_resource_server_basic")), + // ("class", Value::new_class("oauth2_resource_server_basic")), ("oauth2_rs_name", Value::new_iname("test_resource_server")), ("displayname", Value::new_utf8s("test_resource_server")), ( @@ -812,7 +812,7 @@ mod tests { let e2 = entry_init!( ("class", Value::new_class("object")), ("class", Value::new_class("oauth2_resource_server")), - ("class", Value::new_class("oauth2_resource_server_basic")), + // ("class", Value::new_class("oauth2_resource_server_basic")), ("uuid", Value::Uuid(rs_uuid)), ("oauth2_rs_name", Value::new_iname("test_resource_server")), ("displayname", Value::new_utf8s("test_resource_server")), diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index b5d17d5ca..c5a5a48c3 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -504,9 +504,8 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_CLASS_POSIXACCOUNT, JSON_SCHEMA_CLASS_POSIXGROUP, JSON_SCHEMA_CLASS_SYSTEM_CONFIG, - JSON_SCHEMA_CLASS_OAUTH2_RS, - JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC, JSON_SCHEMA_CLASS_SYNC_ACCOUNT, + JSON_SCHEMA_CLASS_OAUTH2_RS, JSON_SCHEMA_ATTR_PRIVATE_COOKIE_KEY, ]; @@ -515,13 +514,28 @@ impl<'a> QueryServerWriteTransaction<'a> { // Each item individually logs it's result .try_for_each(|e_str| self.internal_migrate_or_create_str(e_str)); - if r.is_ok() { - debug!("initialise_schema_idm -> Ok!"); - } else { + if r.is_err() { + error!(res = ?r, "initialise_schema_idm -> Error"); + } + + debug_assert!(r.is_ok()); + + let idm_schema_classes = [ + E_SCHEMA_CLASS_OAUTH2_RS_BASIC.clone(), + E_SCHEMA_CLASS_OAUTH2_RS_PUBLIC.clone(), + ]; + + let r: Result<(), _> = idm_schema_classes + .into_iter() + .try_for_each(|entry| self.internal_migrate_or_create(entry)); + + if r.is_err() { error!(res = ?r, "initialise_schema_idm -> Error"); } debug_assert!(r.is_ok()); + debug!("initialise_schema_idm -> Ok!"); + r } diff --git a/server/testkit/tests/https_middleware.rs b/server/testkit/tests/https_middleware.rs index 535379164..4cb97b6ba 100644 --- a/server/testkit/tests/https_middleware.rs +++ b/server/testkit/tests/https_middleware.rs @@ -6,7 +6,7 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) { let addr = rsclient.get_url(); // here we test the /ui/ endpoint which should have the headers - let response = match reqwest::get(format!("{}/ui/", &addr)).await { + let response = match reqwest::get(format!("{}/ui", &addr)).await { Ok(value) => value, Err(error) => { panic!("Failed to query {:?} : {:#?}", addr, error); diff --git a/server/testkit/tests/oauth2_test.rs b/server/testkit/tests/oauth2_test.rs index b60a62116..5ee2a89bc 100644 --- a/server/testkit/tests/oauth2_test.rs +++ b/server/testkit/tests/oauth2_test.rs @@ -384,6 +384,257 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) { .expect("Failed to update oauth2 scopes"); } +#[kanidmd_testkit::test] +async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) { + let res = rsclient + .auth_simple_password("admin", ADMIN_TEST_PASSWORD) + .await; + assert!(res.is_ok()); + + // Create an oauth2 application integration. + rsclient + .idm_oauth2_rs_public_create( + TEST_INTEGRATION_RS_ID, + TEST_INTEGRATION_RS_DISPLAY, + TEST_INTEGRATION_RS_URL, + ) + .await + .expect("Failed to create oauth2 config"); + + // Extend the admin account with extended details for openid claims. + rsclient + .idm_group_add_members("idm_admins", &["admin"]) + .await + .unwrap(); + + rsclient + .idm_person_account_create("oauth_test", "oauth_test") + .await + .expect("Failed to create account details"); + + rsclient + .idm_person_account_set_attr("oauth_test", "mail", &["oauth_test@localhost"]) + .await + .expect("Failed to create account mail"); + + rsclient + .idm_person_account_primary_credential_set_password("oauth_test", ADMIN_TEST_PASSWORD) + .await + .expect("Failed to configure account password"); + + rsclient + .idm_oauth2_rs_update("test_integration", None, None, None, None, true, true, true) + .await + .expect("Failed to update oauth2 config"); + + rsclient + .idm_oauth2_rs_update_scope_map( + "test_integration", + "idm_all_accounts", + vec!["read", "email", "openid"], + ) + .await + .expect("Failed to update oauth2 scopes"); + + rsclient + .idm_oauth2_rs_update_sup_scope_map("test_integration", "idm_all_accounts", vec!["admin"]) + .await + .expect("Failed to update oauth2 scopes"); + + // Get our admin's auth token for our new client. + // We have to re-auth to update the mail field. + let res = rsclient + .auth_simple_password("oauth_test", ADMIN_TEST_PASSWORD) + .await; + assert!(res.is_ok()); + let oauth_test_uat = rsclient + .get_token() + .await + .expect("No user auth token found"); + + let url = rsclient.get_url().to_string(); + + // We need a new reqwest client here. + + // from here, we can now begin what would be a "interaction" to the oauth server. + // Create a new reqwest client - we'll be using this manually. + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .no_proxy() + .build() + .expect("Failed to create client."); + + // Step 0 - get the jwks public key. + let response = client + .get(format!( + "{}/oauth2/openid/test_integration/public_key.jwk", + url + )) + .send() + .await + .expect("Failed to send request."); + + assert!(response.status() == reqwest::StatusCode::OK); + assert_no_cache!(response); + + let mut jwk_set: JwkKeySet = response + .json() + .await + .expect("Failed to access response body"); + + let public_jwk = jwk_set.keys.pop().expect("No public key in set!"); + + let jws_validator = JwsValidator::try_from(&public_jwk).expect("failed to build validator"); + + // Step 1 - the Oauth2 Resource Server would send a redirect to the authorisation + // server, where the url contains a series of authorisation request parameters. + // + // Since we are a client, we can just "pretend" we got the redirect, and issue the + // get call directly. This should be a 200. (?) + let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); + + let response = client + .get(format!("{}/oauth2/authorise", url)) + .bearer_auth(oauth_test_uat.clone()) + .query(&[ + ("response_type", "code"), + ("client_id", "test_integration"), + ("state", "YWJjZGVm"), + ("code_challenge", pkce_code_challenge.as_str()), + ("code_challenge_method", "S256"), + ("redirect_uri", "https://demo.example.com/oauth2/flow"), + ("scope", "email read openid"), + ]) + .send() + .await + .expect("Failed to send request."); + + assert!(response.status() == reqwest::StatusCode::OK); + assert_no_cache!(response); + + let consent_req: AuthorisationResponse = response + .json() + .await + .expect("Failed to access response body"); + + let consent_token = if let AuthorisationResponse::ConsentRequested { + consent_token, + scopes, + .. + } = consent_req + { + // Note the supplemental scope here (admin) + assert!(scopes.contains(&"admin".to_string())); + consent_token + } else { + unreachable!(); + }; + + // Step 2 - we now send the consent get to the server which yields a redirect with a + // state and code. + let response = client + .get(format!("{}/oauth2/authorise/permit", url)) + .bearer_auth(oauth_test_uat) + .query(&[("token", consent_token.as_str())]) + .send() + .await + .expect("Failed to send request."); + + // This should yield a 302 redirect with some query params. + assert!(response.status() == reqwest::StatusCode::FOUND); + assert_no_cache!(response); + + // And we should have a URL in the location header. + let redir_str = response + .headers() + .get("Location") + .and_then(|hv| hv.to_str().ok().map(str::to_string)) + .expect("Invalid redirect url"); + + // Now check it's content + let redir_url = Url::parse(&redir_str).expect("Url parse failure"); + + // We should have state and code. + let pairs: HashMap<_, _> = redir_url.query_pairs().collect(); + + let code = pairs.get("code").expect("code not found!"); + + let state = pairs.get("state").expect("state not found!"); + + assert!(state == "YWJjZGVm"); + + // Step 3 - the "resource server" then uses this state and code to directly contact + // the authorisation server to request a token. + + let form_req = AccessTokenRequest { + grant_type: GrantTypeReq::AuthorizationCode { + code: code.to_string(), + redirect_uri: Url::parse("https://demo.example.com/oauth2/flow").expect("Invalid URL"), + code_verifier: Some(pkce_code_verifier.secret().clone()), + }, + client_id: Some("test_integration".to_string()), + client_secret: None, + }; + + let response = client + .post(format!("{}/oauth2/token", url)) + .form(&form_req) + .send() + .await + .expect("Failed to send code exchange request."); + + assert!(response.status() == reqwest::StatusCode::OK); + assert_no_cache!(response); + + // The body is a json AccessTokenResponse + let atr = response + .json::() + .await + .expect("Unable to decode AccessTokenResponse"); + + // Step 5 - check that the id_token (openid) matches the userinfo endpoint. + let oidc_unverified = + OidcUnverified::from_str(atr.id_token.as_ref().unwrap()).expect("Failed to parse id_token"); + + let oidc = oidc_unverified + .validate(&jws_validator, 0) + .expect("Failed to verify oidc"); + + // This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc + // token and the userinfo endpoints. + assert!(oidc.iss == Url::parse(&format!("{}/oauth2/openid/test_integration", url)).unwrap()); + eprintln!("{:?}", oidc.s_claims.email); + assert!(oidc.s_claims.email.as_deref() == Some("oauth_test@localhost")); + assert!(oidc.s_claims.email_verified == Some(true)); + + let response = client + .get(format!("{}/oauth2/openid/test_integration/userinfo", url)) + .bearer_auth(atr.access_token.clone()) + .send() + .await + .expect("Failed to send userinfo request."); + + let userinfo = response + .json::() + .await + .expect("Unable to decode OidcToken from userinfo"); + + eprintln!("userinfo {userinfo:?}"); + eprintln!("oidc {oidc:?}"); + + assert!(userinfo == oidc); + + // auth back with admin so we can test deleting things + let res = rsclient + .auth_simple_password("admin", ADMIN_TEST_PASSWORD) + .await; + assert!(res.is_ok()); + rsclient + .idm_oauth2_rs_delete_sup_scope_map("test_integration", TEST_INTEGRATION_RS_GROUP_ALL) + .await + .expect("Failed to update oauth2 scopes"); +} + #[kanidmd_testkit::test] async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) { let res = rsclient diff --git a/server/testkit/tests/routes.rs b/server/testkit/tests/routes.rs index 8e1fb3d46..5b35b0d44 100644 --- a/server/testkit/tests/routes.rs +++ b/server/testkit/tests/routes.rs @@ -17,11 +17,11 @@ async fn test_routes(rsclient: KanidmClient) { "method": "GET" }, { - "path": "/ui/", + "path": "/ui", "method": "GET" }, { - "path": "/ui/*", + "path": "/ui/login", "method": "GET" }, { diff --git a/server/web_ui/pkg/kanidmd_web_ui.js b/server/web_ui/pkg/kanidmd_web_ui.js index 92d035f34..0cdb26c05 100644 --- a/server/web_ui/pkg/kanidmd_web_ui.js +++ b/server/web_ui/pkg/kanidmd_web_ui.js @@ -8,7 +8,23 @@ heap.push(undefined, null, true, false); function getObject(idx) { return heap[idx]; } -let WASM_VECTOR_LEN = 0; +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; let cachedUint8Memory0 = null; @@ -19,6 +35,22 @@ function getUint8Memory0() { return cachedUint8Memory0; } +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +let WASM_VECTOR_LEN = 0; + const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' @@ -85,38 +117,6 @@ function getInt32Memory0() { return cachedInt32Memory0; } -let heap_next = heap.length; - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - -const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); - -if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -function dropObject(idx) { - if (idx < 132) return; - heap[idx] = heap_next; - heap_next = idx; -} - -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} - let cachedFloat64Memory0 = null; function getFloat64Memory0() { @@ -234,7 +234,7 @@ function addBorrowedObject(obj) { } function __wbg_adapter_48(arg0, arg1, arg2) { try { - wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6570e9fcbe2dd992(arg0, arg1, addBorrowedObject(arg2)); + wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6181404b47c1b27d(arg0, arg1, addBorrowedObject(arg2)); } finally { heap[stack_pointer++] = undefined; } @@ -242,14 +242,14 @@ function __wbg_adapter_48(arg0, arg1, arg2) { function __wbg_adapter_51(arg0, arg1, arg2) { try { - wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h9a25baf5f77e5e3e(arg0, arg1, addBorrowedObject(arg2)); + wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0469109c0dc279df(arg0, arg1, addBorrowedObject(arg2)); } finally { heap[stack_pointer++] = undefined; } } function __wbg_adapter_54(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb285c11f4a69b963(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h23ae592972fec7fc(arg0, arg1, addHeapObject(arg2)); } /** @@ -330,6 +330,22 @@ async function __wbg_load(module, imports) { function __wbg_get_imports() { const imports = {}; imports.wbg = {}; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'string' ? obj : undefined; @@ -338,40 +354,16 @@ function __wbg_get_imports() { getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbindgen_bigint_from_i64 = function(arg0) { - const ret = arg0; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) { - const ret = getObject(arg0) === getObject(arg1); - return ret; - }; - imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) { - const ret = BigInt.asUintN(64, arg0); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_error_new = function(arg0, arg1) { - const ret = new Error(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - const ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; imports.wbg.__wbindgen_object_clone_ref = function(arg0) { const ret = getObject(arg0); return addHeapObject(ret); }; - imports.wbg.__wbindgen_is_undefined = function(arg0) { - const ret = getObject(arg0) === undefined; - return ret; + imports.wbg.__wbg_modalhidebyid_14daee5d362376c0 = function(arg0, arg1) { + modal_hide_by_id(getStringFromWasm0(arg0, arg1)); }; - imports.wbg.__wbindgen_in = function(arg0, arg1) { - const ret = getObject(arg0) in getObject(arg1); - return ret; + imports.wbg.__wbindgen_error_new = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); }; imports.wbg.__wbindgen_boolean_get = function(arg0) { const v = getObject(arg0); @@ -382,6 +374,14 @@ function __wbg_get_imports() { const ret = typeof(getObject(arg0)) === 'bigint'; return ret; }; + imports.wbg.__wbindgen_bigint_from_i64 = function(arg0) { + const ret = arg0; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) { + const ret = getObject(arg0) === getObject(arg1); + return ret; + }; imports.wbg.__wbindgen_number_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'number' ? obj : undefined; @@ -393,22 +393,22 @@ function __wbg_get_imports() { const ret = typeof(val) === 'object' && val !== null; return ret; }; + imports.wbg.__wbindgen_in = function(arg0, arg1) { + const ret = getObject(arg0) in getObject(arg1); + return ret; + }; + imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) { + const ret = BigInt.asUintN(64, arg0); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_is_string = function(arg0) { const ret = typeof(getObject(arg0)) === 'string'; return ret; }; - imports.wbg.__wbindgen_cb_drop = function(arg0) { - const obj = takeObject(arg0).original; - if (obj.cnt-- == 1) { - obj.a = 0; - return true; - } - const ret = false; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; return ret; }; - imports.wbg.__wbg_modalhidebyid_14daee5d362376c0 = function(arg0, arg1) { - modal_hide_by_id(getStringFromWasm0(arg0, arg1)); - }; imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) { const ret = getObject(arg1).__yew_listener_id; getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; @@ -470,6 +470,13 @@ function __wbg_get_imports() { imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) { getObject(arg0)[takeObject(arg1)] = takeObject(arg2); }; + imports.wbg.__wbg_getwithrefkey_5e6d9547403deab8 = function(arg0, arg1) { + const ret = getObject(arg0)[getObject(arg1)]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_841ac57cff3d672b = function(arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + }; imports.wbg.__wbg_debug_783a3d4910bc24c7 = function(arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4); @@ -1118,16 +1125,16 @@ function __wbg_get_imports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper4655 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1076, __wbg_adapter_48); + imports.wbg.__wbindgen_closure_wrapper2575 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1196, __wbg_adapter_48); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper5440 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1368, __wbg_adapter_51); + imports.wbg.__wbindgen_closure_wrapper3416 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1503, __wbg_adapter_51); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper6559 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1442, __wbg_adapter_54); + imports.wbg.__wbindgen_closure_wrapper4520 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1577, __wbg_adapter_54); return addHeapObject(ret); }; diff --git a/server/web_ui/pkg/kanidmd_web_ui_bg.wasm b/server/web_ui/pkg/kanidmd_web_ui_bg.wasm index c0ad13653..38a5486ed 100644 Binary files a/server/web_ui/pkg/kanidmd_web_ui_bg.wasm and b/server/web_ui/pkg/kanidmd_web_ui_bg.wasm differ diff --git a/server/web_ui/src/credential/reset.rs b/server/web_ui/src/credential/reset.rs index b21c29304..0cb323f08 100644 --- a/server/web_ui/src/credential/reset.rs +++ b/server/web_ui/src/credential/reset.rs @@ -573,7 +573,7 @@ impl CredentialResetApp { { pw_warn } { pw_html_inner } - + } } @@ -593,7 +593,7 @@ impl CredentialResetApp { <>

{ "Strong cryptographic authenticators with self contained multi-factor authentication." }

{ "No Passkeys Registered" }

- + } } else { diff --git a/server/web_ui/src/lib.rs b/server/web_ui/src/lib.rs index 20fbbb946..8680730bd 100644 --- a/server/web_ui/src/lib.rs +++ b/server/web_ui/src/lib.rs @@ -69,6 +69,7 @@ pub async fn do_request( opts.method(&method.to_string()); opts.mode(RequestMode::SameOrigin); opts.credentials(web_sys::RequestCredentials::SameOrigin); + if let Some(body) = body { #[cfg(debug_assertions)] if method == RequestMethod::GET { @@ -81,7 +82,21 @@ pub async fn do_request( request .headers() .set("content-type", "application/json") - .expect_throw("failed to set header"); + .expect_throw("failed to set content-type header"); + + if let Some(sessionid) = models::pop_auth_session_id() { + request + .headers() + .set("x-kanidm-auth-session-id", &sessionid) + .expect_throw("failed to set auth session id header"); + } + + if let Some(bearer_token) = models::get_bearer_token() { + request + .headers() + .set("authorization", &bearer_token) + .expect_throw("failed to set authorisation header"); + } let window = utils::window(); let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; @@ -89,6 +104,10 @@ pub async fn do_request( let status = resp.status(); let headers: Headers = resp.headers(); + if let Some(sessionid) = headers.get("x-kanidm-auth-session-id").ok().flatten() { + models::push_auth_session_id(sessionid); + } + let kopid = headers.get("x-kanidm-opid").ok().flatten(); Ok((kopid, status, JsFuture::from(resp.json()?).await?, headers)) diff --git a/server/web_ui/src/login/mod.rs b/server/web_ui/src/login/mod.rs index fee3627a2..61ac758f4 100644 --- a/server/web_ui/src/login/mod.rs +++ b/server/web_ui/src/login/mod.rs @@ -99,7 +99,7 @@ impl LoginApp { let authreq = AuthRequest { step: AuthStep::Init2 { username, - issue: AuthIssueSession::Cookie, + issue: AuthIssueSession::Token, }, }; let req_jsvalue = serde_json::to_string(&authreq) @@ -127,7 +127,7 @@ impl LoginApp { } async fn reauth_init() -> Result { - let issue = AuthIssueSession::Cookie; + let issue = AuthIssueSession::Token; let authreq_jsvalue = serde_json::to_string(&issue) .map(|s| JsValue::from(&s)) .expect_throw("Failed to serialise authreq"); @@ -588,7 +588,7 @@ impl Component for LoginApp { fn create(ctx: &Context) -> Self { #[cfg(debug_assertions)] - console::debug!("create".to_string()); + console::debug!("login::create".to_string()); let workflow = &ctx.props().workflow; let state = match workflow { @@ -602,21 +602,6 @@ impl Component for LoginApp { .or_else(|| models::get_login_remember_me().map(|user| (user, true))) .unwrap_or_default(); - #[cfg(debug_assertions)] - { - let document = utils::document(); - let html_document = document - .dyn_into::() - .expect_throw("failed to dyn cast to htmldocument"); - let cookie = html_document - .cookie() - .expect_throw("failed to access page cookies"); - console::debug!("cookies".to_string()); - console::debug!(cookie); - } - // Clean any cookies. - // TODO: actually check that it's cleaning the cookies. - LoginState::InitLogin { enable: true, remember_me, @@ -958,22 +943,13 @@ impl Component for LoginApp { self.state = LoginState::Denied(reason); true } - AuthState::Success(_bearer_token) => { + AuthState::Success(bearer_token) => { // Store the bearer here! - /* + // We need to format the bearer onto it. + let bearer_token = format!("Bearer {}", bearer_token); models::set_bearer_token(bearer_token); self.state = LoginState::Authenticated; true - */ - self.state = LoginState::Error { - emsg: "Invalid Issued Session Type, expected cookie".to_string(), - kopid: None, - }; - true - } - AuthState::SuccessCookie => { - self.state = LoginState::Authenticated; - true } } } diff --git a/server/web_ui/src/manager.rs b/server/web_ui/src/manager.rs index e279f6e9d..4f9de80c0 100644 --- a/server/web_ui/src/manager.rs +++ b/server/web_ui/src/manager.rs @@ -19,7 +19,7 @@ use crate::views::{ViewRoute, ViewsApp}; // router to decide on state. #[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum Route { - #[at("/")] + #[at("/ui")] Landing, #[at("/ui/login")] @@ -44,6 +44,8 @@ pub enum Route { #[function_component(Landing)] fn landing() -> Html { + #[cfg(debug_assertions)] + console::debug!("manager::landing"); // Do this to allow use_navigator to work because lol. yew_router::hooks::use_navigator() .expect_throw("Unable to access history") @@ -55,7 +57,7 @@ fn landing() -> Html { #[allow(clippy::needless_pass_by_value)] fn switch(route: Route) -> Html { #[cfg(debug_assertions)] - console::debug!("manager::switch"); + console::debug!(format!("manager::switch -> {:?}", route).as_str()); match route { #[allow(clippy::let_unit_value)] Route::Landing => html! { }, diff --git a/server/web_ui/src/models/mod.rs b/server/web_ui/src/models/mod.rs index a5d281bbf..09646df54 100644 --- a/server/web_ui/src/models/mod.rs +++ b/server/web_ui/src/models/mod.rs @@ -12,8 +12,36 @@ use yew_router::navigator::Navigator; use crate::manager::Route; use crate::views::ViewRoute; +pub fn set_bearer_token(r: String) { + PersistentStorage::set("bearer_token", r).expect_throw("failed to set bearer_token"); +} + +pub fn get_bearer_token() -> Option { + let l: Result = PersistentStorage::get("bearer_token"); + #[cfg(debug_assertions)] + console::debug!(format!( + "login_hint::get_login_remember_me -> present={:?}", + l.is_ok() + ) + .as_str()); + l.ok() +} + pub fn clear_bearer_token() { - PersistentStorage::delete("kanidm_bearer_token"); + PersistentStorage::delete("bearer_token"); +} + +pub fn push_auth_session_id(r: String) { + TemporaryStorage::set("auth_session_id", r) + .expect_throw("failed to set auth_session_id in temporary storage"); +} + +pub fn pop_auth_session_id() -> Option { + let l: Result = TemporaryStorage::get("auth_session_id"); + #[cfg(debug_assertions)] + console::debug!(format!("auth_session_id -> {:?}", l).as_str()); + TemporaryStorage::delete("auth_session_id"); + l.ok() } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] diff --git a/tools/cli/src/cli/oauth2.rs b/tools/cli/src/cli/oauth2.rs index 86f340dfd..571529826 100644 --- a/tools/cli/src/cli/oauth2.rs +++ b/tools/cli/src/cli/oauth2.rs @@ -6,12 +6,12 @@ impl Oauth2Opt { match self { Oauth2Opt::List(copt) => copt.debug, Oauth2Opt::Get(nopt) => nopt.copt.debug, - Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::UpdateScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::DeleteScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::UpdateSupScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::DeleteSupScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::ResetSecrets(cbopt) => cbopt.copt.debug, + // Should this be renamed to show client id? client secrets? Oauth2Opt::ShowBasicSecret(nopt) => nopt.copt.debug, Oauth2Opt::Delete(nopt) => nopt.copt.debug, Oauth2Opt::SetDisplayname(cbopt) => cbopt.nopt.copt.debug, @@ -23,6 +23,9 @@ impl Oauth2Opt { Oauth2Opt::DisableLegacyCrypto(nopt) => nopt.copt.debug, Oauth2Opt::PreferShortUsername(nopt) => nopt.copt.debug, Oauth2Opt::PreferSPNUsername(nopt) => nopt.copt.debug, + Oauth2Opt::CreateBasic { copt, .. } | Oauth2Opt::CreatePublic { copt, .. } => { + copt.debug + } } } @@ -43,13 +46,37 @@ impl Oauth2Opt { Err(e) => error!("Error -> {:?}", e), } } - Oauth2Opt::CreateBasic(cbopt) => { - let client = cbopt.nopt.copt.to_client(OpType::Read).await; + Oauth2Opt::CreateBasic { + name, + displayname, + origin, + copt, + } => { + let client = copt.to_client(OpType::Write).await; match client .idm_oauth2_rs_basic_create( - cbopt.nopt.name.as_str(), - cbopt.displayname.as_str(), - cbopt.origin.as_str(), + name.as_str(), + displayname.as_str(), + origin.as_str(), + ) + .await + { + Ok(_) => println!("Success"), + Err(e) => error!("Error -> {:?}", e), + } + } + Oauth2Opt::CreatePublic { + name, + displayname, + origin, + copt, + } => { + let client = copt.to_client(OpType::Write).await; + match client + .idm_oauth2_rs_public_create( + name.as_str(), + displayname.as_str(), + origin.as_str(), ) .await { diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index f7b8df42e..9395220d0 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -606,16 +606,6 @@ pub enum SelfOpt { Whoami(CommonOpt), } -#[derive(Debug, Args)] -pub struct Oauth2BasicCreateOpt { - #[clap(flatten)] - nopt: Named, - #[clap(name = "displayname")] - displayname: String, - #[clap(name = "origin")] - origin: String, -} - #[derive(Debug, Args)] pub struct Oauth2SetDisplayname { #[clap(flatten)] @@ -662,8 +652,33 @@ pub enum Oauth2Opt { // /// Set options for a selected oauth2 resource server // Set(), #[clap(name = "create")] - /// Create a new oauth2 resource server - CreateBasic(Oauth2BasicCreateOpt), + /// Create a new oauth2 confidential resource server that is protected by basic auth. + CreateBasic { + #[clap(name = "name")] + name: String, + #[clap(name = "displayname")] + displayname: String, + #[clap(name = "origin")] + origin: String, + #[clap(flatten)] + copt: CommonOpt, + }, + #[clap(name = "create-public")] + /// Create a new OAuth2 public resource server that requires PKCE. You should prefer + /// using confidential resource server types if possible over public ones. + /// + /// Public clients have many limitations and can not access all API's of OAuth2. For + /// example rfc7662 token introspection requires client authentication. + CreatePublic { + #[clap(name = "name")] + name: String, + #[clap(name = "displayname")] + displayname: String, + #[clap(name = "origin")] + origin: String, + #[clap(flatten)] + copt: CommonOpt, + }, #[clap(name = "update-scope-map", visible_aliases=&["create-scope-map"])] /// Update or add a new mapping from a group to scopes that it provides to members UpdateScopeMap(Oauth2CreateScopeMapOpt),