mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
1792 public oauth clients (#1821)
This commit is contained in:
parent
d1f51f0a84
commit
8e1e533f40
219
Cargo.lock
generated
219
Cargo.lock
generated
|
@ -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",
|
||||
|
|
1
Makefile
1
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' \
|
||||
|
|
|
@ -216,7 +216,7 @@ title=WARNING text=Changing these settings MAY have serious consequences on the
|
|||
|
||||
<!-- deno-fmt-ignore-end -->
|
||||
|
||||
To disable PKCE for a resource server:
|
||||
To disable PKCE for a confidential resource server:
|
||||
|
||||
```bash
|
||||
kanidm system oauth2 warning-insecure-client-disable-pkce <resource server name>
|
||||
|
@ -228,6 +228,32 @@ To enable legacy cryptograhy (RSA PKCS1-5 SHA256):
|
|||
kanidm system oauth2 warning-enable-legacy-crypto <resource server name>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- deno-fmt-ignore-start -->
|
||||
|
||||
{{#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.
|
||||
}}
|
||||
|
||||
<!-- deno-fmt-ignore-end -->
|
||||
|
||||
To create an OAuth2 public resource server:
|
||||
|
||||
```bash
|
||||
kanidm system oauth2 create-public <name> <displayname> <origin>
|
||||
kanidm system oauth2 create mywebapp "My Web App" https://webapp.example.com
|
||||
```
|
||||
|
||||
## Example Integrations
|
||||
|
||||
### Apache mod\_auth\_openidc
|
||||
|
|
|
@ -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<Vec<Entry>, 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<Option<Entry>, 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<Option<String>, 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<String> = 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<String> = 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<Vec<Entry>, ClientError> {
|
||||
self.perform_get_request("/v1/recycle_bin").await
|
||||
|
|
247
libs/client/src/oauth.rs
Normal file
247
libs/client/src/oauth.rs
Normal file
|
@ -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<Vec<Entry>, 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<Option<Entry>, 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<Option<String>, 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<String> = 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<String> = 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
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@ impl KanidmClient {
|
|||
format!("/v1/sync_account/{}/_attr/sync_credential_portal", id).as_str(),
|
||||
)
|
||||
.await
|
||||
.map(|values: Vec<Url>| values.get(0).map(|u| u.clone()))
|
||||
.map(|values: Vec<Url>| values.get(0).cloned())
|
||||
}
|
||||
|
||||
pub async fn idm_sync_account_create(
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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<B>(
|
|||
// 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::<SessionHandle>() {
|
||||
Some(sess) => {
|
||||
// we have a session!
|
||||
sess.read().await.get::<String>("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 });
|
||||
|
|
|
@ -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<Uuid> {
|
||||
fn get_current_auth_session_id(&self, headers: &HeaderMap) -> Option<Uuid> {
|
||||
// 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::<Uuid>("auth-session-id"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,7 +126,6 @@ pub fn get_js_files(role: ServerRole) -> Vec<JavaScriptFile> {
|
|||
|
||||
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
|
||||
|
|
|
@ -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<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> 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<FilterInvalid> {
|
||||
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<KOpId>,
|
||||
) -> Response<Body> {
|
||||
// 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 {
|
||||
|
|
|
@ -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<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
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<ServerState>,
|
||||
mut msession: WritableSession,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> 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<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
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<ServerState>,
|
||||
TrustedClientIp(ip_addr): TrustedClientIp,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
session: WritableSession,
|
||||
Json(obj): Json<AuthIssueSession>,
|
||||
) -> 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<ServerState>,
|
||||
TrustedClientIp(ip_addr): TrustedClientIp,
|
||||
session: WritableSession,
|
||||
headers: HeaderMap,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<AuthRequest>,
|
||||
|
@ -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<AuthResult, OperationError>,
|
||||
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<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
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<ServerState> {
|
||||
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<ServerState> {
|
|||
"/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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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<Uuid, BTreeSet<String>>,
|
||||
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||
// 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<String>,
|
||||
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<String> =
|
||||
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<String> =
|
||||
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<AccessTokenResponse, Oauth2Error> {
|
||||
// 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<OidcToken, Oauth2Error> {
|
||||
// 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<EntryInit, EntryNew> = 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<String>) {
|
||||
// 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}")));
|
||||
|
||||
|
|
|
@ -46,12 +46,14 @@ impl Plugin for JwsKeygen {
|
|||
impl JwsKeygen {
|
||||
fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> 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();
|
||||
|
|
|
@ -731,7 +731,7 @@ mod tests {
|
|||
let ea: Entry<EntryInit, EntryNew> = 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")),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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::<AccessTokenResponse>()
|
||||
.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::<OidcToken>()
|
||||
.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
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
|
@ -573,7 +573,7 @@ impl CredentialResetApp {
|
|||
{ pw_warn }
|
||||
{ pw_html_inner }
|
||||
|
||||
<PwModalApp token={ token.clone() } cb={ cb.clone() } />
|
||||
<PwModalApp token={ token.clone() } cb={ cb } />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
@ -593,7 +593,7 @@ impl CredentialResetApp {
|
|||
<>
|
||||
<p>{ "Strong cryptographic authenticators with self contained multi-factor authentication." }</p>
|
||||
<p>{ "No Passkeys Registered" }</p>
|
||||
<PasskeyModalApp token={ token.clone() } cb={ cb.clone() } />
|
||||
<PasskeyModalApp token={ token.clone() } cb={ cb } />
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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<LoginAppMsg, FetchError> {
|
||||
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>) -> 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::<web_sys::HtmlDocument>()
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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! { <Landing /> },
|
||||
|
|
|
@ -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<String> {
|
||||
let l: Result<String, _> = 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<String> {
|
||||
let l: Result<String, _> = 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)]
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue