1792 public oauth clients (#1821)

This commit is contained in:
Firstyear 2023-07-07 18:53:31 +10:00 committed by GitHub
parent d1f51f0a84
commit 8e1e533f40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1319 additions and 917 deletions

219
Cargo.lock generated
View file

@ -88,7 +88,7 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"once_cell", "once_cell",
"version_check", "version_check",
] ]
@ -152,18 +152,6 @@ dependencies = [
"password-hash", "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]] [[package]]
name = "asn1-rs" name = "asn1-rs"
version = "0.3.1" version = "0.3.1"
@ -279,7 +267,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"autocfg", "autocfg",
"cfg-if 1.0.0", "cfg-if",
"concurrent-queue", "concurrent-queue",
"futures-lite", "futures-lite",
"log", "log",
@ -311,27 +299,6 @@ dependencies = [
"syn 2.0.23", "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]] [[package]]
name = "async-std" name = "async-std"
version = "1.12.0" version = "1.12.0"
@ -400,7 +367,7 @@ checksum = "d06c690e5e2800f70c0cf8773a9fe7680d66e719dae9b4cabedd13ef4885d056"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bitflags 1.3.2", "bitflags 1.3.2",
"cfg-if 1.0.0", "cfg-if",
"core-foundation", "core-foundation",
"devd-rs", "devd-rs",
"libc", "libc",
@ -503,29 +470,6 @@ dependencies = [
"tokio", "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]] [[package]]
name = "axum-macros" name = "axum-macros"
version = "0.3.7" version = "0.3.7"
@ -556,22 +500,6 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.68" version = "0.3.68"
@ -580,7 +508,7 @@ checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc", "cc",
"cfg-if 1.0.0", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
"object", "object",
@ -708,21 +636,6 @@ dependencies = [
"digest 0.10.7", "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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.7.3" version = "0.7.3"
@ -837,12 +750,6 @@ dependencies = [
"nom", "nom",
] ]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -1034,7 +941,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1044,12 +951,6 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.14.4" version = "0.14.4"
@ -1059,7 +960,7 @@ dependencies = [
"aes-gcm", "aes-gcm",
"base64 0.13.1", "base64 0.13.1",
"hkdf", "hkdf",
"hmac 0.10.1", "hmac",
"percent-encoding", "percent-encoding",
"rand 0.8.5", "rand 0.8.5",
"sha2 0.9.9", "sha2 0.9.9",
@ -1078,22 +979,6 @@ dependencies = [
"version_check", "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]] [[package]]
name = "cookie_store" name = "cookie_store"
version = "0.16.2" version = "0.16.2"
@ -1148,7 +1033,7 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
] ]
[[package]] [[package]]
@ -1204,7 +1089,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"crossbeam-channel", "crossbeam-channel",
"crossbeam-deque", "crossbeam-deque",
"crossbeam-epoch", "crossbeam-epoch",
@ -1218,7 +1103,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
] ]
@ -1228,7 +1113,7 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"crossbeam-epoch", "crossbeam-epoch",
"crossbeam-utils", "crossbeam-utils",
] ]
@ -1240,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cfg-if 1.0.0", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
"memoffset 0.9.0", "memoffset 0.9.0",
"scopeguard", "scopeguard",
@ -1252,7 +1137,7 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
] ]
@ -1262,7 +1147,7 @@ version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
] ]
[[package]] [[package]]
@ -1275,16 +1160,6 @@ dependencies = [
"typenum", "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]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.10.1" version = "0.10.1"
@ -1295,16 +1170,6 @@ dependencies = [
"subtle", "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]] [[package]]
name = "csv" name = "csv"
version = "1.2.2" version = "1.2.2"
@ -1561,7 +1426,7 @@ version = "0.8.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
] ]
[[package]] [[package]]
@ -1682,7 +1547,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"libc", "libc",
"redox_syscall 0.2.16", "redox_syscall 0.2.16",
"windows-sys 0.48.0", "windows-sys 0.48.0",
@ -1887,7 +1752,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"libc", "libc",
"wasi 0.9.0+wasi-snapshot-preview1", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
@ -2224,7 +2089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [ dependencies = [
"digest 0.9.0", "digest 0.9.0",
"hmac 0.10.1", "hmac",
] ]
[[package]] [[package]]
@ -2233,29 +2098,10 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [ dependencies = [
"crypto-mac 0.10.1", "crypto-mac",
"digest 0.9.0", "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]] [[package]]
name = "hostname-validator" name = "hostname-validator"
version = "1.1.1" version = "1.1.1"
@ -2507,7 +2353,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
] ]
[[package]] [[package]]
@ -2755,7 +2601,6 @@ dependencies = [
"axum-csp", "axum-csp",
"axum-macros", "axum-macros",
"axum-server", "axum-server",
"axum-sessions",
"chrono", "chrono",
"compact_jwt", "compact_jwt",
"cron", "cron",
@ -3022,7 +2867,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"winapi", "winapi",
] ]
@ -3520,7 +3365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"cfg-if 1.0.0", "cfg-if",
"foreign-types", "foreign-types",
"libc", "libc",
"once_cell", "once_cell",
@ -3628,7 +3473,7 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"libc", "libc",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"smallvec", "smallvec",
@ -3840,7 +3685,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bitflags 1.3.2", "bitflags 1.3.2",
"cfg-if 1.0.0", "cfg-if",
"concurrent-queue", "concurrent-queue",
"libc", "libc",
"log", "log",
@ -4596,7 +4441,7 @@ version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"cpufeatures", "cpufeatures",
"digest 0.10.7", "digest 0.10.7",
] ]
@ -4626,7 +4471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [ dependencies = [
"block-buffer 0.9.0", "block-buffer 0.9.0",
"cfg-if 1.0.0", "cfg-if",
"cpufeatures", "cpufeatures",
"digest 0.9.0", "digest 0.9.0",
"opaque-debug 0.3.0", "opaque-debug 0.3.0",
@ -4638,7 +4483,7 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"cpufeatures", "cpufeatures",
"digest 0.10.7", "digest 0.10.7",
] ]
@ -4894,7 +4739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cfg-if 1.0.0", "cfg-if",
"fastrand", "fastrand",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix 0.37.22", "rustix 0.37.22",
@ -4951,7 +4796,7 @@ version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"once_cell", "once_cell",
] ]
@ -5252,7 +5097,7 @@ version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"log", "log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
@ -5549,7 +5394,7 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"serde", "serde",
"serde_json", "serde_json",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -5576,7 +5421,7 @@ version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",

View file

@ -125,6 +125,7 @@ codespell:
-L 'crate,unexpect,Pres,pres,ACI,aci,te,ue,unx,aNULL' \ -L 'crate,unexpect,Pres,pres,ACI,aci,te,ue,unx,aNULL' \
--skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache,./pykanidm/poetry.lock' \ --skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache,./pykanidm/poetry.lock' \
--skip='./book/book/*' \ --skip='./book/book/*' \
--skip='./book/src/images/*' \
--skip='./docs/*,./.git' \ --skip='./docs/*,./.git' \
--skip='./rlm_python/mods-available/eap' \ --skip='./rlm_python/mods-available/eap' \
--skip='./server/web_ui/static/external,./server/web_ui/pkg/external' \ --skip='./server/web_ui/static/external,./server/web_ui/pkg/external' \

View file

@ -216,7 +216,7 @@ title=WARNING text=Changing these settings MAY have serious consequences on the
<!-- deno-fmt-ignore-end --> <!-- deno-fmt-ignore-end -->
To disable PKCE for a resource server: To disable PKCE for a confidential resource server:
```bash ```bash
kanidm system oauth2 warning-insecure-client-disable-pkce <resource server name> 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> 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 ## Example Integrations
### Apache mod\_auth\_openidc ### Apache mod\_auth\_openidc
@ -239,7 +265,7 @@ with an appropriate include.
OIDCRedirectURI /protected/redirect_uri OIDCRedirectURI /protected/redirect_uri
OIDCCryptoPassphrase <random password here> OIDCCryptoPassphrase <random password here>
OIDCProviderMetadataURL https://kanidm.example.com/oauth2/openid/<resource server name>/.well-known/openid-configuration OIDCProviderMetadataURL https://kanidm.example.com/oauth2/openid/<resource server name>/.well-known/openid-configuration
OIDCScope "openid" OIDCScope "openid"
OIDCUserInfoTokenMethod authz_header OIDCUserInfoTokenMethod authz_header
OIDCClientID <resource server name> OIDCClientID <resource server name>
OIDCClientSecret <resource server password> OIDCClientSecret <resource server password>

View file

@ -37,6 +37,7 @@ use webauthn_rs_proto::{
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
}; };
mod oauth;
mod person; mod person;
mod scim; mod scim;
mod service_account; mod service_account;
@ -1795,228 +1796,6 @@ impl KanidmClient {
.await .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 // ==== recycle bin
pub async fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> { pub async fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/recycle_bin").await self.perform_get_request("/v1/recycle_bin").await

247
libs/client/src/oauth.rs Normal file
View 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
}
}

View file

@ -39,7 +39,7 @@ impl KanidmClient {
format!("/v1/sync_account/{}/_attr/sync_credential_portal", id).as_str(), format!("/v1/sync_account/{}/_attr/sync_credential_portal", id).as_str(),
) )
.await .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( pub async fn idm_sync_account_create(

View file

@ -906,7 +906,6 @@ impl fmt::Display for AuthMech {
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AuthIssueSession { pub enum AuthIssueSession {
Token, Token,
Cookie,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -1011,7 +1010,9 @@ pub enum AuthState {
// the result. // the result.
Success(String), Success(String),
// Everything is good, your cookie has been issued. // 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)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -19,7 +19,6 @@ axum-auth = "0.4.0"
axum-csp = { workspace = true } axum-csp = { workspace = true }
axum-macros = "0.3.7" axum-macros = "0.3.7"
axum-server = { version = "0.5.1", features = ["tls-openssl"] } axum-server = { version = "0.5.1", features = ["tls-openssl"] }
axum-sessions = "0.5.0"
chrono = { workspace = true } chrono = { workspace = true }
compact_jwt = { workspace = true } compact_jwt = { workspace = true }
cron = { workspace = true } cron = { workspace = true }

View file

@ -5,7 +5,6 @@ use axum::{
response::Response, response::Response,
TypedHeader, TypedHeader,
}; };
use axum_sessions::SessionHandle;
use http::HeaderValue; use http::HeaderValue;
use uuid::Uuid; use uuid::Uuid;
@ -41,20 +40,8 @@ pub async fn kopid_middleware<B>(
// generate the event ID // generate the event ID
let eventid = sketching::tracing_forest::id(); let eventid = sketching::tracing_forest::id();
// get the bearer token from the headers or the session // get the bearer token from the headers if present.
let uat = match auth { let uat = auth.map(|bearer| bearer.token().to_string());
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,
}
}
};
// insert the extension so we can pull it out later // insert the extension so we can pull it out later
request.extensions_mut().insert(KOpId { eventid, uat }); request.extensions_mut().insert(KOpId { eventid, uat });

View file

@ -19,8 +19,6 @@ use axum::routing::*;
use axum::Router; use axum::Router;
use axum_csp::{CspDirectiveType, CspValue}; use axum_csp::{CspDirectiveType, CspValue};
use axum_macros::FromRef; use axum_macros::FromRef;
use axum_sessions::extractors::WritableSession;
use axum_sessions::{async_session, SameSite, SessionLayer};
use compact_jwt::{Jws, JwsSigner, JwsUnverified}; use compact_jwt::{Jws, JwsSigner, JwsUnverified};
use generic::*; use generic::*;
use http::{HeaderMap, HeaderValue}; use http::{HeaderMap, HeaderValue};
@ -76,11 +74,7 @@ impl ServerState {
} }
} }
fn get_current_auth_session_id( fn get_current_auth_session_id(&self, headers: &HeaderMap) -> Option<Uuid> {
&self,
headers: &HeaderMap,
session: &WritableSession,
) -> Option<Uuid> {
// We see if there is a signed header copy first. // We see if there is a signed header copy first.
headers headers
.get("X-KANIDM-AUTH-SESSION-ID") .get("X-KANIDM-AUTH-SESSION-ID")
@ -89,8 +83,6 @@ impl ServerState {
hv.to_str().ok() hv.to_str().ok()
}) })
.and_then(|s| Some(self.reinflate_uuid_from_bytes(s)).unwrap_or(None)) .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( pub async fn create_https_server(
config: Configuration, config: Configuration,
cookie_key: [u8; 64],
jws_signer: JwsSigner, jws_signer: JwsSigner,
status_ref: &'static StatusActor, status_ref: &'static StatusActor,
qe_w_ref: &'static QueryServerWriteV1, qe_w_ref: &'static QueryServerWriteV1,
@ -185,15 +176,6 @@ pub async fn create_https_server(
vec![CspValue::SelfSite, CspValue::SchemeData], 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 trust_x_forward_for = config.trust_x_forward_for;
let state = ServerState { let state = ServerState {
@ -209,13 +191,18 @@ pub async fn create_https_server(
let static_routes = match config.role { let static_routes = match config.role {
ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => { 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() Router::new()
// direct users to the login page // direct users to the base app page. If a login is required,
.route("/", get(|| async { Redirect::temporary("/ui/login") })) // then views will take care of redirection. We shouldn't redir
.route("/ui/", get(crate::https::ui::ui_handler)) // to login because that force clears previous sessions!
// matches /ui/* but adds a path var `key` if you really wanted to capture it later. .route("/", get(|| async { Redirect::temporary("/ui") }))
.route("/ui/*key", get(crate::https::ui::ui_handler))
.route("/manifest.webmanifest", get(manifest::manifest)) .route("/manifest.webmanifest", get(manifest::manifest))
.nest("/ui", spa_router)
.layer(middleware::compression::new()) // TODO: this needs to be configured properly .layer(middleware::compression::new()) // TODO: this needs to be configured properly
} }
ServerRole::WriteReplicaNoUI => Router::new(), ServerRole::WriteReplicaNoUI => Router::new(),
@ -250,7 +237,6 @@ pub async fn create_https_server(
middleware::csp_headers::cspheaders_layer, middleware::csp_headers::cspheaders_layer,
)) ))
.layer(from_fn(middleware::version_middleware)) .layer(from_fn(middleware::version_middleware))
.layer(session_layer)
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
// This must be the LAST middleware. // This must be the LAST middleware.
// This is because the last middleware here is the first to be entered and the last // This is because the last middleware here is the first to be entered and the last

View file

@ -47,6 +47,19 @@ pub async fn oauth2_basic_post(
json_rest_event_post(state, classes, obj, kopid).await 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> { fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
filter_all!(f_and!([ filter_all!(f_and!([
f_eq("class", PartialValue::new_class("oauth2_resource_server")), 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 // This is called directly by the resource server, where we then issue
// the token to the caller. // the token to the caller.
// Get the authz header (if present). In the future depending on the // Get the authz header (if present). Not all exchange types require this.
// type of exchanges we support, this could become an Option type. let client_authz = headers
let client_authz = match headers
.get("authorization") .get("authorization")
.and_then(|hv| hv.to_str().ok()) .and_then(|hv| hv.to_str().ok())
.and_then(|h| h.split(' ').last()) .and_then(|h| h.split(' ').last())
.map(str::to_string) .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();
}
};
// Do we change the method/path we take here based on the type of requested // 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 // 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 let res = state
.qe_w_ref .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; .await;
match res { match res {
@ -612,8 +612,7 @@ pub async fn oauth2_openid_userinfo_get(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
) -> Response<Body> { ) -> Response<Body> {
// The token we want to inspect is in the authorisation header. // The token we want to inspect is in the authorisation header.
let client_token = match kopid.uat {
let client_authz = match kopid.uat {
Some(val) => val, Some(val) => val,
None => { None => {
error!("Bearer Authentication Not Provided"); error!("Bearer Authentication Not Provided");
@ -628,7 +627,7 @@ pub async fn oauth2_openid_userinfo_get(
let res = state let res = state
.qe_r_ref .qe_r_ref
.handle_oauth2_openid_userinfo(client_id, client_authz, kopid.eventid) .handle_oauth2_openid_userinfo(client_id, client_token, kopid.eventid)
.await; .await;
match res { match res {

View file

@ -8,7 +8,6 @@ use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post, put}; use axum::routing::{delete, get, post, put};
use axum::{Extension, Json, Router}; use axum::{Extension, Json, Router};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use axum_sessions::extractors::{ReadableSession, WritableSession};
use compact_jwt::Jws; use compact_jwt::Jws;
use http::{HeaderMap, HeaderValue, StatusCode}; use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::Body; use hyper::Body;
@ -104,26 +103,18 @@ pub async fn whoami(
pub async fn whoami_uat( pub async fn whoami_uat(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
session: ReadableSession,
) -> impl IntoResponse { ) -> impl IntoResponse {
let uat = match kopid.uat { let res = state
Some(val) => Some(val), .qe_r_ref
None => session.get("bearer"), .handle_whoami_uat(kopid.uat, kopid.eventid)
}; .await;
let res = state.qe_r_ref.handle_whoami_uat(uat, kopid.eventid).await;
to_axum_response(res) to_axum_response(res)
} }
pub async fn logout( pub async fn logout(
State(state): State<ServerState>, State(state): State<ServerState>,
mut msession: WritableSession,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse { ) -> 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; let res = state.qe_w_ref.handle_logout(kopid.uat, kopid.eventid).await;
to_axum_response(res) to_axum_response(res)
@ -1222,15 +1213,10 @@ pub async fn recycle_bin_revive_id_post(
pub async fn applinks_get( pub async fn applinks_get(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
session: ReadableSession,
) -> impl IntoResponse { ) -> impl IntoResponse {
let uat = match kopid.uat {
Some(val) => Some(val),
None => session.get("bearer"),
};
let res = state let res = state
.qe_r_ref .qe_r_ref
.handle_list_applinks(uat, kopid.eventid) .handle_list_applinks(kopid.uat, kopid.eventid)
.await; .await;
to_axum_response(res) to_axum_response(res)
} }
@ -1244,7 +1230,6 @@ pub async fn reauth(
State(state): State<ServerState>, State(state): State<ServerState>,
TrustedClientIp(ip_addr): TrustedClientIp, TrustedClientIp(ip_addr): TrustedClientIp,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
session: WritableSession,
Json(obj): Json<AuthIssueSession>, Json(obj): Json<AuthIssueSession>,
) -> impl IntoResponse { ) -> impl IntoResponse {
// This may change in the future ... // This may change in the future ...
@ -1253,13 +1238,12 @@ pub async fn reauth(
.handle_reauth(kopid.uat, obj, kopid.eventid, ip_addr) .handle_reauth(kopid.uat, obj, kopid.eventid, ip_addr)
.await; .await;
debug!("REAuth result: {:?}", inter); debug!("REAuth result: {:?}", inter);
auth_session_state_management(state, inter, session) auth_session_state_management(state, inter)
} }
pub async fn auth( pub async fn auth(
State(state): State<ServerState>, State(state): State<ServerState>,
TrustedClientIp(ip_addr): TrustedClientIp, TrustedClientIp(ip_addr): TrustedClientIp,
session: WritableSession,
headers: HeaderMap, headers: HeaderMap,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
Json(obj): Json<AuthRequest>, Json(obj): Json<AuthRequest>,
@ -1268,7 +1252,7 @@ pub async fn auth(
// Do anything here first that's needed like getting the session details // Do anything here first that's needed like getting the session details
// out of the req cookie. // 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); debug!("Session ID: {:?}", maybe_sessionid);
// We probably need to know if we allocate the cookie, that this is a // 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 // new session, and in that case, anything *except* authrequest init is
@ -1280,14 +1264,13 @@ pub async fn auth(
debug!("Auth result: {:?}", inter); debug!("Auth result: {:?}", inter);
auth_session_state_management(state, inter, session) auth_session_state_management(state, inter)
} }
#[instrument(skip(state))] #[instrument(skip(state))]
fn auth_session_state_management( fn auth_session_state_management(
state: ServerState, state: ServerState,
inter: Result<AuthResult, OperationError>, inter: Result<AuthResult, OperationError>,
mut msession: WritableSession,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut auth_session_id_tok = None; let mut auth_session_id_tok = None;
@ -1300,77 +1283,44 @@ fn auth_session_state_management(
match auth_state { match auth_state {
AuthState::Choose(allowed) => { AuthState::Choose(allowed) => {
debug!("🧩 -> AuthState::Choose"); // TODO: this should be ... less work debug!("🧩 -> AuthState::Choose"); // TODO: this should be ... less work
// Ensure the auth-session-id is set
// Ensure the auth-session-id is set let kref = &state.jws_signer;
msession.remove("auth-session-id"); let jws = Jws::new(SessionId { sessionid });
msession // Get the header token ready.
.insert("auth-session-id", sessionid) jws.sign(kref)
.map(|jwss| {
auth_session_id_tok = Some(jwss.to_string());
})
.map_err(|e| { .map_err(|e| {
error!(?e); error!(?e);
OperationError::InvalidSessionState 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)) .map(|_| ProtoAuthState::Choose(allowed))
} }
AuthState::Continue(allowed) => { AuthState::Continue(allowed) => {
debug!("🧩 -> AuthState::Continue"); debug!("🧩 -> AuthState::Continue");
let kref = &state.jws_signer;
// Ensure the auth-session-id is set // Get the header token ready.
msession.remove("auth-session-id"); let jws = Jws::new(SessionId { sessionid });
trace!(?sessionid, "🔥 🔥 "); jws.sign(kref)
msession .map(|jwss| {
.insert("auth-session-id", sessionid) auth_session_id_tok = Some(jwss.to_string());
})
.map_err(|e| { .map_err(|e| {
error!(?e); error!(?e);
OperationError::InvalidSessionState 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)) .map(|_| ProtoAuthState::Continue(allowed))
} }
AuthState::Success(token, issue) => { AuthState::Success(token, issue) => {
debug!("🧩 -> AuthState::Success"); debug!("🧩 -> AuthState::Success");
// Remove the auth-session-id
msession.remove("auth-session-id");
// Create a session cookie?
msession.remove("bearer");
match issue { match issue {
AuthIssueSession::Cookie => msession
.insert("bearer", token)
.map_err(|_| OperationError::InvalidSessionState)
.map(|_| ProtoAuthState::SuccessCookie),
AuthIssueSession::Token => Ok(ProtoAuthState::Success(token)), AuthIssueSession::Token => Ok(ProtoAuthState::Success(token)),
} }
} }
AuthState::Denied(reason) => { AuthState::Denied(reason) => {
debug!("🧩 -> AuthState::Denied"); debug!("🧩 -> AuthState::Denied");
// Remove the auth-session-id
msession.remove("auth-session-id");
Ok(ProtoAuthState::Denied(reason)) Ok(ProtoAuthState::Denied(reason))
} }
} }
@ -1398,13 +1348,11 @@ fn auth_session_state_management(
pub async fn auth_valid( pub async fn auth_valid(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
session: ReadableSession,
) -> impl IntoResponse { ) -> impl IntoResponse {
let uat = match kopid.uat { let res = state
Some(val) => Some(val), .qe_r_ref
None => session.get("bearer"), .handle_auth_valid(kopid.uat, kopid.eventid)
}; .await;
let res = state.qe_r_ref.handle_auth_valid(uat, kopid.eventid).await;
to_axum_response(res) to_axum_response(res)
} }
@ -1412,6 +1360,11 @@ pub async fn auth_valid(
pub fn router(state: ServerState) -> Router<ServerState> { pub fn router(state: ServerState) -> Router<ServerState> {
Router::new() Router::new()
.route("/v1/oauth2", get(super::oauth2::oauth2_get)) .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( .route(
"/v1/oauth2/:rs_name", "/v1/oauth2/:rs_name",
get(super::oauth2::oauth2_id_get) get(super::oauth2::oauth2_id_get)
@ -1422,7 +1375,6 @@ pub fn router(state: ServerState) -> Router<ServerState> {
"/v1/oauth2/:rs_name/_basic_secret", "/v1/oauth2/:rs_name/_basic_secret",
get(super::oauth2::oauth2_id_get_basic_secret), get(super::oauth2::oauth2_id_get_basic_secret),
) )
.route("/v1/oauth2/_basic", post(super::oauth2::oauth2_basic_post))
.route( .route(
"/v1/oauth2/:rs_name/_scopemap/:group", "/v1/oauth2/:rs_name/_scopemap/:group",
post(super::oauth2::oauth2_id_scopemap_post) post(super::oauth2::oauth2_id_scopemap_post)

View file

@ -762,8 +762,6 @@ pub async fn create_server_core(
} }
}; };
let cookie_key: [u8; 64] = idms.get_cookie_key();
// Any pre-start tasks here. // Any pre-start tasks here.
match &config.integration_test_config { match &config.integration_test_config {
Some(itc) => { Some(itc) => {
@ -913,7 +911,6 @@ pub async fn create_server_core(
} else { } else {
let h: tokio::task::JoinHandle<()> = match https::create_https_server( let h: tokio::task::JoinHandle<()> = match https::create_https_server(
config.clone(), config.clone(),
cookie_key,
jws_signer, jws_signer,
status_ref, status_ref,
server_write_ref, server_write_ref,

View file

@ -1536,7 +1536,8 @@ lazy_static! {
("acp_create_class", Value::new_iutf8("object")), ("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")),
("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"))
); );
} }

View file

@ -1867,7 +1867,6 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#"
"description", "description",
"oauth2_rs_scope_map", "oauth2_rs_scope_map",
"oauth2_rs_sup_scope_map", "oauth2_rs_sup_scope_map",
"oauth2_allow_insecure_client_disable_pkce",
"rs256_private_key_der", "rs256_private_key_der",
"oauth2_jwt_legacy_crypto_enable", "oauth2_jwt_legacy_crypto_enable",
"oauth2_prefer_short_username", "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#" lazy_static! {
{ pub static ref E_SCHEMA_CLASS_OAUTH2_RS_BASIC: EntryInitNew = entry_init!(
"attrs": { ("class", CLASS_OBJECT.clone()),
"class": [ ("class", CLASS_SYSTEM.clone()),
"object", ("class", CLASS_CLASSTYPE.clone()),
"system", (
"classtype" "description",
], Value::new_utf8s(
"description": [ "The class representing a configured Oauth2 Resource Server authenticated with http basic authentication"),
"The class representing a configured Oauth2 Resource Server authenticated with http basic" ),
], ("classname", Value::new_iutf8("oauth2_resource_server_basic")),
"classname": [ ("systemmay", Value::new_iutf8("oauth2_allow_insecure_client_disable_pkce")),
"oauth2_resource_server_basic" ("systemmust", Value::new_iutf8("oauth2_rs_basic_secret")),
], ("systemexcludes", Value::new_iutf8("oauth2_resource_server_public")),
"systemmay": [], ("uuid", Value::Uuid(UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC))
"systemmust": [ );
"oauth2_rs_basic_secret"
], pub static ref E_SCHEMA_CLASS_OAUTH2_RS_PUBLIC: EntryInitNew = entry_init!(
"uuid": [ ("class", CLASS_OBJECT.clone()),
"00000000-0000-0000-0000-ffff00000086" ("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))
);
}

View file

@ -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 = pub const UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000136"); 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 // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -24,6 +24,8 @@ lazy_static! {
PartialValue::new_class("oauth2_resource_server"); PartialValue::new_class("oauth2_resource_server");
pub static ref PVCLASS_OAUTH2_BASIC: PartialValue = pub static ref PVCLASS_OAUTH2_BASIC: PartialValue =
PartialValue::new_class("oauth2_resource_server_basic"); 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_PERSON: PartialValue = PartialValue::new_class("person");
pub static ref PVCLASS_POSIXACCOUNT: PartialValue = PartialValue::new_class("posixaccount"); pub static ref PVCLASS_POSIXACCOUNT: PartialValue = PartialValue::new_class("posixaccount");
pub static ref PVCLASS_POSIXGROUP: PartialValue = PartialValue::new_class("posixgroup"); 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_ACCOUNT: Value = Value::new_class("account");
pub static ref CLASS_ATTRIBUTETYPE: Value = Value::new_class("attributetype"); pub static ref CLASS_ATTRIBUTETYPE: Value = Value::new_class("attributetype");
pub static ref CLASS_CLASS: Value = Value::new_class("class"); 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_DOMAIN_INFO: Value = Value::new_class("domain_info");
pub static ref CLASS_DYNGROUP: Value = Value::new_class("dyngroup"); pub static ref CLASS_DYNGROUP: Value = Value::new_class("dyngroup");
pub static ref CLASS_GROUP: Value = Value::new_class("group"); pub static ref CLASS_GROUP: Value = Value::new_class("group");

View file

@ -187,6 +187,29 @@ pub struct AuthorisePermitSuccess {
pub code: String, 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)] #[derive(Clone)]
pub struct Oauth2RS { pub struct Oauth2RS {
name: String, name: String,
@ -196,15 +219,10 @@ pub struct Oauth2RS {
origin_https: bool, origin_https: bool,
scope_maps: BTreeMap<Uuid, BTreeSet<String>>, scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
sup_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. // Our internal exchange encryption material for this rs.
token_fernet: Fernet, token_fernet: Fernet,
jws_signer: JwsSigner, jws_signer: JwsSigner,
// jws_validator: JwsValidator, // 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. // For oidc we also need our issuer url.
iss: Url, iss: Url,
// For discovery we need to build and keep a number of values. // For discovery we need to build and keep a number of values.
@ -214,6 +232,7 @@ pub struct Oauth2RS {
jwks_uri: Url, jwks_uri: Url,
scopes_supported: BTreeSet<String>, scopes_supported: BTreeSet<String>,
prefer_short_username: bool, prefer_short_username: bool,
type_: OauthRSType,
} }
impl std::fmt::Debug for Oauth2RS { impl std::fmt::Debug for Oauth2RS {
@ -222,6 +241,7 @@ impl std::fmt::Debug for Oauth2RS {
.field("name", &self.name) .field("name", &self.name)
.field("displayname", &self.displayname) .field("displayname", &self.displayname)
.field("uuid", &self.uuid) .field("uuid", &self.uuid)
.field("type", &self.type_)
.field("origin", &self.origin) .field("origin", &self.origin)
.field("scope_maps", &self.scope_maps) .field("scope_maps", &self.scope_maps)
.field("sup_scope_maps", &self.sup_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) { if !ent.attribute_equality("class", &PVCLASS_OAUTH2_RS) {
admin_error!("Missing class oauth2_resource_server"); admin_error!("Missing class oauth2_resource_server");
// Check we have oauth2_resource_server class // Check we have oauth2_resource_server class
Err(OperationError::InvalidEntryState) return 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)?;
let landing_valid = ent let type_ = if ent.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) {
.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 authz_secret = ent let authz_secret = ent
.get_ava_single_secret("oauth2_rs_basic_secret") .get_ava_single_secret("oauth2_rs_basic_secret")
.map(str::to_string) .map(str::to_string)
.ok_or(OperationError::InvalidValueState)?; .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 let enable_pkce = ent
.get_ava_single_bool("oauth2_allow_insecure_client_disable_pkce") .get_ava_single_bool("oauth2_allow_insecure_client_disable_pkce")
.map(|e| !e) .map(|e| !e)
.unwrap_or(true); .unwrap_or(true);
let prefer_short_username = ent OauthRSType::Basic {
.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,
authz_secret, authz_secret,
token_fernet,
jws_signer,
// jws_validator,
enable_pkce, enable_pkce,
iss, }
authorization_endpoint, } else if ent.attribute_equality("class", &PVCLASS_OAUTH2_PUBLIC) {
token_endpoint, OauthRSType::Public
userinfo_endpoint,
jwks_uri,
scopes_supported,
prefer_short_username,
};
Ok((client_id, rscfg))
} else { } 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(); .collect();
@ -478,10 +494,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})?; })?;
// check the secret. // check the secret.
if o2rs.authz_secret != secret { match &o2rs.type_ {
security_info!("Invalid oauth2 client_id secret"); OauthRSType::Basic { authz_secret, .. } => {
return Err(Oauth2Error::AuthenticationRequired); 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 ... // We are authenticated! Yay! Now we can actually check things ...
// Can we deserialise the token? // Can we deserialise the token?
@ -550,14 +573,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
token_req: &AccessTokenRequest, token_req: &AccessTokenRequest,
ct: Duration, ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> { ) -> 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 { 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 { } else {
match (&token_req.client_id, &token_req.client_secret) { 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!( 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); return Err(Oauth2Error::AuthenticationRequired);
} }
@ -579,10 +605,27 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
}; };
// check the secret. // check the secret.
if o2rs.authz_secret != secret { match &o2rs.type_ {
security_info!("Invalid oauth2 client_id secret"); OauthRSType::Basic { authz_secret, .. } => {
return Err(Oauth2Error::AuthenticationRequired); 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 ... // 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! // 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 // 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 // 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); return Err(Oauth2Error::InvalidRequest);
} }
} else if o2rs.enable_pkce { } else if require_pkce {
security_info!( security_info!(
"PKCE code verification failed - no code challenge present in PKCE enforced mode" "PKCE code verification failed - no code challenge present in PKCE enforced mode"
); );
@ -1126,10 +1174,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})?; })?;
// check the secret. // check the secret.
if o2rs.authz_secret != secret { match &o2rs.type_ {
security_info!("Invalid oauth2 client_id secret"); OauthRSType::Basic { authz_secret, .. } => {
return Err(OperationError::InvalidSessionState); 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 o2rs.token_fernet
.decrypt(token) .decrypt(token)
@ -1207,8 +1261,13 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
return Err(Oauth2Error::InvalidOrigin); 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 { 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!"); security_info!(?o2rs.name, "Insecure rs configuration - pkce is not enforced, but rs is requesting it!");
} }
// CodeChallengeMethod must be S256 // CodeChallengeMethod must be S256
@ -1217,7 +1276,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
return Err(Oauth2Error::InvalidRequest); return Err(Oauth2Error::InvalidRequest);
} }
Some(pkce_request.code_challenge.clone()) 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."); security_error!(?o2rs.name, "No PKCE code challenge was provided with client in enforced PKCE mode.");
return Err(Oauth2Error::InvalidRequest); return Err(Oauth2Error::InvalidRequest);
} else { } else {
@ -1498,10 +1557,17 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
})?; })?;
// check the secret. // check the secret.
if o2rs.authz_secret != secret { match &o2rs.type_ {
security_info!("Invalid oauth2 client_id secret"); OauthRSType::Basic { authz_secret, .. } => {
return Err(Oauth2Error::AuthenticationRequired); 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 ... // We are authenticated! Yay! Now we can actually check things ...
let token: Oauth2TokenType = o2rs let token: Oauth2TokenType = o2rs
@ -1590,7 +1656,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
pub fn oauth2_openid_userinfo( pub fn oauth2_openid_userinfo(
&mut self, &mut self,
client_id: &str, client_id: &str,
client_authz: &str, token_str: &str,
ct: Duration, ct: Duration,
) -> Result<OidcToken, Oauth2Error> { ) -> Result<OidcToken, Oauth2Error> {
// DANGER: Why do we have to do this? During the use of qs for internal search // 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 let token: Oauth2TokenType = o2rs
.token_fernet .token_fernet
.decrypt(client_authz) .decrypt(token_str)
.map_err(|_| { .map_err(|_| {
admin_error!("Failed to decrypt token introspection request"); admin_error!("Failed to decrypt token introspection request");
Oauth2Error::InvalidRequest Oauth2Error::InvalidRequest
@ -1954,7 +2020,7 @@ mod tests {
} }
// setup an oauth2 instance. // setup an oauth2 instance.
async fn setup_oauth2_resource_server( async fn setup_oauth2_resource_server_basic(
idms: &IdmServer, idms: &IdmServer,
ct: Duration, ct: Duration,
enable_pkce: bool, enable_pkce: bool,
@ -2077,6 +2143,105 @@ mod tests {
(secret, uat, ident, uuid) (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) { async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
let mut idms_prox_write = idms.proxy_write(ct).await; let mut idms_prox_write = idms.proxy_write(ct).await;
let account = idms_prox_write let account = idms_prox_write
@ -2102,7 +2267,7 @@ mod tests {
) { ) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
@ -2162,6 +2327,72 @@ mod tests {
assert!(idms_prox_write.commit().is_ok()); 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] #[idm_test]
async fn test_idm_oauth2_invalid_authorisation_requests( async fn test_idm_oauth2_invalid_authorisation_requests(
idms: &IdmServer, idms: &IdmServer,
@ -2170,7 +2401,7 @@ mod tests {
// Test invalid oauth2 authorisation states/requests. // Test invalid oauth2 authorisation states/requests.
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) = 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 (anon_uat, anon_ident) = setup_idm_admin(idms, ct).await;
let (idm_admin_uat, idm_admin_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. // Test invalid oauth2 authorisation states/requests.
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) = 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 (uat2, ident2) = {
let mut idms_prox_write = idms.proxy_write(ct).await; 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 ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, mut uat, ident, _) = 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 // ⚠️ 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 // 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 ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}")));
@ -2691,7 +2922,7 @@ mod tests {
// First, setup to get a token. // First, setup to get a token.
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}")));
@ -2837,7 +3068,7 @@ mod tests {
// First, setup to get a token. // First, setup to get a token.
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); 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 ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) = 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 (uat2, ident2) = {
let mut idms_prox_write = idms.proxy_write(ct).await; 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 ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, _ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
@ -3162,7 +3393,7 @@ mod tests {
) { ) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); 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 // but change the preferred_username setting on the RS
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); 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 // but change the preferred_username setting on the RS
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); 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) { async fn test_idm_oauth2_insecure_pkce(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
@ -3511,7 +3742,7 @@ mod tests {
) { ) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
// The public key url should offer an rs key // The public key url should offer an rs key
// discovery should offer RS256 // discovery should offer RS256
@ -3611,7 +3842,7 @@ mod tests {
) { ) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
@ -3811,7 +4042,7 @@ mod tests {
) { ) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, o2rs_uuid) = 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 there are no consent maps yet.
assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none()); assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none());
@ -3899,7 +4130,7 @@ mod tests {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
// Enable pkce is set to FALSE // Enable pkce is set to FALSE
let (secret, uat, ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
@ -3976,7 +4207,7 @@ mod tests {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
// Enable pkce is set to FALSE // Enable pkce is set to FALSE
let (secret, uat, ident, _) = 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; let idms_prox_read = idms.proxy_read().await;
@ -4064,7 +4295,7 @@ mod tests {
) -> (AccessTokenResponse, Option<String>) { ) -> (AccessTokenResponse, Option<String>) {
// First, setup to get a token. // First, setup to get a token.
let (secret, uat, ident, _) = 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 = let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}"))); Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}")));

View file

@ -46,12 +46,14 @@ impl Plugin for JwsKeygen {
impl JwsKeygen { impl JwsKeygen {
fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> Result<(), OperationError> { fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> Result<(), OperationError> {
cand.iter_mut().try_for_each(|e| { cand.iter_mut().try_for_each(|e| {
if e.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) { if e.attribute_equality("class", &PVCLASS_OAUTH2_BASIC) &&
if !e.attribute_pres("oauth2_rs_basic_secret") { !e.attribute_pres("oauth2_rs_basic_secret") {
security_info!("regenerating oauth2 basic secret"); security_info!("regenerating oauth2 basic secret");
let v = Value::SecretValue(password_from_random()); let v = Value::SecretValue(password_from_random());
e.add_ava("oauth2_rs_basic_secret", v); e.add_ava("oauth2_rs_basic_secret", v);
} }
if e.attribute_equality("class", &PVCLASS_OAUTH2_RS) {
if !e.attribute_pres("oauth2_rs_token_key") { if !e.attribute_pres("oauth2_rs_token_key") {
security_info!("regenerating oauth2 token key"); security_info!("regenerating oauth2 token key");
let k = fernet::Fernet::generate_key(); let k = fernet::Fernet::generate_key();

View file

@ -731,7 +731,7 @@ mod tests {
let ea: Entry<EntryInit, EntryNew> = entry_init!( let ea: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("object")), ("class", Value::new_class("object")),
("class", Value::new_class("oauth2_resource_server")), ("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")), ("oauth2_rs_name", Value::new_iname("test_resource_server")),
("displayname", Value::new_utf8s("test_resource_server")), ("displayname", Value::new_utf8s("test_resource_server")),
( (
@ -812,7 +812,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
("class", Value::new_class("object")), ("class", Value::new_class("object")),
("class", Value::new_class("oauth2_resource_server")), ("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)), ("uuid", Value::Uuid(rs_uuid)),
("oauth2_rs_name", Value::new_iname("test_resource_server")), ("oauth2_rs_name", Value::new_iname("test_resource_server")),
("displayname", Value::new_utf8s("test_resource_server")), ("displayname", Value::new_utf8s("test_resource_server")),

View file

@ -504,9 +504,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_CLASS_POSIXACCOUNT, JSON_SCHEMA_CLASS_POSIXACCOUNT,
JSON_SCHEMA_CLASS_POSIXGROUP, JSON_SCHEMA_CLASS_POSIXGROUP,
JSON_SCHEMA_CLASS_SYSTEM_CONFIG, JSON_SCHEMA_CLASS_SYSTEM_CONFIG,
JSON_SCHEMA_CLASS_OAUTH2_RS,
JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC,
JSON_SCHEMA_CLASS_SYNC_ACCOUNT, JSON_SCHEMA_CLASS_SYNC_ACCOUNT,
JSON_SCHEMA_CLASS_OAUTH2_RS,
JSON_SCHEMA_ATTR_PRIVATE_COOKIE_KEY, JSON_SCHEMA_ATTR_PRIVATE_COOKIE_KEY,
]; ];
@ -515,13 +514,28 @@ impl<'a> QueryServerWriteTransaction<'a> {
// Each item individually logs it's result // Each item individually logs it's result
.try_for_each(|e_str| self.internal_migrate_or_create_str(e_str)); .try_for_each(|e_str| self.internal_migrate_or_create_str(e_str));
if r.is_ok() { if r.is_err() {
debug!("initialise_schema_idm -> Ok!"); error!(res = ?r, "initialise_schema_idm -> Error");
} else { }
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"); error!(res = ?r, "initialise_schema_idm -> Error");
} }
debug_assert!(r.is_ok()); debug_assert!(r.is_ok());
debug!("initialise_schema_idm -> Ok!");
r r
} }

View file

@ -6,7 +6,7 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) {
let addr = rsclient.get_url(); let addr = rsclient.get_url();
// here we test the /ui/ endpoint which should have the headers // 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, Ok(value) => value,
Err(error) => { Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error); panic!("Failed to query {:?} : {:#?}", addr, error);

View file

@ -384,6 +384,257 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
.expect("Failed to update oauth2 scopes"); .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] #[kanidmd_testkit::test]
async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) { async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) {
let res = rsclient let res = rsclient

View file

@ -17,11 +17,11 @@ async fn test_routes(rsclient: KanidmClient) {
"method": "GET" "method": "GET"
}, },
{ {
"path": "/ui/", "path": "/ui",
"method": "GET" "method": "GET"
}, },
{ {
"path": "/ui/*", "path": "/ui/login",
"method": "GET" "method": "GET"
}, },
{ {

View file

@ -8,7 +8,23 @@ heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; } 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; let cachedUint8Memory0 = null;
@ -19,6 +35,22 @@ function getUint8Memory0() {
return cachedUint8Memory0; 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 cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
@ -85,38 +117,6 @@ function getInt32Memory0() {
return cachedInt32Memory0; 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; let cachedFloat64Memory0 = null;
function getFloat64Memory0() { function getFloat64Memory0() {
@ -234,7 +234,7 @@ function addBorrowedObject(obj) {
} }
function __wbg_adapter_48(arg0, arg1, arg2) { function __wbg_adapter_48(arg0, arg1, arg2) {
try { 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 { } finally {
heap[stack_pointer++] = undefined; heap[stack_pointer++] = undefined;
} }
@ -242,14 +242,14 @@ function __wbg_adapter_48(arg0, arg1, arg2) {
function __wbg_adapter_51(arg0, arg1, arg2) { function __wbg_adapter_51(arg0, arg1, arg2) {
try { 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 { } finally {
heap[stack_pointer++] = undefined; heap[stack_pointer++] = undefined;
} }
} }
function __wbg_adapter_54(arg0, arg1, arg2) { 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() { function __wbg_get_imports() {
const imports = {}; const imports = {};
imports.wbg = {}; 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) { imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1); const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined; const ret = typeof(obj) === 'string' ? obj : undefined;
@ -338,40 +354,16 @@ function __wbg_get_imports() {
getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1; 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) { imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0); const ret = getObject(arg0);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_is_undefined = function(arg0) { imports.wbg.__wbg_modalhidebyid_14daee5d362376c0 = function(arg0, arg1) {
const ret = getObject(arg0) === undefined; modal_hide_by_id(getStringFromWasm0(arg0, arg1));
return ret;
}; };
imports.wbg.__wbindgen_in = function(arg0, arg1) { imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = getObject(arg0) in getObject(arg1); const ret = new Error(getStringFromWasm0(arg0, arg1));
return ret; return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_boolean_get = function(arg0) { imports.wbg.__wbindgen_boolean_get = function(arg0) {
const v = getObject(arg0); const v = getObject(arg0);
@ -382,6 +374,14 @@ function __wbg_get_imports() {
const ret = typeof(getObject(arg0)) === 'bigint'; const ret = typeof(getObject(arg0)) === 'bigint';
return ret; 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) { imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
const obj = getObject(arg1); const obj = getObject(arg1);
const ret = typeof(obj) === 'number' ? obj : undefined; const ret = typeof(obj) === 'number' ? obj : undefined;
@ -393,22 +393,22 @@ function __wbg_get_imports() {
const ret = typeof(val) === 'object' && val !== null; const ret = typeof(val) === 'object' && val !== null;
return ret; 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) { imports.wbg.__wbindgen_is_string = function(arg0) {
const ret = typeof(getObject(arg0)) === 'string'; const ret = typeof(getObject(arg0)) === 'string';
return ret; return ret;
}; };
imports.wbg.__wbindgen_cb_drop = function(arg0) { imports.wbg.__wbindgen_is_undefined = function(arg0) {
const obj = takeObject(arg0).original; const ret = getObject(arg0) === undefined;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret; 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) { imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id; const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; 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) { imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) {
getObject(arg0)[takeObject(arg1)] = takeObject(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) { imports.wbg.__wbg_debug_783a3d4910bc24c7 = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4); wasm.__wbindgen_free(arg0, arg1 * 4);
@ -1118,16 +1125,16 @@ function __wbg_get_imports() {
const ret = wasm.memory; const ret = wasm.memory;
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper4655 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper2575 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1076, __wbg_adapter_48); const ret = makeMutClosure(arg0, arg1, 1196, __wbg_adapter_48);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper5440 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper3416 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1368, __wbg_adapter_51); const ret = makeMutClosure(arg0, arg1, 1503, __wbg_adapter_51);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper6559 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper4520 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1442, __wbg_adapter_54); const ret = makeMutClosure(arg0, arg1, 1577, __wbg_adapter_54);
return addHeapObject(ret); return addHeapObject(ret);
}; };

View file

@ -573,7 +573,7 @@ impl CredentialResetApp {
{ pw_warn } { pw_warn }
{ pw_html_inner } { 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>{ "Strong cryptographic authenticators with self contained multi-factor authentication." }</p>
<p>{ "No Passkeys Registered" }</p> <p>{ "No Passkeys Registered" }</p>
<PasskeyModalApp token={ token.clone() } cb={ cb.clone() } /> <PasskeyModalApp token={ token.clone() } cb={ cb } />
</> </>
} }
} else { } else {

View file

@ -69,6 +69,7 @@ pub async fn do_request(
opts.method(&method.to_string()); opts.method(&method.to_string());
opts.mode(RequestMode::SameOrigin); opts.mode(RequestMode::SameOrigin);
opts.credentials(web_sys::RequestCredentials::SameOrigin); opts.credentials(web_sys::RequestCredentials::SameOrigin);
if let Some(body) = body { if let Some(body) = body {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if method == RequestMethod::GET { if method == RequestMethod::GET {
@ -81,7 +82,21 @@ pub async fn do_request(
request request
.headers() .headers()
.set("content-type", "application/json") .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 window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; 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 status = resp.status();
let headers: Headers = resp.headers(); 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(); let kopid = headers.get("x-kanidm-opid").ok().flatten();
Ok((kopid, status, JsFuture::from(resp.json()?).await?, headers)) Ok((kopid, status, JsFuture::from(resp.json()?).await?, headers))

View file

@ -99,7 +99,7 @@ impl LoginApp {
let authreq = AuthRequest { let authreq = AuthRequest {
step: AuthStep::Init2 { step: AuthStep::Init2 {
username, username,
issue: AuthIssueSession::Cookie, issue: AuthIssueSession::Token,
}, },
}; };
let req_jsvalue = serde_json::to_string(&authreq) let req_jsvalue = serde_json::to_string(&authreq)
@ -127,7 +127,7 @@ impl LoginApp {
} }
async fn reauth_init() -> Result<LoginAppMsg, FetchError> { async fn reauth_init() -> Result<LoginAppMsg, FetchError> {
let issue = AuthIssueSession::Cookie; let issue = AuthIssueSession::Token;
let authreq_jsvalue = serde_json::to_string(&issue) let authreq_jsvalue = serde_json::to_string(&issue)
.map(|s| JsValue::from(&s)) .map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq"); .expect_throw("Failed to serialise authreq");
@ -588,7 +588,7 @@ impl Component for LoginApp {
fn create(ctx: &Context<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
console::debug!("create".to_string()); console::debug!("login::create".to_string());
let workflow = &ctx.props().workflow; let workflow = &ctx.props().workflow;
let state = match workflow { let state = match workflow {
@ -602,21 +602,6 @@ impl Component for LoginApp {
.or_else(|| models::get_login_remember_me().map(|user| (user, true))) .or_else(|| models::get_login_remember_me().map(|user| (user, true)))
.unwrap_or_default(); .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 { LoginState::InitLogin {
enable: true, enable: true,
remember_me, remember_me,
@ -958,22 +943,13 @@ impl Component for LoginApp {
self.state = LoginState::Denied(reason); self.state = LoginState::Denied(reason);
true true
} }
AuthState::Success(_bearer_token) => { AuthState::Success(bearer_token) => {
// Store the bearer here! // 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); models::set_bearer_token(bearer_token);
self.state = LoginState::Authenticated; self.state = LoginState::Authenticated;
true true
*/
self.state = LoginState::Error {
emsg: "Invalid Issued Session Type, expected cookie".to_string(),
kopid: None,
};
true
}
AuthState::SuccessCookie => {
self.state = LoginState::Authenticated;
true
} }
} }
} }

View file

@ -19,7 +19,7 @@ use crate::views::{ViewRoute, ViewsApp};
// router to decide on state. // router to decide on state.
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum Route { pub enum Route {
#[at("/")] #[at("/ui")]
Landing, Landing,
#[at("/ui/login")] #[at("/ui/login")]
@ -44,6 +44,8 @@ pub enum Route {
#[function_component(Landing)] #[function_component(Landing)]
fn landing() -> Html { fn landing() -> Html {
#[cfg(debug_assertions)]
console::debug!("manager::landing");
// Do this to allow use_navigator to work because lol. // Do this to allow use_navigator to work because lol.
yew_router::hooks::use_navigator() yew_router::hooks::use_navigator()
.expect_throw("Unable to access history") .expect_throw("Unable to access history")
@ -55,7 +57,7 @@ fn landing() -> Html {
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn switch(route: Route) -> Html { fn switch(route: Route) -> Html {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
console::debug!("manager::switch"); console::debug!(format!("manager::switch -> {:?}", route).as_str());
match route { match route {
#[allow(clippy::let_unit_value)] #[allow(clippy::let_unit_value)]
Route::Landing => html! { <Landing /> }, Route::Landing => html! { <Landing /> },

View file

@ -12,8 +12,36 @@ use yew_router::navigator::Navigator;
use crate::manager::Route; use crate::manager::Route;
use crate::views::ViewRoute; 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() { 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)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]

View file

@ -6,12 +6,12 @@ impl Oauth2Opt {
match self { match self {
Oauth2Opt::List(copt) => copt.debug, Oauth2Opt::List(copt) => copt.debug,
Oauth2Opt::Get(nopt) => nopt.copt.debug, Oauth2Opt::Get(nopt) => nopt.copt.debug,
Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::UpdateScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::UpdateScopeMap(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::DeleteScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::DeleteScopeMap(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::UpdateSupScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::UpdateSupScopeMap(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::DeleteSupScopeMap(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::DeleteSupScopeMap(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::ResetSecrets(cbopt) => cbopt.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::ShowBasicSecret(nopt) => nopt.copt.debug,
Oauth2Opt::Delete(nopt) => nopt.copt.debug, Oauth2Opt::Delete(nopt) => nopt.copt.debug,
Oauth2Opt::SetDisplayname(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::SetDisplayname(cbopt) => cbopt.nopt.copt.debug,
@ -23,6 +23,9 @@ impl Oauth2Opt {
Oauth2Opt::DisableLegacyCrypto(nopt) => nopt.copt.debug, Oauth2Opt::DisableLegacyCrypto(nopt) => nopt.copt.debug,
Oauth2Opt::PreferShortUsername(nopt) => nopt.copt.debug, Oauth2Opt::PreferShortUsername(nopt) => nopt.copt.debug,
Oauth2Opt::PreferSPNUsername(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), Err(e) => error!("Error -> {:?}", e),
} }
} }
Oauth2Opt::CreateBasic(cbopt) => { Oauth2Opt::CreateBasic {
let client = cbopt.nopt.copt.to_client(OpType::Read).await; name,
displayname,
origin,
copt,
} => {
let client = copt.to_client(OpType::Write).await;
match client match client
.idm_oauth2_rs_basic_create( .idm_oauth2_rs_basic_create(
cbopt.nopt.name.as_str(), name.as_str(),
cbopt.displayname.as_str(), displayname.as_str(),
cbopt.origin.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 .await
{ {

View file

@ -606,16 +606,6 @@ pub enum SelfOpt {
Whoami(CommonOpt), 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)] #[derive(Debug, Args)]
pub struct Oauth2SetDisplayname { pub struct Oauth2SetDisplayname {
#[clap(flatten)] #[clap(flatten)]
@ -662,8 +652,33 @@ pub enum Oauth2Opt {
// /// Set options for a selected oauth2 resource server // /// Set options for a selected oauth2 resource server
// Set(), // Set(),
#[clap(name = "create")] #[clap(name = "create")]
/// Create a new oauth2 resource server /// Create a new oauth2 confidential resource server that is protected by basic auth.
CreateBasic(Oauth2BasicCreateOpt), 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"])] #[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 /// Update or add a new mapping from a group to scopes that it provides to members
UpdateScopeMap(Oauth2CreateScopeMapOpt), UpdateScopeMap(Oauth2CreateScopeMapOpt),