diff --git a/Cargo.lock b/Cargo.lock
index 51d4ce7db..b35248c6e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "once_cell",
  "serde",
  "version_check",
@@ -232,9 +232,9 @@ dependencies = [
 
 [[package]]
 name = "async-compression"
-version = "0.4.22"
+version = "0.4.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
+checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07"
 dependencies = [
  "flate2",
  "futures-core",
@@ -621,9 +621,9 @@ checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
 
 [[package]]
 name = "bstr"
-version = "1.11.3"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
 dependencies = [
  "memchr",
  "regex-automata 0.4.9",
@@ -668,9 +668,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
 
 [[package]]
 name = "cc"
-version = "1.2.17"
+version = "1.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
+checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
 dependencies = [
  "shlex",
 ]
@@ -792,8 +792,7 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 [[package]]
 name = "compact_jwt"
 version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12bbab6445446e8d0b07468a01d0bfdae15879de5c440c5e47ae4ae0e18a1fba"
+source = "git+https://github.com/Firstyear/compact-jwt.git?rev=b3d2b5700cfe567d384c81df35d25537fbf7f110#b3d2b5700cfe567d384c81df35d25537fbf7f110"
 dependencies = [
  "base64 0.21.7",
  "base64urlsafedata",
@@ -1053,9 +1052,9 @@ dependencies = [
 
 [[package]]
 name = "darling"
-version = "0.20.10"
+version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
 dependencies = [
  "darling_core",
  "darling_macro",
@@ -1063,9 +1062,9 @@ dependencies = [
 
 [[package]]
 name = "darling_core"
-version = "0.20.10"
+version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
 dependencies = [
  "fnv",
  "ident_case",
@@ -1077,9 +1076,9 @@ dependencies = [
 
 [[package]]
 name = "darling_macro"
-version = "0.20.10"
+version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 dependencies = [
  "darling_core",
  "quote",
@@ -1088,15 +1087,15 @@ dependencies = [
 
 [[package]]
 name = "data-encoding"
-version = "2.8.0"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
 
 [[package]]
 name = "der"
-version = "0.7.9"
+version = "0.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
 dependencies = [
  "const-oid",
  "der_derive",
@@ -1132,9 +1131,9 @@ dependencies = [
 
 [[package]]
 name = "deranged"
-version = "0.4.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 dependencies = [
  "powerfmt",
  "serde",
@@ -1223,22 +1222,23 @@ dependencies = [
 
 [[package]]
 name = "dirs"
-version = "4.0.0"
+version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
 dependencies = [
  "dirs-sys",
 ]
 
 [[package]]
 name = "dirs-sys"
-version = "0.3.7"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
 dependencies = [
  "libc",
+ "option-ext",
  "redox_users",
- "winapi",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -1351,9 +1351,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "errno"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
 dependencies = [
  "libc",
  "windows-sys 0.59.0",
@@ -1361,9 +1361,9 @@ dependencies = [
 
 [[package]]
 name = "escargot"
-version = "0.5.13"
+version = "0.5.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05a3ac187a16b5382fef8c69fd1bad123c67b7cf3932240a2d43dcdd32cded88"
+checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211"
 dependencies = [
  "log",
  "once_cell",
@@ -1444,19 +1444,6 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
 
-[[package]]
-name = "fernet"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c66b725fe9483b9ee72ccaec072b15eb8ad95a3ae63a8c798d5748883b72fd33"
-dependencies = [
- "base64 0.22.1",
- "byteorder",
- "getrandom 0.2.15",
- "openssl",
- "zeroize",
-]
-
 [[package]]
 name = "file-id"
 version = "0.2.2"
@@ -1486,15 +1473,15 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
 
 [[package]]
 name = "flagset"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec"
+checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
 
 [[package]]
 name = "flate2"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -1573,7 +1560,7 @@ version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
 dependencies = [
- "rustix 1.0.3",
+ "rustix 1.0.5",
  "windows-sys 0.59.0",
 ]
 
@@ -1687,9 +1674,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -2277,7 +2264,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http 0.2.12",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
  "slab",
  "tokio",
  "tokio-util",
@@ -2286,9 +2273,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
+checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2296,7 +2283,7 @@ dependencies = [
  "futures-core",
  "futures-sink",
  "http 1.3.1",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
  "slab",
  "tokio",
  "tokio-util",
@@ -2503,7 +2490,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "h2 0.4.8",
+ "h2 0.4.9",
  "http 1.3.1",
  "http-body 1.0.1",
  "httparse",
@@ -2565,9 +2552,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.10"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -2575,6 +2562,7 @@ dependencies = [
  "http 1.3.1",
  "http-body 1.0.1",
  "hyper 1.6.0",
+ "libc",
  "pin-project-lite",
  "socket2",
  "tokio",
@@ -2584,9 +2572,9 @@ dependencies = [
 
 [[package]]
 name = "iana-time-zone"
-version = "0.1.62"
+version = "0.1.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
 dependencies = [
  "android_system_properties",
  "core-foundation-sys",
@@ -2801,9 +2789,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.8.0"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
 dependencies = [
  "equivalent",
  "hashbrown 0.15.2",
@@ -3277,7 +3265,6 @@ dependencies = [
  "concread",
  "dhat",
  "dyn-clone",
- "fernet",
  "futures",
  "hashbrown 0.15.2",
  "hex",
@@ -3473,9 +3460,9 @@ dependencies = [
 
 [[package]]
 name = "libm"
-version = "0.2.11"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72"
 
 [[package]]
 name = "libmimalloc-sys"
@@ -3547,9 +3534,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.9.3"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
 
 [[package]]
 name = "litemap"
@@ -3732,18 +3719,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.5"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
 dependencies = [
  "adler2",
 ]
 
 [[package]]
 name = "mintex"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bec4598fddb13cc7b528819e697852653252b760f1228b7642679bf2ff2cd07"
+checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536"
 
 [[package]]
 name = "mio"
@@ -4034,7 +4021,7 @@ checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
 dependencies = [
  "base64 0.13.1",
  "chrono",
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "http 0.2.12",
  "rand 0.8.5",
  "reqwest 0.11.27",
@@ -4054,7 +4041,7 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
 dependencies = [
  "base64 0.22.1",
  "chrono",
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "http 1.3.1",
  "rand 0.8.5",
  "reqwest 0.12.15",
@@ -4095,9 +4082,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.21.2"
+version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
 [[package]]
 name = "openssl"
@@ -4228,6 +4215,12 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "orca"
 version = "1.6.0-dev"
@@ -4372,7 +4365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
 dependencies = [
  "fixedbitset",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
  "serde",
 ]
 
@@ -4491,9 +4484,9 @@ dependencies = [
 
 [[package]]
 name = "prettyplease"
-version = "0.2.31"
+version = "0.2.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb"
+checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
 dependencies = [
  "proc-macro2",
  "syn 2.0.100",
@@ -4501,12 +4494,11 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-crate"
-version = "1.3.1"
+version = "3.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
 dependencies = [
- "once_cell",
- "toml_edit 0.19.15",
+ "toml_edit",
 ]
 
 [[package]]
@@ -4628,9 +4620,9 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.11.10"
+version = "0.11.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
+checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b"
 dependencies = [
  "bytes",
  "getrandom 0.3.2",
@@ -4722,7 +4714,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
 ]
 
 [[package]]
@@ -4742,22 +4734,22 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.10"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
+checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
 dependencies = [
  "bitflags 2.9.0",
 ]
 
 [[package]]
 name = "redox_users"
-version = "0.4.6"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
 dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "libredox",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
 ]
 
 [[package]]
@@ -4888,7 +4880,7 @@ dependencies = [
  "futures-channel",
  "futures-core",
  "futures-util",
- "h2 0.4.8",
+ "h2 0.4.9",
  "http 1.3.1",
  "http-body 1.0.1",
  "http-body-util",
@@ -4942,7 +4934,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
 dependencies = [
  "cc",
  "cfg-if",
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "libc",
  "untrusted",
  "windows-sys 0.52.0",
@@ -4980,9 +4972,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed"
-version = "8.6.0"
+version = "8.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f"
+checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d"
 dependencies = [
  "rust-embed-impl",
  "rust-embed-utils",
@@ -4991,9 +4983,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-impl"
-version = "8.6.0"
+version = "8.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae"
+checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5004,9 +4996,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-utils"
-version = "8.6.0"
+version = "8.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a"
+checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21"
 dependencies = [
  "sha2",
  "walkdir",
@@ -5054,22 +5046,22 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "1.0.3"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
+checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
 dependencies = [
  "bitflags 2.9.0",
  "errno",
  "libc",
- "linux-raw-sys 0.9.3",
+ "linux-raw-sys 0.9.4",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "rustls"
-version = "0.23.25"
+version = "0.23.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
+checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
 dependencies = [
  "once_cell",
  "ring",
@@ -5352,7 +5344,7 @@ dependencies = [
  "chrono",
  "hex",
  "indexmap 1.9.3",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
  "serde",
  "serde_derive",
  "serde_json",
@@ -5448,9 +5440,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.2"
+version = "1.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
 dependencies = [
  "libc",
 ]
@@ -5483,9 +5475,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
 dependencies = [
  "serde",
 ]
@@ -5513,9 +5505,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.5.8"
+version = "0.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -5686,7 +5678,7 @@ dependencies = [
  "fastrand",
  "getrandom 0.3.2",
  "once_cell",
- "rustix 1.0.3",
+ "rustix 1.0.5",
  "windows-sys 0.59.0",
 ]
 
@@ -5917,9 +5909,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.14"
+version = "0.7.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
 dependencies = [
  "bytes",
  "futures-core",
@@ -5937,7 +5929,7 @@ dependencies = [
  "serde",
  "serde_spanned",
  "toml_datetime",
- "toml_edit 0.22.24",
+ "toml_edit",
 ]
 
 [[package]]
@@ -5949,24 +5941,13 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "toml_edit"
-version = "0.19.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
-dependencies = [
- "indexmap 2.8.0",
- "toml_datetime",
- "winnow 0.5.40",
-]
-
 [[package]]
 name = "toml_edit"
 version = "0.22.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
 dependencies = [
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -5984,7 +5965,7 @@ dependencies = [
  "axum",
  "base64 0.22.1",
  "bytes",
- "h2 0.4.8",
+ "h2 0.4.9",
  "http 1.3.1",
  "http-body 1.0.1",
  "http-body-util",
@@ -6309,7 +6290,7 @@ version = "4.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23"
 dependencies = [
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
  "serde",
  "serde_json",
  "utoipa-gen",
@@ -6729,11 +6710,37 @@ dependencies = [
 
 [[package]]
 name = "windows-core"
-version = "0.52.0"
+version = "0.61.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
 dependencies = [
- "windows-targets 0.52.6",
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings 0.4.0",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -6749,7 +6756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
 dependencies = [
  "windows-result",
- "windows-strings",
+ "windows-strings 0.3.1",
  "windows-targets 0.53.0",
 ]
 
@@ -6771,6 +6778,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-strings"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -7025,15 +7041,6 @@ version = "0.53.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
 
-[[package]]
-name = "winnow"
-version = "0.5.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "winnow"
 version = "0.6.26"
diff --git a/Cargo.toml b/Cargo.toml
index 6d614450e..0b91a387b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -101,7 +101,7 @@ codegen-units = 256
 ## As Kanidm maintains a number of libraries, sometimes during development we need to override them
 ## with local or git versions. This patch table allows quick uncommenting to achieve that.
 
-# compact_jwt = { path = "../compact_jwt" }
+# compact_jwt = { path = "../compact-jwt" }
 # concread = { path = "../concread" }
 
 # idlset = { path = "../idlset" }
@@ -122,7 +122,10 @@ codegen-units = 256
 libnss = { git = "https://github.com/Firstyear/libnss-rs.git", branch = "20250207-freebsd" }
 # Allow ssh keys to have comments with spaces.
 sshkeys = { git = "https://github.com/Firstyear/rust-sshkeys.git", rev = "3a081cbf7480628223bcb96fc8aaa8c19109d007" }
-
+# Branch currently carrying some needed rs256 signer patches for jwk handling,
+# as main is currently working to drop openssl and may need more work before
+# we commit to that change here.
+compact_jwt = { git = "https://github.com/Firstyear/compact-jwt.git", rev = "b3d2b5700cfe567d384c81df35d25537fbf7f110" }
 
 [workspace.dependencies]
 kanidmd_core = { path = "./server/core", version = "=1.6.0-dev" }
@@ -173,7 +176,6 @@ csv = "1.3.1"
 dialoguer = "0.11.0"
 dhat = "0.3.3"
 dyn-clone = "^1.0.17"
-fernet = "^0.2.1"
 filetime = "^0.2.24"
 fs4 = "^0.13.0"
 futures = "^0.3.31"
diff --git a/libs/client/src/oauth.rs b/libs/client/src/oauth.rs
index 9b4ce756d..526268ec0 100644
--- a/libs/client/src/oauth.rs
+++ b/libs/client/src/oauth.rs
@@ -1,16 +1,18 @@
 use crate::{ClientError, KanidmClient};
 use kanidm_proto::attribute::Attribute;
 use kanidm_proto::constants::{
-    ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_NAME,
+    ATTR_DISPLAYNAME, ATTR_KEY_ACTION_REVOKE, ATTR_KEY_ACTION_ROTATE, ATTR_NAME,
     ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
     ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_PREFER_SHORT_USERNAME,
     ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING,
-    ATTR_OAUTH2_RS_TOKEN_KEY, ATTR_OAUTH2_STRICT_REDIRECT_URI, ATTR_RS256_PRIVATE_KEY_DER,
+    ATTR_OAUTH2_STRICT_REDIRECT_URI,
 };
 use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
 use kanidm_proto::v1::Entry;
 use reqwest::multipart;
 use std::collections::BTreeMap;
+use time::format_description::well_known::Rfc3339;
+use time::OffsetDateTime;
 use url::Url;
 
 impl KanidmClient {
@@ -84,7 +86,34 @@ impl KanidmClient {
             .await
     }
 
-    #[allow(clippy::too_many_arguments)]
+    pub async fn idm_oauth2_rs_revoke_key(
+        &self,
+        id: &str,
+        key_id: &str,
+    ) -> Result<(), ClientError> {
+        self.perform_post_request(
+            &format!("/v1/oauth2/{}/_attr/{}", id, ATTR_KEY_ACTION_REVOKE),
+            vec![key_id.to_string()],
+        )
+        .await
+    }
+
+    pub async fn idm_oauth2_rs_rotate_keys(
+        &self,
+        id: &str,
+        rotate_at_time: OffsetDateTime,
+    ) -> Result<(), ClientError> {
+        let rfc_3339_str = rotate_at_time.format(&Rfc3339).map_err(|_| {
+            ClientError::InvalidRequest("Unable to format rfc 3339 datetime".into())
+        })?;
+
+        self.perform_post_request(
+            &format!("/v1/oauth2/{}/_attr/{}", id, ATTR_KEY_ACTION_ROTATE),
+            vec![rfc_3339_str],
+        )
+        .await
+    }
+
     pub async fn idm_oauth2_rs_update(
         &self,
         id: &str,
@@ -92,8 +121,6 @@ impl KanidmClient {
         displayname: 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(),
@@ -121,19 +148,6 @@ impl KanidmClient {
                 .attrs
                 .insert(ATTR_OAUTH2_RS_BASIC_SECRET.to_string(), Vec::new());
         }
-        if reset_token_key {
-            update_oauth2_rs
-                .attrs
-                .insert(ATTR_OAUTH2_RS_TOKEN_KEY.to_string(), Vec::new());
-        }
-        if reset_sign_key {
-            update_oauth2_rs
-                .attrs
-                .insert(ATTR_ES256_PRIVATE_KEY_DER.to_string(), Vec::new());
-            update_oauth2_rs
-                .attrs
-                .insert(ATTR_RS256_PRIVATE_KEY_DER.to_string(), Vec::new());
-        }
         self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
             .await
     }
diff --git a/proto/src/attribute.rs b/proto/src/attribute.rs
index 493509944..174942689 100644
--- a/proto/src/attribute.rs
+++ b/proto/src/attribute.rs
@@ -89,6 +89,7 @@ pub enum Attribute {
     KeyActionRotate,
     KeyActionRevoke,
     KeyActionImportJwsEs256,
+    KeyActionImportJwsRs256,
     KeyInternalData,
     KeyProvider,
     LastModifiedCid,
@@ -323,6 +324,7 @@ impl Attribute {
             Attribute::KeyActionRotate => ATTR_KEY_ACTION_ROTATE,
             Attribute::KeyActionRevoke => ATTR_KEY_ACTION_REVOKE,
             Attribute::KeyActionImportJwsEs256 => ATTR_KEY_ACTION_IMPORT_JWS_ES256,
+            Attribute::KeyActionImportJwsRs256 => ATTR_KEY_ACTION_IMPORT_JWS_RS256,
             Attribute::KeyInternalData => ATTR_KEY_INTERNAL_DATA,
             Attribute::KeyProvider => ATTR_KEY_PROVIDER,
             Attribute::LastModifiedCid => ATTR_LAST_MODIFIED_CID,
@@ -510,6 +512,7 @@ impl Attribute {
             ATTR_KEY_ACTION_ROTATE => Attribute::KeyActionRotate,
             ATTR_KEY_ACTION_REVOKE => Attribute::KeyActionRevoke,
             ATTR_KEY_ACTION_IMPORT_JWS_ES256 => Attribute::KeyActionImportJwsEs256,
+            ATTR_KEY_ACTION_IMPORT_JWS_RS256 => Attribute::KeyActionImportJwsRs256,
             ATTR_KEY_INTERNAL_DATA => Attribute::KeyInternalData,
             ATTR_KEY_PROVIDER => Attribute::KeyProvider,
             ATTR_LAST_MODIFIED_CID => Attribute::LastModifiedCid,
diff --git a/proto/src/constants.rs b/proto/src/constants.rs
index 414c51791..aaa1c7392 100644
--- a/proto/src/constants.rs
+++ b/proto/src/constants.rs
@@ -133,6 +133,7 @@ pub const ATTR_JWS_ES256_PRIVATE_KEY: &str = "jws_es256_private_key";
 pub const ATTR_KEY_ACTION_ROTATE: &str = "key_action_rotate";
 pub const ATTR_KEY_ACTION_REVOKE: &str = "key_action_revoke";
 pub const ATTR_KEY_ACTION_IMPORT_JWS_ES256: &str = "key_action_import_jws_es256";
+pub const ATTR_KEY_ACTION_IMPORT_JWS_RS256: &str = "key_action_import_jws_rs256";
 pub const ATTR_KEY_INTERNAL_DATA: &str = "key_internal_data";
 pub const ATTR_KEY_PROVIDER: &str = "key_provider";
 pub const ATTR_LAST_MODIFIED_CID: &str = "last_modified_cid";
@@ -319,5 +320,6 @@ pub const ENTRYCLASS_KEY_PROVIDER: &str = "key_provider";
 pub const ENTRYCLASS_KEY_PROVIDER_INTERNAL: &str = "key_provider_internal";
 pub const ENTRYCLASS_KEY_OBJECT: &str = "key_object";
 pub const ENTRYCLASS_KEY_OBJECT_JWT_ES256: &str = "key_object_jwt_es256";
+pub const ENTRYCLASS_KEY_OBJECT_JWT_RS256: &str = "key_object_jwt_rs256";
 pub const ENTRYCLASS_KEY_OBJECT_JWE_A128GCM: &str = "key_object_jwe_a128gcm";
 pub const ENTRYCLASS_KEY_OBJECT_INTERNAL: &str = "key_object_internal";
diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs
index 09f6cb144..3facaa936 100644
--- a/proto/src/internal/error.rs
+++ b/proto/src/internal/error.rs
@@ -270,6 +270,25 @@ pub enum OperationError {
     KP0043KeyObjectJweA128GCMEncryption,
     KP0044KeyObjectJwsPublicJwk,
 
+    KP0045KeyObjectImportJwsRs256DerInvalid,
+    KP0046KeyObjectSignerToVerifier,
+    KP0047KeyObjectPublicToDer,
+    KP0048KeyObjectJwtRs256Generation,
+    KP0049KeyObjectSignerToVerifier,
+    KP0050KeyObjectPrivateToDer,
+    KP0051KeyObjectPublicToDer,
+    KP0052KeyObjectJwsRs256DerInvalid,
+    KP0053KeyObjectSignerToVerifier,
+    KP0054KeyObjectJwsRs256DerInvalid,
+    KP0055KeyObjectJwsRs256DerInvalid,
+    KP0056KeyObjectJwsRs256Signature,
+    KP0057KeyObjectJwsNotAssociated,
+    KP0058KeyObjectJwsInvalid,
+    KP0059KeyObjectJwsKeyRevoked,
+    KP0060KeyObjectJwsPublicJwk,
+    KP0061KeyObjectNoActiveSigningKeys,
+    KP0062KeyProviderNoSuchKey,
+
     // Plugins
     PL0001GidOverlapsSystemRange,
 
@@ -448,6 +467,26 @@ impl OperationError {
             Self::KP0042KeyObjectNoActiveEncryptionKeys => None,
             Self::KP0043KeyObjectJweA128GCMEncryption => None,
             Self::KP0044KeyObjectJwsPublicJwk => None,
+
+            Self::KP0045KeyObjectImportJwsRs256DerInvalid => None,
+            Self::KP0046KeyObjectSignerToVerifier => None,
+            Self::KP0047KeyObjectPublicToDer => None,
+            Self::KP0048KeyObjectJwtRs256Generation => None,
+            Self::KP0049KeyObjectSignerToVerifier => None,
+            Self::KP0050KeyObjectPrivateToDer => None,
+            Self::KP0051KeyObjectPublicToDer => None,
+            Self::KP0052KeyObjectJwsRs256DerInvalid => None,
+            Self::KP0053KeyObjectSignerToVerifier => None,
+            Self::KP0054KeyObjectJwsRs256DerInvalid => None,
+            Self::KP0055KeyObjectJwsRs256DerInvalid => None,
+            Self::KP0056KeyObjectJwsRs256Signature => None,
+            Self::KP0057KeyObjectJwsNotAssociated => None,
+            Self::KP0058KeyObjectJwsInvalid => None,
+            Self::KP0059KeyObjectJwsKeyRevoked => None,
+            Self::KP0060KeyObjectJwsPublicJwk => None,
+            Self::KP0061KeyObjectNoActiveSigningKeys => None,
+            Self::KP0062KeyProviderNoSuchKey => None,
+
             Self::KU001InitWhileSessionActive => Some("The session was active when the init function was called.".into()),
             Self::KU002ContinueWhileSessionInActive => Some("Attempted to continue auth session while current session is inactive".into()),
             Self::KU003PamAuthFailed => Some("Failed PAM account authentication step".into()),
diff --git a/server/Dockerfile b/server/Dockerfile
index 5c92524c1..1bfc8586a 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -65,6 +65,8 @@ RUN <<EOF
     ls -Rla /out/libs-root
 EOF
 
+RUN ls /usr/src/kanidm/target/release/kanidmd
+
 # ======================
 
 FROM scratch
diff --git a/server/daemon/insecure_server.toml b/server/daemon/insecure_server.toml
index b0cc23bc8..660ae9cf2 100644
--- a/server/daemon/insecure_server.toml
+++ b/server/daemon/insecure_server.toml
@@ -13,9 +13,9 @@ tls_key = "/tmp/kanidm/key.pem"
 #   NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable
 #   Defaults to "info"
 #
-log_level = "info"
+# log_level = "info"
 # log_level = "debug"
-# log_level = "trace"
+log_level = "trace"
 
 # otel_grpc_url = "http://localhost:4317"
 
diff --git a/server/lib/Cargo.toml b/server/lib/Cargo.toml
index aad01fb19..7b048e5d3 100644
--- a/server/lib/Cargo.toml
+++ b/server/lib/Cargo.toml
@@ -32,7 +32,6 @@ compact_jwt = { workspace = true, features = ["openssl", "hsm-crypto"] }
 concread = { workspace = true }
 dhat = { workspace = true, optional = true }
 dyn-clone = { workspace = true }
-fernet = { workspace = true, features = ["fernet_danger_timestamps"] }
 # futures-util = { workspace = true }
 hashbrown = { workspace = true }
 idlset = { workspace = true }
diff --git a/server/lib/src/be/dbvalue.rs b/server/lib/src/be/dbvalue.rs
index b6fbc5df0..dc0d5d040 100644
--- a/server/lib/src/be/dbvalue.rs
+++ b/server/lib/src/be/dbvalue.rs
@@ -692,6 +692,7 @@ pub enum DbValueImage {
 #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
 pub enum DbValueKeyUsage {
     JwsEs256,
+    JwsRs256,
     JweA128GCM,
 }
 
diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs
index 36f48aac4..e8f0f1b56 100644
--- a/server/lib/src/constants/entries.rs
+++ b/server/lib/src/constants/entries.rs
@@ -40,6 +40,7 @@ pub enum EntryClass {
     KeyProviderInternal,
     KeyObject,
     KeyObjectJwtEs256,
+    KeyObjectJwtRs256,
     KeyObjectJweA128GCM,
     KeyObjectInternal,
     MemberOf,
@@ -94,6 +95,7 @@ impl From<EntryClass> for &'static str {
             EntryClass::KeyProviderInternal => ENTRYCLASS_KEY_PROVIDER_INTERNAL,
             EntryClass::KeyObject => ENTRYCLASS_KEY_OBJECT,
             EntryClass::KeyObjectJwtEs256 => ENTRYCLASS_KEY_OBJECT_JWT_ES256,
+            EntryClass::KeyObjectJwtRs256 => ENTRYCLASS_KEY_OBJECT_JWT_RS256,
             EntryClass::KeyObjectJweA128GCM => ENTRYCLASS_KEY_OBJECT_JWE_A128GCM,
             EntryClass::KeyObjectInternal => ENTRYCLASS_KEY_OBJECT_INTERNAL,
             EntryClass::MemberOf => ENTRYCLASS_MEMBER_OF,
diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs
index fc6c2f286..2471aee40 100644
--- a/server/lib/src/constants/uuids.rs
+++ b/server/lib/src/constants/uuids.rs
@@ -334,6 +334,10 @@ pub const UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS: Uuid =
     uuid!("00000000-0000-0000-0000-ffff00000189");
 pub const UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS: Uuid =
     uuid!("00000000-0000-0000-0000-ffff00000190");
+pub const UUID_SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256: Uuid =
+    uuid!("00000000-0000-0000-0000-ffff00000191");
+pub const UUID_SCHEMA_CLASS_KEY_OBJECT_JWT_RS256: Uuid =
+    uuid!("00000000-0000-0000-0000-ffff00000192");
 
 // System and domain infos
 // I'd like to strongly criticise william of the past for making poor choices about these allocations.
diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs
index 2905abbd2..ca02884a1 100644
--- a/server/lib/src/idm/oauth2.rs
+++ b/server/lib/src/idm/oauth2.rs
@@ -4,57 +4,49 @@
 //! integrations, which are then able to be used an accessed from the IDM layer
 //! for operations involving OAuth2 authentication processing.
 
-use std::collections::btree_map::Entry as BTreeEntry;
-use std::collections::{BTreeMap, BTreeSet};
-use std::convert::TryFrom;
-use std::fmt;
-use std::str::FromStr;
-use std::sync::Arc;
-use std::time::Duration;
-
+use crate::idm::account::Account;
+use crate::idm::server::{
+    IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
+};
+use crate::prelude::*;
+use crate::server::keys::{KeyObject, KeyProvidersTransaction, KeyProvidersWriteTransaction};
+use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
 use base64::{engine::general_purpose, Engine as _};
-use hashbrown::HashSet;
-
 pub use compact_jwt::{compact::JwkKeySet, OidcToken};
 use compact_jwt::{
-    crypto::JwsRs256Signer, jws::JwsBuilder, JwsCompact, JwsEs256Signer, JwsSigner,
-    JwsSignerToVerifier, JwsVerifier, OidcClaims, OidcSubject,
+    crypto::{JweA128GCMEncipher, JweA128KWEncipher},
+    jwe::Jwe,
+    jws::JwsBuilder,
+    JweCompact, JwsCompact, OidcClaims, OidcSubject,
 };
 use concread::cowcell::*;
-use fernet::Fernet;
 use hashbrown::HashMap;
+use hashbrown::HashSet;
 use kanidm_proto::constants::*;
-
-// #[cfg(feature = "dev-oauth2-device-flow")]
-// use kanidm_proto::oauth2::OAUTH2_DEVICE_CODE_EXPIRY_SECONDS;
-
 pub use kanidm_proto::oauth2::{
     AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
     AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
     OAuth2RFC9068Token, OAuth2RFC9068TokenExtensions, Oauth2Rfc8414MetadataResponse,
     OidcDiscoveryResponse, OidcWebfingerRel, OidcWebfingerResponse, PkceAlg, TokenRevokeRequest,
 };
-
 use kanidm_proto::oauth2::{
     AccessTokenType, ClaimType, DeviceAuthorizationResponse, DisplayValue, GrantType,
     IdTokenSignAlg, ResponseMode, ResponseType, SubjectType, TokenEndpointAuthMethod,
 };
 use openssl::sha;
-
 use serde::{Deserialize, Serialize};
 use serde_with::{formats, serde_as};
+use std::collections::btree_map::Entry as BTreeEntry;
+use std::collections::{BTreeMap, BTreeSet};
+use std::fmt;
+use std::str::FromStr;
+use std::sync::Arc;
+use std::time::Duration;
 use time::OffsetDateTime;
 use tracing::trace;
 use uri::{OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
 use url::{Host, Origin, Url};
 
-use crate::idm::account::Account;
-use crate::idm::server::{
-    IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
-};
-use crate::prelude::*;
-use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
-
 #[derive(Serialize, Deserialize, Debug, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum Oauth2Error {
@@ -134,6 +126,8 @@ struct ConsentToken {
     pub client_id: String,
     // Must match the session id of the Uat,
     pub session_id: Uuid,
+    pub expiry: u64,
+
     // So we can ensure that we really match the same uat to prevent confusions.
     pub ident_id: IdentityId,
     // CSRF
@@ -158,10 +152,11 @@ struct ConsentToken {
 struct TokenExchangeCode {
     // We don't need the client_id here, because it's signed with an RS specific
     // key which gives us the assurance that it's the correct combination.
-    // pub uat: UserAuthToken,
     pub account_uuid: Uuid,
     pub session_id: Uuid,
 
+    pub expiry: u64,
+
     // The S256 code challenge.
     #[serde_as(
         as = "Option<serde_with::base64::Base64<serde_with::base64::UrlSafe, formats::Unpadded>>"
@@ -359,12 +354,6 @@ impl std::fmt::Debug for OauthRSType {
     }
 }
 
-#[derive(Clone)]
-enum Oauth2JwsSigner {
-    ES256 { signer: JwsEs256Signer },
-    RS256 { signer: JwsRs256Signer },
-}
-
 #[derive(Clone, Debug)]
 struct ClaimValue {
     join: OauthClaimMapJoin,
@@ -398,6 +387,12 @@ impl ClaimValue {
     }
 }
 
+#[derive(Clone, Copy, Debug)]
+enum SignatureAlgo {
+    Es256,
+    Rs256,
+}
+
 #[derive(Clone)]
 pub struct Oauth2RS {
     name: String,
@@ -416,8 +411,8 @@ pub struct Oauth2RS {
     client_scopes: BTreeSet<String>,
     client_sup_scopes: BTreeSet<String>,
     // Our internal exchange encryption material for this rs.
-    token_fernet: Fernet,
-    jws_signer: Oauth2JwsSigner,
+    sign_alg: SignatureAlgo,
+    key_object: Arc<KeyObject>,
 
     // For oidc we also need our issuer url.
     iss: Url,
@@ -486,7 +481,7 @@ impl std::fmt::Debug for Oauth2RS {
 #[derive(Clone)]
 struct Oauth2RSInner {
     origin: Url,
-    fernet: Fernet,
+    consent_key: JweA128KWEncipher,
     rs_set: HashMap<String, Oauth2RS>,
 }
 
@@ -502,31 +497,20 @@ pub struct Oauth2ResourceServersWriteTransaction<'a> {
     inner: CowCellWriteTxn<'a, Oauth2RSInner>,
 }
 
-impl TryFrom<(Vec<Arc<EntrySealedCommitted>>, Url, DomainVersion)> for Oauth2ResourceServers {
-    type Error = OperationError;
+impl Oauth2ResourceServers {
+    pub fn new(origin: Url) -> Result<Self, OperationError> {
+        let consent_key = JweA128KWEncipher::generate_ephemeral()
+            .map_err(|_| OperationError::CryptographyError)?;
 
-    fn try_from(
-        value: (Vec<Arc<EntrySealedCommitted>>, Url, DomainVersion),
-    ) -> Result<Self, Self::Error> {
-        let (value, origin, domain_level) = value;
-        let fernet =
-            Fernet::new(&Fernet::generate_key()).ok_or(OperationError::CryptographyError)?;
-        let oauth2rs = Oauth2ResourceServers {
+        Ok(Oauth2ResourceServers {
             inner: CowCell::new(Oauth2RSInner {
                 origin,
-                fernet,
+                consent_key,
                 rs_set: HashMap::new(),
             }),
-        };
-
-        let mut oauth2rs_wr = oauth2rs.write();
-        oauth2rs_wr.reload(value, domain_level)?;
-        oauth2rs_wr.commit();
-        Ok(oauth2rs)
+        })
     }
-}
 
-impl Oauth2ResourceServers {
     pub fn read(&self) -> Oauth2ResourceServersReadTransaction {
         Oauth2ResourceServersReadTransaction {
             inner: self.inner.read(),
@@ -544,6 +528,7 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
     pub fn reload(
         &mut self,
         value: Vec<Arc<EntrySealedCommitted>>,
+        key_providers: &KeyProvidersWriteTransaction,
         domain_level: DomainVersion,
     ) -> Result<(), OperationError> {
         let rs_set: Result<HashMap<_, _>, _> = value
@@ -552,13 +537,23 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                 let uuid = ent.get_uuid();
                 trace!(?uuid, "Checking OAuth2 configuration");
                 // From each entry, attempt to make an OAuth2 configuration.
-                if !ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) {
+                if !ent
+                    .attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
+                {
                     error!("Missing class oauth2_resource_server");
                     // Check we have oauth2_resource_server class
                     return Err(OperationError::InvalidEntryState);
                 }
 
-                let type_ = if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) {
+                let Some(key_object) = key_providers.get_key_object_handle(uuid) else {
+                    error!("OAuth2 rs is missing it's key object!");
+                    return Err(OperationError::InvalidEntryState);
+                };
+
+                let type_ = if ent.attribute_equality(
+                    Attribute::Class,
+                    &EntryClass::OAuth2ResourceServerBasic.into(),
+                ) {
                     let authz_secret = ent
                         .get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
                         .map(str::to_string)
@@ -573,13 +568,16 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                         authz_secret,
                         enable_pkce,
                     }
-                } else if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerPublic.into()) {
+                } else if ent.attribute_equality(
+                    Attribute::Class,
+                    &EntryClass::OAuth2ResourceServerPublic.into(),
+                ) {
                     let allow_localhost_redirect = ent
                         .get_ava_single_bool(Attribute::OAuth2AllowLocalhostRedirect)
                         .unwrap_or(false);
 
                     OauthRSType::Public {
-                        allow_localhost_redirect
+                        allow_localhost_redirect,
                     }
                 } else {
                     error!("Missing class determining OAuth2 rs type");
@@ -597,7 +595,6 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                     .map(str::to_string)
                     .ok_or(OperationError::InvalidValueState)?;
 
-
                 // Setup the landing uri and its implied origin, as well as
                 // the supplemental origins.
                 let landing_url = ent
@@ -605,14 +602,17 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                     .cloned()
                     .ok_or(OperationError::InvalidValueState)?;
 
-                let maybe_extra_urls = ent.get_ava_set(Attribute::OAuth2RsOrigin).and_then(|s| s.as_url_set());
+                let maybe_extra_urls = ent
+                    .get_ava_set(Attribute::OAuth2RsOrigin)
+                    .and_then(|s| s.as_url_set());
 
                 let len_uris = maybe_extra_urls.map(|s| s.len() + 1).unwrap_or(1);
 
                 // If we are DL8, then strict enforcement is always required.
-                let strict_redirect_uri = cfg!(test) ||
-                    domain_level >= DOMAIN_LEVEL_8 ||
-                    ent.get_ava_single_bool(Attribute::OAuth2StrictRedirectUri)
+                let strict_redirect_uri = cfg!(test)
+                    || domain_level >= DOMAIN_LEVEL_8
+                    || ent
+                        .get_ava_single_bool(Attribute::OAuth2StrictRedirectUri)
                         .unwrap_or(false);
 
                 // The reason we have to allocate this is that we need to do some processing on these
@@ -651,13 +651,6 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                     }
                 }
 
-                let token_fernet = ent
-                    .get_ava_single_secret(Attribute::OAuth2RsTokenKey)
-                    .ok_or(OperationError::InvalidValueState)
-                    .and_then(|key| {
-                        Fernet::new(key).ok_or(OperationError::CryptographyError)
-                    })?;
-
                 let scope_maps = ent
                     .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
                     .cloned()
@@ -670,38 +663,38 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
 
                 // From our scope maps we can now determine what scopes would be granted to our
                 // client during a client credentials authentication.
-                let (client_scopes, client_sup_scopes) = if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) {
-                    let client_scopes =
-                        scope_maps
-                        .iter()
-                        .filter_map(|(u, m)| {
-                            if client_member_of.contains(u) {
-                                Some(m.iter())
-                            } else {
-                                None
-                            }
-                        })
-                        .flatten()
-                        .cloned()
-                        .collect::<BTreeSet<_>>();
+                let (client_scopes, client_sup_scopes) =
+                    if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) {
+                        let client_scopes = scope_maps
+                            .iter()
+                            .filter_map(|(u, m)| {
+                                if client_member_of.contains(u) {
+                                    Some(m.iter())
+                                } else {
+                                    None
+                                }
+                            })
+                            .flatten()
+                            .cloned()
+                            .collect::<BTreeSet<_>>();
 
-                    let client_sup_scopes = sup_scope_maps
-                        .iter()
-                        .filter_map(|(u, m)| {
-                            if client_member_of.contains(u) {
-                                Some(m.iter())
-                            } else {
-                                None
-                            }
-                        })
-                        .flatten()
-                        .cloned()
-                        .collect::<BTreeSet<_>>();
+                        let client_sup_scopes = sup_scope_maps
+                            .iter()
+                            .filter_map(|(u, m)| {
+                                if client_member_of.contains(u) {
+                                    Some(m.iter())
+                                } else {
+                                    None
+                                }
+                            })
+                            .flatten()
+                            .cloned()
+                            .collect::<BTreeSet<_>>();
 
-                    (client_scopes, client_sup_scopes)
-                } else {
-                    (BTreeSet::default(), BTreeSet::default())
-                };
+                        (client_scopes, client_sup_scopes)
+                    } else {
+                        (BTreeSet::default(), BTreeSet::default())
+                    };
 
                 let e_claim_maps = ent
                     .get_ava_set(Attribute::OAuth2RsClaimMap)
@@ -720,23 +713,21 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                             // to be unique.
                             match claim_map.entry(*group_uuid) {
                                 BTreeEntry::Vacant(e) => {
-                                    e.insert(
-                                        vec![
-                                            (
-                                            claim_name.clone(), ClaimValue {
-                                                join: claim_mapping.join(),
-                                                values: claim_values.clone()
-                                            }
-                                            )
-                                        ]
-                                    );
+                                    e.insert(vec![(
+                                        claim_name.clone(),
+                                        ClaimValue {
+                                            join: claim_mapping.join(),
+                                            values: claim_values.clone(),
+                                        },
+                                    )]);
                                 }
                                 BTreeEntry::Occupied(mut e) => {
                                     e.get_mut().push((
-                                            claim_name.clone(), ClaimValue {
-                                                join: claim_mapping.join(),
-                                                values: claim_values.clone()
-                                            }
+                                        claim_name.clone(),
+                                        ClaimValue {
+                                            join: claim_mapping.join(),
+                                            values: claim_values.clone(),
+                                        },
                                     ));
                                 }
                             }
@@ -748,33 +739,13 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                     BTreeMap::default()
                 };
 
-                trace!("{}", Attribute::OAuth2JwtLegacyCryptoEnable);
-                let jws_signer = if ent.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false) {
-                    trace!("{}", Attribute::Rs256PrivateKeyDer);
-                    ent
-                        .get_ava_single_private_binary(Attribute::Rs256PrivateKeyDer)
-                        .ok_or(OperationError::InvalidValueState)
-                        .and_then(|key_der| {
-                            JwsRs256Signer::from_rs256_der(key_der)
-                            .map(|signer| Oauth2JwsSigner::RS256 { signer })
-                            .map_err(|e| {
-                                admin_error!(err = ?e, "Unable to load Legacy RS256 JwsSigner from DER");
-                                OperationError::CryptographyError
-                            })
-                        })?
+                let sign_alg = if ent
+                    .get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable)
+                    .unwrap_or(false)
+                {
+                    SignatureAlgo::Rs256
                 } else {
-                    trace!("{}", Attribute::Es256PrivateKeyDer);
-                    ent
-                        .get_ava_single_private_binary(Attribute::Es256PrivateKeyDer)
-                        .ok_or(OperationError::InvalidValueState)
-                        .and_then(|key_der| {
-                            JwsEs256Signer::from_es256_der(key_der)
-                            .map(|signer| Oauth2JwsSigner::ES256 { signer })
-                            .map_err(|e| {
-                                admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER");
-                                OperationError::CryptographyError
-                            })
-                        })?
+                    SignatureAlgo::Es256
                 };
 
                 let prefer_short_username = ent
@@ -804,33 +775,31 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                 let mut iss = self.inner.origin.clone();
                 iss.set_path(&format!("/oauth2/openid/{name}"));
 
-                let scopes_supported: BTreeSet<String> =
-                scope_maps
+                let scopes_supported: BTreeSet<String> = scope_maps
                     .values()
                     .flat_map(|bts| bts.iter())
-
-                    .chain(
-                        sup_scope_maps
-                            .values()
-                            .flat_map(|bts| bts.iter())
-                    )
-
+                    .chain(sup_scope_maps.values().flat_map(|bts| bts.iter()))
                     .cloned()
                     .collect();
 
-
-                    let device_authorization_endpoint: Option<Url> = match cfg!(feature="dev-oauth2-device-flow") {
+                let device_authorization_endpoint: Option<Url> =
+                    match cfg!(feature = "dev-oauth2-device-flow") {
                         true => {
-                            match ent.get_ava_single_bool(Attribute::OAuth2DeviceFlowEnable).unwrap_or(false) {
-                            true => {
-                                let mut device_authorization_endpoint = self.inner.origin.clone();
-                                device_authorization_endpoint.set_path(uri::OAUTH2_AUTHORISE_DEVICE);
-                                Some(device_authorization_endpoint)
-                            },
-                            false => None
+                            match ent
+                                .get_ava_single_bool(Attribute::OAuth2DeviceFlowEnable)
+                                .unwrap_or(false)
+                            {
+                                true => {
+                                    let mut device_authorization_endpoint =
+                                        self.inner.origin.clone();
+                                    device_authorization_endpoint
+                                        .set_path(uri::OAUTH2_AUTHORISE_DEVICE);
+                                    Some(device_authorization_endpoint)
+                                }
+                                false => None,
                             }
-                        },
-                        false => {None}
+                        }
+                        false => None,
                     };
                 let client_id = name.clone();
                 let rscfg = Oauth2RS {
@@ -847,8 +816,8 @@ impl Oauth2ResourceServersWriteTransaction<'_> {
                     client_scopes,
                     client_sup_scopes,
                     claim_map,
-                    token_fernet,
-                    jws_signer,
+                    sign_alg,
+                    key_object,
                     iss,
                     authorization_endpoint,
                     token_endpoint,
@@ -919,24 +888,19 @@ impl IdmServerProxyWriteTransaction<'_> {
         // are either signed *or* encrypted, we need to check both options.
 
         let (session_id, expiry, uuid) = if let Ok(jwsc) = JwsCompact::from_str(&revoke_req.token) {
-            let access_token = match &o2rs.jws_signer {
-                Oauth2JwsSigner::ES256 { signer } => signer
-                    .get_verifier()
-                    .and_then(|verifier| verifier.verify(&jwsc)),
-                Oauth2JwsSigner::RS256 { signer } => signer
-                    .get_verifier()
-                    .and_then(|verifier| verifier.verify(&jwsc)),
-            }
-            .map_err(|err| {
-                admin_error!(?err, "Unable to verify access token");
-                Oauth2Error::InvalidRequest
-            })
-            .and_then(|jws| {
-                jws.from_json().map_err(|err| {
-                    admin_error!(?err, "Unable to deserialise access token");
+            let access_token = o2rs
+                .key_object
+                .jws_verify(&jwsc)
+                .map_err(|err| {
+                    admin_error!(?err, "Unable to verify access token");
                     Oauth2Error::InvalidRequest
                 })
-            })?;
+                .and_then(|jws| {
+                    jws.from_json().map_err(|err| {
+                        admin_error!(?err, "Unable to deserialise access token");
+                        Oauth2Error::InvalidRequest
+                    })
+                })?;
 
             let OAuth2RFC9068Token::<_> {
                 sub: uuid,
@@ -948,17 +912,21 @@ impl IdmServerProxyWriteTransaction<'_> {
             (session_id, exp, uuid)
         } else {
             // Assume it's encrypted.
+            let jwe_compact = JweCompact::from_str(&revoke_req.token).map_err(|_| {
+                error!("Failed to deserialise a valid JWE");
+                Oauth2Error::InvalidRequest
+            })?;
 
             let token: Oauth2TokenType = o2rs
-                .token_fernet
-                .decrypt(&revoke_req.token)
+                .key_object
+                .jwe_decrypt(&jwe_compact)
                 .map_err(|_| {
-                    admin_error!("Failed to decrypt token revoke request");
+                    error!("Failed to decrypt token revoke request");
                     Oauth2Error::InvalidRequest
                 })
-                .and_then(|data| {
-                    serde_json::from_slice(&data).map_err(|e| {
-                        admin_error!("Failed to deserialise token - {:?}", e);
+                .and_then(|jwe| {
+                    jwe.from_json().map_err(|err| {
+                        error!(?err, "Failed to deserialise token");
                         Oauth2Error::InvalidRequest
                     })
                 })?;
@@ -1185,19 +1153,23 @@ impl IdmServerProxyWriteTransaction<'_> {
             return Err(OperationError::InvalidSessionState);
         };
 
-        // Decode the consent req with our system fernet key. Use a ttl of 5 minutes.
+        let consent_token_jwe = JweCompact::from_str(consent_token).map_err(|err| {
+            error!(?err, "Consent token is not a valid jwe compact");
+            OperationError::InvalidSessionState
+        })?;
+
         let consent_req: ConsentToken = self
             .oauth2rs
             .inner
-            .fernet
-            .decrypt_at_time(consent_token, Some(300), ct.as_secs())
-            .map_err(|_| {
-                admin_error!("Failed to decrypt consent request");
+            .consent_key
+            .decipher(&consent_token_jwe)
+            .map_err(|err| {
+                error!(?err, "Failed to decrypt consent request");
                 OperationError::CryptographyError
             })
-            .and_then(|data| {
-                serde_json::from_slice(&data).map_err(|e| {
-                    admin_error!(err = ?e, "Failed to deserialise consent request");
+            .and_then(|jwe| {
+                jwe.from_json().map_err(|err| {
+                    error!(?err, "Failed to deserialise consent request");
                     OperationError::SerdeJsonError
                 })
             })?;
@@ -1214,6 +1186,15 @@ impl IdmServerProxyWriteTransaction<'_> {
             return Err(OperationError::InvalidSessionState);
         }
 
+        if consent_req.expiry <= ct.as_secs() {
+            // Token is expired
+            error!("Failed to decrypt consent request");
+            return Err(OperationError::CryptographyError);
+        }
+
+        // The exchange must be performed in the next 60 seconds.
+        let expiry = ct.as_secs() + 60;
+
         // Get the resource server config based on this client_id.
         let o2rs = self
             .oauth2rs
@@ -1227,22 +1208,29 @@ impl IdmServerProxyWriteTransaction<'_> {
 
         // Extract the state, code challenge, redirect_uri
         let xchg_code = TokenExchangeCode {
-            // uat: uat.clone(),
             account_uuid,
             session_id: ident.get_session_id(),
+            expiry,
             code_challenge: consent_req.code_challenge,
             redirect_uri: consent_req.redirect_uri.clone(),
             scopes: consent_req.scopes.clone(),
             nonce: consent_req.nonce,
         };
 
-        // Encrypt the exchange token with the fernet key of the client resource server
-        let code_data = serde_json::to_vec(&xchg_code).map_err(|e| {
-            admin_error!(err = ?e, "Unable to encode xchg_code data");
+        // Encrypt the exchange token
+        let code_data_jwe = Jwe::into_json(&xchg_code).map_err(|err| {
+            error!(?err, "Unable to encode xchg_code data");
             OperationError::SerdeJsonError
         })?;
 
-        let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
+        let code = o2rs
+            .key_object
+            .jwe_a128gcm_encrypt(&code_data_jwe, ct)
+            .map(|code| code.to_string())
+            .map_err(|err| {
+                error!(?err, "Unable to encrypt xchg_code");
+                OperationError::CryptographyError
+            })?;
 
         // Everything is DONE! Now submit that it's all happy and the user consented correctly.
         // this will let them bypass consent steps in the future.
@@ -1283,21 +1271,31 @@ impl IdmServerProxyWriteTransaction<'_> {
     ) -> Result<AccessTokenResponse, Oauth2Error> {
         // Check the token_req is within the valid time, and correctly signed for
         // this client.
+        let jwe_compact = JweCompact::from_str(token_req_code).map_err(|_| {
+            error!("Failed to deserialise a valid JWE");
+            Oauth2Error::InvalidRequest
+        })?;
 
         let code_xchg: TokenExchangeCode = o2rs
-            .token_fernet
-            .decrypt_at_time(token_req_code, Some(60), ct.as_secs())
+            .key_object
+            .jwe_decrypt(&jwe_compact)
             .map_err(|_| {
                 admin_error!("Failed to decrypt token exchange request");
                 Oauth2Error::InvalidRequest
             })
-            .and_then(|data| {
-                serde_json::from_slice(&data).map_err(|e| {
-                    admin_error!("Failed to deserialise token exchange code - {:?}", e);
+            .and_then(|jwe| {
+                debug!(?jwe);
+                jwe.from_json::<TokenExchangeCode>().map_err(|err| {
+                    error!(?err, "Failed to deserialise token exchange code");
                     Oauth2Error::InvalidRequest
                 })
             })?;
 
+        if code_xchg.expiry <= ct.as_secs() {
+            error!("Expired token exchange request");
+            return Err(Oauth2Error::InvalidRequest);
+        }
+
         // If we have a verifier present, we MUST assert that a code challenge is present!
         // It is worth noting here that code_xchg is *server issued* and encrypted, with
         // a short validity period. The client controlled value is in token_req.code_verifier
@@ -1380,17 +1378,22 @@ impl IdmServerProxyWriteTransaction<'_> {
         req_scopes: Option<&BTreeSet<String>>,
         ct: Duration,
     ) -> Result<AccessTokenResponse, Oauth2Error> {
+        let jwe_compact = JweCompact::from_str(refresh_token).map_err(|_| {
+            error!("Failed to deserialise a valid JWE");
+            Oauth2Error::InvalidRequest
+        })?;
+
         // Validate the refresh token decrypts and it's expiry is within the valid window.
         let token: Oauth2TokenType = o2rs
-            .token_fernet
-            .decrypt(refresh_token)
+            .key_object
+            .jwe_decrypt(&jwe_compact)
             .map_err(|_| {
                 admin_error!("Failed to decrypt refresh token request");
                 Oauth2Error::InvalidRequest
             })
-            .and_then(|data| {
-                serde_json::from_slice(&data).map_err(|e| {
-                    admin_error!("Failed to deserialise token - {:?}", e);
+            .and_then(|jwe| {
+                jwe.from_json().map_err(|err| {
+                    error!(?err, "Failed to deserialise token");
                     Oauth2Error::InvalidRequest
                 })
             })?;
@@ -1568,14 +1571,19 @@ impl IdmServerProxyWriteTransaction<'_> {
             nbf: iat,
         };
 
-        let access_token_data = serde_json::to_vec(&access_token_raw).map_err(|e| {
-            admin_error!(err = ?e, "Unable to encode token data");
+        let access_token_data = Jwe::into_json(&access_token_raw).map_err(|err| {
+            error!(?err, "Unable to encode token data");
             Oauth2Error::ServerError(OperationError::SerdeJsonError)
         })?;
 
         let access_token = o2rs
-            .token_fernet
-            .encrypt_at_time(&access_token_data, ct.as_secs());
+            .key_object
+            .jwe_a128gcm_encrypt(&access_token_data, ct)
+            .map(|jwe| jwe.to_string())
+            .map_err(|err| {
+                error!(?err, "Unable to encode token data");
+                Oauth2Error::ServerError(OperationError::CryptographyError)
+            })?;
 
         // Write the session to the db
         let session = Value::Oauth2Session(
@@ -1703,13 +1711,19 @@ impl IdmServerProxyWriteTransaction<'_> {
             };
 
             trace!(?oidc);
+            let oidc = JwsBuilder::into_json(&oidc)
+                .map(|builder| builder.build())
+                .map_err(|err| {
+                    admin_error!(?err, "Unable to encode access token data");
+                    Oauth2Error::ServerError(OperationError::InvalidState)
+                })?;
 
-            let jwt_signed = match &o2rs.jws_signer {
-                Oauth2JwsSigner::ES256 { signer } => signer.sign(&oidc),
-                Oauth2JwsSigner::RS256 { signer } => signer.sign(&oidc),
+            let jwt_signed = match o2rs.sign_alg {
+                SignatureAlgo::Es256 => o2rs.key_object.jws_es256_sign(&oidc, ct),
+                SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_sign(&oidc, ct),
             }
-            .map_err(|e| {
-                admin_error!(err = ?e, "Unable to encode uat data");
+            .map_err(|err| {
+                error!(?err, "Unable to encode oidc token data");
                 Oauth2Error::ServerError(OperationError::InvalidState)
             })?;
 
@@ -1742,14 +1756,14 @@ impl IdmServerProxyWriteTransaction<'_> {
 
         let access_token_data = JwsBuilder::into_json(&access_token_data)
             .map(|builder| builder.set_typ(Some("at+jwt")).build())
-            .map_err(|e| {
-                admin_error!(err = ?e, "Unable to encode access token data");
+            .map_err(|err| {
+                error!(?err, "Unable to encode access token data");
                 Oauth2Error::ServerError(OperationError::InvalidState)
             })?;
 
-        let access_token = match &o2rs.jws_signer {
-            Oauth2JwsSigner::ES256 { signer } => signer.sign(&access_token_data),
-            Oauth2JwsSigner::RS256 { signer } => signer.sign(&access_token_data),
+        let access_token = match o2rs.sign_alg {
+            SignatureAlgo::Es256 => o2rs.key_object.jws_es256_sign(&access_token_data, ct),
+            SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_sign(&access_token_data, ct),
         }
         .map_err(|e| {
             admin_error!(err = ?e, "Unable to sign access token data");
@@ -1767,14 +1781,19 @@ impl IdmServerProxyWriteTransaction<'_> {
             nonce,
         };
 
-        let refresh_token_data = serde_json::to_vec(&refresh_token_raw).map_err(|e| {
-            admin_error!(err = ?e, "Unable to encode token data");
+        let refresh_token_data = Jwe::into_json(&refresh_token_raw).map_err(|err| {
+            error!(?err, "Unable to encode token data");
             Oauth2Error::ServerError(OperationError::SerdeJsonError)
         })?;
 
         let refresh_token = o2rs
-            .token_fernet
-            .encrypt_at_time(&refresh_token_data, ct.as_secs());
+            .key_object
+            .jwe_a128gcm_encrypt(&refresh_token_data, ct)
+            .map(|jwe| jwe.to_string())
+            .map_err(|err| {
+                error!(?err, "Unable to encrypt token data");
+                Oauth2Error::ServerError(OperationError::CryptographyError)
+            })?;
 
         // Write the session to the db even with the refresh path, we need to do
         // this to update the "not issued before" time.
@@ -1846,15 +1865,20 @@ impl IdmServerProxyWriteTransaction<'_> {
             }
         }
 
-        o2rs.token_fernet
-            .decrypt(token)
-            .map_err(|_| {
-                admin_error!("Failed to decrypt token reflection request");
+        let jwe_compact = JweCompact::from_str(token).map_err(|err| {
+            error!(?err, "Failed to deserialise a valid JWE");
+            OperationError::InvalidSessionState
+        })?;
+
+        o2rs.key_object
+            .jwe_decrypt(&jwe_compact)
+            .map_err(|err| {
+                error!(?err, "Failed to decrypt token reflection request");
                 OperationError::CryptographyError
             })
-            .and_then(|data| {
-                serde_json::from_slice(&data).map_err(|e| {
-                    admin_error!("Failed to deserialise token exchange code - {:?}", e);
+            .and_then(|jwe| {
+                jwe.from_json().map_err(|err| {
+                    error!(?err, "Failed to deserialise token for reflection");
                     OperationError::SerdeJsonError
                 })
             })
@@ -2129,23 +2153,34 @@ impl IdmServerProxyReadTransaction<'_> {
                 );
             }
 
+            // Xchg token expires in
+            let expiry = ct.as_secs() + 60;
+
             // Setup for the permit success
             let xchg_code = TokenExchangeCode {
                 account_uuid,
                 session_id,
+                expiry,
                 code_challenge,
                 redirect_uri: auth_req.redirect_uri.clone(),
                 scopes: granted_scopes.into_iter().collect(),
                 nonce: auth_req.nonce.clone(),
             };
 
-            // Encrypt the exchange token with the fernet key of the client resource server
-            let code_data = serde_json::to_vec(&xchg_code).map_err(|e| {
-                admin_error!(err = ?e, "Unable to encode xchg_code data");
+            // Encrypt the exchange token with the key of the client
+            let code_data_jwe = Jwe::into_json(&xchg_code).map_err(|err| {
+                error!(?err, "Unable to encode xchg_code data");
                 Oauth2Error::ServerError(OperationError::SerdeJsonError)
             })?;
 
-            let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
+            let code = o2rs
+                .key_object
+                .jwe_a128gcm_encrypt(&code_data_jwe, ct)
+                .map(|jwe| jwe.to_string())
+                .map_err(|err| {
+                    error!(?err, "Unable to encrypt xchg_code data");
+                    Oauth2Error::ServerError(OperationError::CryptographyError)
+                })?;
 
             Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
                 redirect_uri: auth_req.redirect_uri.clone(),
@@ -2180,6 +2215,9 @@ impl IdmServerProxyReadTransaction<'_> {
                 pii_scopes.insert(OAUTH2_SCOPE_SSH_PUBLICKEYS.to_string());
             }
 
+            // Consent token expires in
+            let expiry = ct.as_secs() + 300;
+
             // Subsequent we then return an encrypted session handle which allows
             // the user to indicate their consent to this authorisation.
             //
@@ -2188,6 +2226,7 @@ impl IdmServerProxyReadTransaction<'_> {
             let consent_req = ConsentToken {
                 client_id: auth_req.client_id.clone(),
                 ident_id: ident.get_event_origin_id(),
+                expiry,
                 session_id,
                 state: auth_req.state.clone(),
                 code_challenge,
@@ -2197,16 +2236,21 @@ impl IdmServerProxyReadTransaction<'_> {
                 response_mode,
             };
 
-            let consent_data = serde_json::to_vec(&consent_req).map_err(|e| {
-                admin_error!(err = ?e, "Unable to encode consent data");
+            let consent_jwe = Jwe::into_json(&consent_req).map_err(|err| {
+                error!(?err, "Unable to encode consent data");
                 Oauth2Error::ServerError(OperationError::SerdeJsonError)
             })?;
 
             let consent_token = self
                 .oauth2rs
                 .inner
-                .fernet
-                .encrypt_at_time(&consent_data, ct.as_secs());
+                .consent_key
+                .encipher::<JweA128GCMEncipher>(&consent_jwe)
+                .map(|jwe_compact| jwe_compact.to_string())
+                .map_err(|err| {
+                    error!(?err, "Unable to encrypt jwe");
+                    Oauth2Error::ServerError(OperationError::CryptographyError)
+                })?;
 
             Ok(AuthoriseResponse::ConsentRequested {
                 client_name: o2rs.displayname.clone(),
@@ -2224,19 +2268,24 @@ impl IdmServerProxyReadTransaction<'_> {
         consent_token: &str,
         ct: Duration,
     ) -> Result<AuthoriseReject, OperationError> {
+        let jwe_compact = JweCompact::from_str(consent_token).map_err(|_| {
+            error!("Failed to deserialise a valid JWE");
+            OperationError::CryptographyError
+        })?;
+
         // Decode the consent req with our system fernet key. Use a ttl of 5 minutes.
         let consent_req: ConsentToken = self
             .oauth2rs
             .inner
-            .fernet
-            .decrypt_at_time(consent_token, Some(300), ct.as_secs())
+            .consent_key
+            .decipher(&jwe_compact)
             .map_err(|_| {
                 admin_error!("Failed to decrypt consent request");
                 OperationError::CryptographyError
             })
-            .and_then(|data| {
-                serde_json::from_slice(&data).map_err(|e| {
-                    admin_error!(err = ?e, "Failed to deserialise consent request");
+            .and_then(|jwe| {
+                jwe.from_json().map_err(|err| {
+                    error!(?err, "Failed to deserialise consent request");
                     OperationError::SerdeJsonError
                 })
             })?;
@@ -2253,6 +2302,12 @@ impl IdmServerProxyReadTransaction<'_> {
             return Err(OperationError::InvalidSessionState);
         }
 
+        if consent_req.expiry <= ct.as_secs() {
+            // Token is expired
+            error!("Failed to decrypt consent request");
+            return Err(OperationError::CryptographyError);
+        }
+
         // Get the resource server config based on this client_id.
         let _o2rs = self
             .oauth2rs
@@ -2308,24 +2363,19 @@ impl IdmServerProxyReadTransaction<'_> {
         let prefer_short_username = o2rs.prefer_short_username;
 
         if let Ok(jwsc) = JwsCompact::from_str(&intr_req.token) {
-            let access_token = match &o2rs.jws_signer {
-                Oauth2JwsSigner::ES256 { signer } => signer
-                    .get_verifier()
-                    .and_then(|verifier| verifier.verify(&jwsc)),
-                Oauth2JwsSigner::RS256 { signer } => signer
-                    .get_verifier()
-                    .and_then(|verifier| verifier.verify(&jwsc)),
-            }
-            .map_err(|err| {
-                admin_error!(?err, "Unable to verify access token");
-                Oauth2Error::InvalidRequest
-            })
-            .and_then(|jws| {
-                jws.from_json().map_err(|err| {
-                    admin_error!(?err, "Unable to deserialise access token");
+            let access_token = o2rs
+                .key_object
+                .jws_verify(&jwsc)
+                .map_err(|err| {
+                    error!(?err, "Unable to verify access token");
                     Oauth2Error::InvalidRequest
                 })
-            })?;
+                .and_then(|jws| {
+                    jws.from_json().map_err(|err| {
+                        error!(?err, "Unable to deserialise access token");
+                        Oauth2Error::InvalidRequest
+                    })
+                })?;
 
             let OAuth2RFC9068Token::<_> {
                 iss: _,
@@ -2398,16 +2448,21 @@ impl IdmServerProxyReadTransaction<'_> {
                 jti: None,
             })
         } else {
+            let jwe_compact = JweCompact::from_str(&intr_req.token).map_err(|_| {
+                error!("Failed to deserialise a valid JWE");
+                Oauth2Error::InvalidRequest
+            })?;
+
             let token: Oauth2TokenType = o2rs
-                .token_fernet
-                .decrypt(&intr_req.token)
+                .key_object
+                .jwe_decrypt(&jwe_compact)
                 .map_err(|_| {
                     admin_error!("Failed to decrypt token introspection request");
                     Oauth2Error::InvalidRequest
                 })
-                .and_then(|data| {
-                    serde_json::from_slice(&data).map_err(|e| {
-                        admin_error!("Failed to deserialise token - {:?}", e);
+                .and_then(|jwe| {
+                    jwe.from_json().map_err(|err| {
+                        error!(?err, "Failed to deserialise token");
                         Oauth2Error::InvalidRequest
                     })
                 })?;
@@ -2495,24 +2550,19 @@ impl IdmServerProxyReadTransaction<'_> {
             &*(s as *const _)
         };
 
-        let access_token = match &o2rs.jws_signer {
-            Oauth2JwsSigner::ES256 { signer } => signer
-                .get_verifier()
-                .and_then(|verifier| verifier.verify(&token)),
-            Oauth2JwsSigner::RS256 { signer } => signer
-                .get_verifier()
-                .and_then(|verifier| verifier.verify(&token)),
-        }
-        .map_err(|err| {
-            admin_error!(?err, "Unable to verify access token");
-            Oauth2Error::InvalidRequest
-        })
-        .and_then(|jws| {
-            jws.from_json().map_err(|err| {
-                admin_error!(?err, "Unable to deserialise access token");
+        let access_token = o2rs
+            .key_object
+            .jws_verify(&token)
+            .map_err(|err| {
+                error!(?err, "Unable to verify access token");
                 Oauth2Error::InvalidRequest
             })
-        })?;
+            .and_then(|jws| {
+                jws.from_json().map_err(|err| {
+                    error!(?err, "Unable to deserialise access token");
+                    Oauth2Error::InvalidRequest
+                })
+            })?;
 
         let OAuth2RFC9068Token::<_> {
             iss: _,
@@ -2685,9 +2735,9 @@ impl IdmServerProxyReadTransaction<'_> {
 
         let subject_types_supported = vec![SubjectType::Public];
 
-        let id_token_signing_alg_values_supported = match &o2rs.jws_signer {
-            Oauth2JwsSigner::ES256 { .. } => vec![IdTokenSignAlg::ES256],
-            Oauth2JwsSigner::RS256 { .. } => vec![IdTokenSignAlg::RS256],
+        let id_token_signing_alg_values_supported = match &o2rs.sign_alg {
+            SignatureAlgo::Es256 => vec![IdTokenSignAlg::ES256],
+            SignatureAlgo::Rs256 => vec![IdTokenSignAlg::RS256],
         };
 
         let userinfo_signing_alg_values_supported = None;
@@ -2818,15 +2868,18 @@ impl IdmServerProxyReadTransaction<'_> {
             OperationError::NoMatchingEntries
         })?;
 
-        match &o2rs.jws_signer {
-            Oauth2JwsSigner::ES256 { signer } => signer.public_key_as_jwk(),
-            Oauth2JwsSigner::RS256 { signer } => signer.public_key_as_jwk(),
+        // How do we return only the active signing algo types?
+
+        error!(sign_alg = ?o2rs.sign_alg);
+
+        match o2rs.sign_alg {
+            SignatureAlgo::Es256 => o2rs.key_object.jws_es256_jwks(),
+            SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_jwks(),
         }
-        .map_err(|e| {
-            admin_error!("Unable to retrieve public key for {} - {:?}", o2rs.name, e);
+        .ok_or_else(|| {
+            error!(o2_client = ?o2rs.name, "Unable to retrieve public keys");
             OperationError::InvalidState
         })
-        .map(|jwk| JwkKeySet { keys: vec![jwk] })
     }
 }
 
@@ -4699,11 +4752,11 @@ mod tests {
         );
 
         // Invalid consent token
-        assert!(
+        assert_eq!(
             idms_prox_read
                 .check_oauth2_authorise_reject(&ident, "not a token", ct)
-                .unwrap_err()
-                == OperationError::CryptographyError
+                .unwrap_err(),
+            OperationError::CryptographyError
         );
 
         // Wrong ident
diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs
index d047bb319..911150bfb 100644
--- a/server/lib/src/idm/server.rs
+++ b/server/lib/src/idm/server.rs
@@ -1,28 +1,3 @@
-use std::convert::TryFrom;
-use std::sync::Arc;
-use std::time::Duration;
-
-use kanidm_lib_crypto::CryptoPolicy;
-
-use compact_jwt::{Jwk, JwsCompact};
-use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
-use concread::cowcell::CowCellReadTxn;
-use concread::hashmap::HashMap;
-use kanidm_proto::internal::{
-    ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken,
-    UatPurpose, UserAuthToken,
-};
-use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
-use rand::prelude::*;
-use tokio::sync::mpsc::{
-    unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender,
-};
-use tokio::sync::{Mutex, Semaphore};
-use tracing::trace;
-use url::Url;
-use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};
-use zxcvbn::{zxcvbn, Score};
-
 use super::event::ReadBackupCodeEvent;
 use super::ldap::{LdapBoundToken, LdapSession};
 use crate::credential::{softlock::CredSoftLock, Credential};
@@ -38,9 +13,6 @@ use crate::idm::delayed::{
     AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, UnixPasswordUpgrade,
     WebauthnCounterIncrement,
 };
-
-#[cfg(test)]
-use crate::idm::event::PasswordChangeEvent;
 use crate::idm::event::{AuthEvent, AuthEventStep, AuthResult};
 use crate::idm::event::{
     CredentialStatusEvent, LdapAuthEvent, LdapTokenAuthEvent, RadiusAuthTokenEvent,
@@ -61,6 +33,31 @@ use crate::server::keys::KeyProvidersTransaction;
 use crate::server::DomainInfo;
 use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
 use crate::value::{Session, SessionState};
+use compact_jwt::{Jwk, JwsCompact};
+use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
+use concread::cowcell::CowCellReadTxn;
+use concread::hashmap::HashMap;
+use kanidm_lib_crypto::CryptoPolicy;
+use kanidm_proto::internal::{
+    ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken,
+    UatPurpose, UserAuthToken,
+};
+use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
+use rand::prelude::*;
+use std::convert::TryFrom;
+use std::sync::Arc;
+use std::time::Duration;
+use tokio::sync::mpsc::{
+    unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender,
+};
+use tokio::sync::{Mutex, Semaphore};
+use tracing::trace;
+use url::Url;
+use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};
+use zxcvbn::{zxcvbn, Score};
+
+#[cfg(test)]
+use crate::idm::event::PasswordChangeEvent;
 
 pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
 pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
@@ -158,14 +155,12 @@ impl IdmServer {
         let (audit_tx, audit_rx) = unbounded();
 
         // Get the domain name, as the relying party id.
-        let (rp_id, rp_name, domain_level, oauth2rs_set, application_set) = {
+        let (rp_id, rp_name, application_set) = {
             let mut qs_read = qs.read().await?;
             (
                 qs_read.get_domain_name().to_string(),
                 qs_read.get_domain_display_name().to_string(),
-                qs_read.get_domain_version(),
                 // Add a read/reload of all oauth2 configurations.
-                qs_read.get_oauth2rs_set()?,
                 qs_read.get_applications_set()?,
             )
         };
@@ -201,11 +196,10 @@ impl IdmServer {
                 OperationError::InvalidState
             })?;
 
-        let oauth2rs = Oauth2ResourceServers::try_from((oauth2rs_set, origin_url, domain_level))
-            .map_err(|e| {
-                admin_error!("Failed to load oauth2 resource servers - {:?}", e);
-                e
-            })?;
+        let oauth2rs = Oauth2ResourceServers::new(origin_url).map_err(|err| {
+            error!(?err, "Failed to load oauth2 resource servers");
+            err
+        })?;
 
         let applications = LdapApplications::try_from(application_set).map_err(|e| {
             admin_error!("Failed to load ldap applications - {:?}", e);
@@ -2121,6 +2115,12 @@ impl IdmServerProxyWriteTransaction<'_> {
 
     #[instrument(level = "debug", skip_all)]
     pub fn commit(mut self) -> Result<(), OperationError> {
+        // The problem we have here is that we need the qs_write layer to reload *first*
+        // so that things like schema and key objects are ready.
+        self.qs_write.reload()?;
+
+        // Now that's done, let's proceed.
+
         if self.qs_write.get_changed_app() {
             self.qs_write
                 .get_applications_set()
@@ -2128,9 +2128,11 @@ impl IdmServerProxyWriteTransaction<'_> {
         }
         if self.qs_write.get_changed_oauth2() {
             let domain_level = self.qs_write.get_domain_version();
-            self.qs_write
-                .get_oauth2rs_set()
-                .and_then(|oauth2rs_set| self.oauth2rs.reload(oauth2rs_set, domain_level))?;
+            self.qs_write.get_oauth2rs_set().and_then(|oauth2rs_set| {
+                let key_providers = self.qs_write.get_key_providers();
+                self.oauth2rs
+                    .reload(oauth2rs_set, key_providers, domain_level)
+            })?;
             // Clear the flag to indicate we completed the reload.
             self.qs_write.clear_changed_oauth2();
         }
diff --git a/server/lib/src/migration_data/dl10/access.rs b/server/lib/src/migration_data/dl10/access.rs
index 3e56613fc..c1b7da233 100644
--- a/server/lib/src/migration_data/dl10/access.rs
+++ b/server/lib/src/migration_data/dl10/access.rs
@@ -614,315 +614,7 @@ lazy_static! {
 }
 
 lazy_static! {
-    pub static ref IDM_ACP_OAUTH2_MANAGE_DL4: BuiltinAcp = BuiltinAcp {
-        classes: vec![
-            EntryClass::Object,
-            EntryClass::AccessControlProfile,
-            EntryClass::AccessControlCreate,
-            EntryClass::AccessControlDelete,
-            EntryClass::AccessControlModify,
-            EntryClass::AccessControlSearch
-        ],
-        name: "idm_acp_hp_oauth2_manage_priv",
-        uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1,
-        description: "Builtin IDM Control for managing oauth2 resource server integrations.",
-        receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]),
-        target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
-            match_class_filter!(EntryClass::OAuth2ResourceServer),
-            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(),
-        ])),
-        search_attrs: vec![
-            Attribute::Class,
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::OAuth2RsName,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        modify_removed_attrs: vec![
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::OAuth2RsName,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        modify_present_attrs: vec![
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::OAuth2RsName,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        create_attrs: vec![
-            Attribute::Class,
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::OAuth2RsName,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        create_classes: vec![
-            EntryClass::Object,
-            EntryClass::OAuth2ResourceServer,
-            EntryClass::OAuth2ResourceServerBasic,
-            EntryClass::OAuth2ResourceServerPublic,
-        ],
-        ..Default::default()
-    };
-}
-
-lazy_static! {
-    pub static ref IDM_ACP_OAUTH2_MANAGE_DL5: BuiltinAcp = BuiltinAcp {
-        classes: vec![
-            EntryClass::Object,
-            EntryClass::AccessControlProfile,
-            EntryClass::AccessControlCreate,
-            EntryClass::AccessControlDelete,
-            EntryClass::AccessControlModify,
-            EntryClass::AccessControlSearch
-        ],
-        name: "idm_acp_hp_oauth2_manage_priv",
-        uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1,
-        description: "Builtin IDM Control for managing oauth2 resource server integrations.",
-        receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]),
-        target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
-            match_class_filter!(EntryClass::OAuth2ResourceServer),
-            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(),
-        ])),
-        search_attrs: vec![
-            Attribute::Class,
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::Name,
-            Attribute::Spn,
-            Attribute::OAuth2Session,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        modify_removed_attrs: vec![
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::Name,
-            Attribute::OAuth2Session,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        modify_present_attrs: vec![
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::Name,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        create_attrs: vec![
-            Attribute::Class,
-            Attribute::Description,
-            Attribute::Name,
-            Attribute::DisplayName,
-            Attribute::OAuth2RsName,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-        ],
-        create_classes: vec![
-            EntryClass::Object,
-            EntryClass::Account,
-            EntryClass::OAuth2ResourceServer,
-            EntryClass::OAuth2ResourceServerBasic,
-            EntryClass::OAuth2ResourceServerPublic,
-        ],
-        ..Default::default()
-    };
-}
-
-lazy_static! {
-    pub static ref IDM_ACP_OAUTH2_MANAGE_DL7: BuiltinAcp = BuiltinAcp {
-        classes: vec![
-            EntryClass::Object,
-            EntryClass::AccessControlProfile,
-            EntryClass::AccessControlCreate,
-            EntryClass::AccessControlDelete,
-            EntryClass::AccessControlModify,
-            EntryClass::AccessControlSearch
-        ],
-        name: "idm_acp_hp_oauth2_manage_priv",
-        uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1,
-        description: "Builtin IDM Control for managing oauth2 resource server integrations.",
-        receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]),
-        target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
-            match_class_filter!(EntryClass::OAuth2ResourceServer),
-            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(),
-        ])),
-        search_attrs: vec![
-            Attribute::Class,
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::Name,
-            Attribute::Spn,
-            Attribute::OAuth2Session,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-            Attribute::OAuth2StrictRedirectUri,
-        ],
-        modify_removed_attrs: vec![
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::Name,
-            Attribute::OAuth2Session,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-            Attribute::OAuth2StrictRedirectUri,
-        ],
-        modify_present_attrs: vec![
-            Attribute::Description,
-            Attribute::DisplayName,
-            Attribute::Name,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-            Attribute::OAuth2StrictRedirectUri,
-        ],
-        create_attrs: vec![
-            Attribute::Class,
-            Attribute::Description,
-            Attribute::Name,
-            Attribute::DisplayName,
-            Attribute::OAuth2RsName,
-            Attribute::OAuth2RsOrigin,
-            Attribute::OAuth2RsOriginLanding,
-            Attribute::OAuth2RsSupScopeMap,
-            Attribute::OAuth2RsScopeMap,
-            Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::OAuth2JwtLegacyCryptoEnable,
-            Attribute::OAuth2PreferShortUsername,
-            Attribute::OAuth2AllowLocalhostRedirect,
-            Attribute::OAuth2RsClaimMap,
-            Attribute::Image,
-            Attribute::OAuth2StrictRedirectUri,
-        ],
-        create_classes: vec![
-            EntryClass::Object,
-            EntryClass::Account,
-            EntryClass::OAuth2ResourceServer,
-            EntryClass::OAuth2ResourceServerBasic,
-            EntryClass::OAuth2ResourceServerPublic,
-        ],
-        ..Default::default()
-    };
-}
-
-lazy_static! {
-    pub static ref IDM_ACP_OAUTH2_MANAGE_DL9: BuiltinAcp = BuiltinAcp {
+    pub static ref IDM_ACP_OAUTH2_MANAGE: BuiltinAcp = BuiltinAcp {
         classes: vec![
             EntryClass::Object,
             EntryClass::AccessControlProfile,
@@ -951,10 +643,7 @@ lazy_static! {
             Attribute::OAuth2RsScopeMap,
             Attribute::OAuth2RsSupScopeMap,
             Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
             Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
             Attribute::OAuth2JwtLegacyCryptoEnable,
             Attribute::OAuth2PreferShortUsername,
             Attribute::OAuth2AllowLocalhostRedirect,
@@ -962,6 +651,7 @@ lazy_static! {
             Attribute::Image,
             Attribute::OAuth2StrictRedirectUri,
             Attribute::OAuth2DeviceFlowEnable,
+            Attribute::KeyInternalData,
         ],
         modify_removed_attrs: vec![
             Attribute::Description,
@@ -973,10 +663,7 @@ lazy_static! {
             Attribute::OAuth2RsScopeMap,
             Attribute::OAuth2RsSupScopeMap,
             Attribute::OAuth2RsBasicSecret,
-            Attribute::OAuth2RsTokenKey,
-            Attribute::Es256PrivateKeyDer,
             Attribute::OAuth2AllowInsecureClientDisablePkce,
-            Attribute::Rs256PrivateKeyDer,
             Attribute::OAuth2JwtLegacyCryptoEnable,
             Attribute::OAuth2PreferShortUsername,
             Attribute::OAuth2AllowLocalhostRedirect,
@@ -984,6 +671,8 @@ lazy_static! {
             Attribute::Image,
             Attribute::OAuth2StrictRedirectUri,
             Attribute::OAuth2DeviceFlowEnable,
+            Attribute::KeyActionRevoke,
+            Attribute::KeyActionRotate,
         ],
         modify_present_attrs: vec![
             Attribute::Description,
@@ -1001,6 +690,8 @@ lazy_static! {
             Attribute::Image,
             Attribute::OAuth2StrictRedirectUri,
             Attribute::OAuth2DeviceFlowEnable,
+            Attribute::KeyActionRevoke,
+            Attribute::KeyActionRotate,
         ],
         create_attrs: vec![
             Attribute::Class,
@@ -1032,122 +723,6 @@ lazy_static! {
     };
 }
 
-lazy_static! {
-    pub static ref IDM_ACP_DOMAIN_ADMIN_DL6: BuiltinAcp = BuiltinAcp {
-        classes: vec![
-            EntryClass::Object,
-            EntryClass::AccessControlProfile,
-            EntryClass::AccessControlModify,
-            EntryClass::AccessControlSearch
-        ],
-        name: "idm_acp_domain_admin",
-        uuid: UUID_IDM_ACP_DOMAIN_ADMIN_V1,
-        description: "Builtin IDM Control for granting domain info administration locally",
-        receiver: BuiltinAcpReceiver::Group(vec![UUID_DOMAIN_ADMINS]),
-        target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
-            ProtoFilter::Eq(
-                Attribute::Uuid.to_string(),
-                STR_UUID_DOMAIN_INFO.to_string()
-            ),
-            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
-        ])),
-        search_attrs: vec![
-            Attribute::Class,
-            Attribute::Name,
-            Attribute::Uuid,
-            Attribute::DomainDisplayName,
-            Attribute::DomainName,
-            Attribute::DomainLdapBasedn,
-            Attribute::LdapMaxQueryableAttrs,
-            Attribute::DomainSsid,
-            Attribute::DomainUuid,
-            // Grants read access to the key object.
-            // But this means we have to specify every type of key object?
-            // Future william problem ...
-            Attribute::KeyInternalData,
-            Attribute::LdapAllowUnixPwBind,
-            Attribute::Version,
-        ],
-        modify_removed_attrs: vec![
-            Attribute::DomainDisplayName,
-            Attribute::DomainSsid,
-            Attribute::DomainLdapBasedn,
-            Attribute::LdapMaxQueryableAttrs,
-            Attribute::LdapAllowUnixPwBind,
-            Attribute::KeyActionRevoke,
-            Attribute::KeyActionRotate,
-        ],
-        modify_present_attrs: vec![
-            Attribute::DomainDisplayName,
-            Attribute::DomainLdapBasedn,
-            Attribute::LdapMaxQueryableAttrs,
-            Attribute::DomainSsid,
-            Attribute::LdapAllowUnixPwBind,
-            Attribute::KeyActionRevoke,
-            Attribute::KeyActionRotate,
-        ],
-        ..Default::default()
-    };
-}
-
-lazy_static! {
-    pub static ref IDM_ACP_DOMAIN_ADMIN_DL8: BuiltinAcp = BuiltinAcp {
-        classes: vec![
-            EntryClass::Object,
-            EntryClass::AccessControlProfile,
-            EntryClass::AccessControlModify,
-            EntryClass::AccessControlSearch
-        ],
-        name: "idm_acp_domain_admin",
-        uuid: UUID_IDM_ACP_DOMAIN_ADMIN_V1,
-        description: "Builtin IDM Control for granting domain info administration locally",
-        receiver: BuiltinAcpReceiver::Group(vec![UUID_DOMAIN_ADMINS]),
-        target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
-            ProtoFilter::Eq(
-                Attribute::Uuid.to_string(),
-                STR_UUID_DOMAIN_INFO.to_string()
-            ),
-            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
-        ])),
-        search_attrs: vec![
-            Attribute::Class,
-            Attribute::Name,
-            Attribute::Uuid,
-            Attribute::DomainDisplayName,
-            Attribute::DomainName,
-            Attribute::DomainLdapBasedn,
-            Attribute::LdapMaxQueryableAttrs,
-            Attribute::DomainSsid,
-            Attribute::DomainUuid,
-            Attribute::KeyInternalData,
-            Attribute::LdapAllowUnixPwBind,
-            Attribute::Version,
-            Attribute::Image,
-        ],
-        modify_removed_attrs: vec![
-            Attribute::DomainDisplayName,
-            Attribute::DomainSsid,
-            Attribute::DomainLdapBasedn,
-            Attribute::LdapMaxQueryableAttrs,
-            Attribute::LdapAllowUnixPwBind,
-            Attribute::KeyActionRevoke,
-            Attribute::KeyActionRotate,
-            Attribute::Image,
-        ],
-        modify_present_attrs: vec![
-            Attribute::DomainDisplayName,
-            Attribute::DomainLdapBasedn,
-            Attribute::LdapMaxQueryableAttrs,
-            Attribute::DomainSsid,
-            Attribute::LdapAllowUnixPwBind,
-            Attribute::KeyActionRevoke,
-            Attribute::KeyActionRotate,
-            Attribute::Image,
-        ],
-        ..Default::default()
-    };
-}
-
 lazy_static! {
     pub static ref IDM_ACP_DOMAIN_ADMIN_DL9: BuiltinAcp = BuiltinAcp {
         classes: vec![
diff --git a/server/lib/src/migration_data/dl10/mod.rs b/server/lib/src/migration_data/dl10/mod.rs
index 8eac91720..483493b5b 100644
--- a/server/lib/src/migration_data/dl10/mod.rs
+++ b/server/lib/src/migration_data/dl10/mod.rs
@@ -105,6 +105,7 @@ pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> {
         // DL10
         SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(),
         SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(),
+        SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256_DL6.clone().into(),
     ]
 }
 
@@ -140,6 +141,7 @@ pub fn phase_2_schema_classes() -> Vec<EntryInitNew> {
         SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(),
         // DL10
         SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into(),
+        SCHEMA_CLASS_KEY_OBJECT_JWT_RS256.clone().into(),
     ]
 }
 
@@ -260,8 +262,9 @@ pub fn phase_7_builtin_access_control_profiles() -> Vec<EntryInitNew> {
         IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
         IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
         // DL9
-        IDM_ACP_OAUTH2_MANAGE_DL9.clone().into(),
         IDM_ACP_GROUP_MANAGE_DL9.clone().into(),
         IDM_ACP_DOMAIN_ADMIN_DL9.clone().into(),
+        // DL10
+        IDM_ACP_OAUTH2_MANAGE.clone().into(),
     ]
 }
diff --git a/server/lib/src/migration_data/dl10/schema.rs b/server/lib/src/migration_data/dl10/schema.rs
index 5117f7121..ec562d6db 100644
--- a/server/lib/src/migration_data/dl10/schema.rs
+++ b/server/lib/src/migration_data/dl10/schema.rs
@@ -657,6 +657,17 @@ pub static ref SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6: SchemaAttribute = Sc
     ..Default::default()
 };
 
+pub static ref SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256_DL6: SchemaAttribute = SchemaAttribute {
+    uuid: UUID_SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256,
+    name: Attribute::KeyActionImportJwsRs256,
+    description: "".to_string(),
+    multivalue: true,
+    // Ephemeral action.
+    phantom: true,
+    syntax: SyntaxType::PrivateBinary,
+    ..Default::default()
+};
+
 pub static ref SCHEMA_ATTR_PATCH_LEVEL_DL7: SchemaAttribute = SchemaAttribute {
     uuid: UUID_SCHEMA_ATTR_PATCH_LEVEL,
     name: Attribute::PatchLevel,
@@ -948,13 +959,12 @@ pub static ref SCHEMA_CLASS_SYSTEM_CONFIG: SchemaClass = SchemaClass {
 pub static ref SCHEMA_CLASS_OAUTH2_RS_DL9: SchemaClass = SchemaClass {
     uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
     name: EntryClass::OAuth2ResourceServer.into(),
-    description: "The class representing a configured OAuth2 Client".to_string(),
+    description: "The class epresenting a configured OAuth2 Client".to_string(),
 
     systemmay: vec![
         Attribute::Description,
         Attribute::OAuth2RsScopeMap,
         Attribute::OAuth2RsSupScopeMap,
-        Attribute::Rs256PrivateKeyDer,
         Attribute::OAuth2JwtLegacyCryptoEnable,
         Attribute::OAuth2PreferShortUsername,
         Attribute::Image,
@@ -963,11 +973,13 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_DL9: SchemaClass = SchemaClass {
         Attribute::OAuth2RsOrigin,
         Attribute::OAuth2StrictRedirectUri,
         Attribute::OAuth2DeviceFlowEnable,
+        // Deprecated
+        Attribute::Rs256PrivateKeyDer,
+        Attribute::OAuth2RsTokenKey,
+        Attribute::Es256PrivateKeyDer,
     ],
     systemmust: vec![
         Attribute::OAuth2RsOriginLanding,
-        Attribute::OAuth2RsTokenKey,
-        Attribute::Es256PrivateKeyDer,
     ],
     ..Default::default()
 };
@@ -1045,6 +1057,16 @@ pub static ref SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6: SchemaClass = SchemaClass
     ..Default::default()
 };
 
+pub static ref SCHEMA_CLASS_KEY_OBJECT_JWT_RS256: SchemaClass = SchemaClass {
+    uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_JWT_RS256,
+    name: EntryClass::KeyObjectJwtRs256.into(),
+    description: "A marker class indicating that this keyobject must provide jwt rs256 capability.".to_string(),
+    systemsupplements: vec![
+        EntryClass::KeyObject.into(),
+    ],
+    ..Default::default()
+};
+
 pub static ref SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6: SchemaClass = SchemaClass {
     uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM,
     name: EntryClass::KeyObjectJweA128GCM.into(),
diff --git a/server/lib/src/plugins/keyobject.rs b/server/lib/src/plugins/keyobject.rs
index b24f8cbff..d8ba04941 100644
--- a/server/lib/src/plugins/keyobject.rs
+++ b/server/lib/src/plugins/keyobject.rs
@@ -135,11 +135,13 @@ impl KeyObjectManagement {
         qs: &mut QueryServerWriteTransaction,
         cand: &mut [Entry<EntryInvalid, T>],
     ) -> Result<(), OperationError> {
-        // Valid from right meow!
+        // New keys will be valid from right meow!
         let valid_from = qs.get_curtime();
         let txn_cid = qs.get_cid().clone();
-
         let key_providers = qs.get_key_providers_mut();
+        // ====================================================================
+        // Transform any found KeyObjects and manage any related key operations
+        // for them
 
         cand.iter_mut()
             .filter(|entry| {
@@ -172,6 +174,14 @@ impl KeyObjectManagement {
                     key_object.jws_es256_import(import_keys, valid_from, &txn_cid)?;
                 }
 
+                let maybe_import = entry.pop_ava(Attribute::KeyActionImportJwsRs256);
+                if let Some(import_keys) = maybe_import
+                    .as_ref()
+                    .and_then(|vs| vs.as_private_binary_set())
+                {
+                    key_object.jws_rs256_import(import_keys, valid_from, &txn_cid)?;
+                }
+
                 // If revoke. This weird looking let dance is to ensure that the inner hexstring set
                 // lives long enough.
                 let maybe_revoked = entry.pop_ava(Attribute::KeyActionRevoke);
@@ -208,6 +218,11 @@ impl KeyObjectManagement {
                     key_object.jws_es256_assert(Duration::ZERO, &txn_cid)?;
                 }
 
+                if entry.attribute_equality(Attribute::Class, &EntryClass::KeyObjectJwtRs256.into())
+                {
+                    key_object.jws_rs256_assert(Duration::ZERO, &txn_cid)?;
+                }
+
                 if entry
                     .attribute_equality(Attribute::Class, &EntryClass::KeyObjectJweA128GCM.into())
                 {
diff --git a/server/lib/src/plugins/mod.rs b/server/lib/src/plugins/mod.rs
index 87668467c..a9d00c2f8 100644
--- a/server/lib/src/plugins/mod.rs
+++ b/server/lib/src/plugins/mod.rs
@@ -18,10 +18,10 @@ mod domain;
 pub(crate) mod dyngroup;
 mod eckeygen;
 pub(crate) mod gidnumber;
-mod jwskeygen;
 mod keyobject;
 mod memberof;
 mod namehistory;
+mod oauth2;
 mod refint;
 mod session;
 mod spn;
@@ -230,15 +230,17 @@ impl Plugins {
     ) -> Result<(), OperationError> {
         base::Base::pre_create_transform(qs, cand, ce)?;
         valuedeny::ValueDeny::pre_create_transform(qs, cand, ce)?;
-        cred_import::CredImport::pre_create_transform(qs, cand, ce)?;
+
+        oauth2::OAuth2::pre_create_transform(qs, cand, ce)?;
+        eckeygen::EcdhKeyGen::pre_create_transform(qs, cand, ce)?;
         keyobject::KeyObjectManagement::pre_create_transform(qs, cand, ce)?;
-        jwskeygen::JwsKeygen::pre_create_transform(qs, cand, ce)?;
+        cred_import::CredImport::pre_create_transform(qs, cand, ce)?;
+
         gidnumber::GidNumber::pre_create_transform(qs, cand, ce)?;
         domain::Domain::pre_create_transform(qs, cand, ce)?;
         spn::Spn::pre_create_transform(qs, cand, ce)?;
         default_values::DefaultValues::pre_create_transform(qs, cand, ce)?;
         namehistory::NameHistory::pre_create_transform(qs, cand, ce)?;
-        eckeygen::EcdhKeyGen::pre_create_transform(qs, cand, ce)?;
         // Should always be last
         attrunique::AttrUnique::pre_create_transform(qs, cand, ce)
     }
@@ -271,16 +273,18 @@ impl Plugins {
     ) -> Result<(), OperationError> {
         base::Base::pre_modify(qs, pre_cand, cand, me)?;
         valuedeny::ValueDeny::pre_modify(qs, pre_cand, cand, me)?;
-        cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?;
-        jwskeygen::JwsKeygen::pre_modify(qs, pre_cand, cand, me)?;
+
+        oauth2::OAuth2::pre_modify(qs, pre_cand, cand, me)?;
+        eckeygen::EcdhKeyGen::pre_modify(qs, pre_cand, cand, me)?;
         keyobject::KeyObjectManagement::pre_modify(qs, pre_cand, cand, me)?;
+        cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?;
+
         gidnumber::GidNumber::pre_modify(qs, pre_cand, cand, me)?;
         domain::Domain::pre_modify(qs, pre_cand, cand, me)?;
         spn::Spn::pre_modify(qs, pre_cand, cand, me)?;
         session::SessionConsistency::pre_modify(qs, pre_cand, cand, me)?;
         default_values::DefaultValues::pre_modify(qs, pre_cand, cand, me)?;
         namehistory::NameHistory::pre_modify(qs, pre_cand, cand, me)?;
-        eckeygen::EcdhKeyGen::pre_modify(qs, pre_cand, cand, me)?;
         // attr unique should always be last
         attrunique::AttrUnique::pre_modify(qs, pre_cand, cand, me)
     }
@@ -306,16 +310,18 @@ impl Plugins {
     ) -> Result<(), OperationError> {
         base::Base::pre_batch_modify(qs, pre_cand, cand, me)?;
         valuedeny::ValueDeny::pre_batch_modify(qs, pre_cand, cand, me)?;
-        cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?;
-        jwskeygen::JwsKeygen::pre_batch_modify(qs, pre_cand, cand, me)?;
+
+        oauth2::OAuth2::pre_batch_modify(qs, pre_cand, cand, me)?;
+        eckeygen::EcdhKeyGen::pre_batch_modify(qs, pre_cand, cand, me)?;
         keyobject::KeyObjectManagement::pre_batch_modify(qs, pre_cand, cand, me)?;
+        cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?;
+
         gidnumber::GidNumber::pre_batch_modify(qs, pre_cand, cand, me)?;
         domain::Domain::pre_batch_modify(qs, pre_cand, cand, me)?;
         spn::Spn::pre_batch_modify(qs, pre_cand, cand, me)?;
         session::SessionConsistency::pre_batch_modify(qs, pre_cand, cand, me)?;
         default_values::DefaultValues::pre_batch_modify(qs, pre_cand, cand, me)?;
         namehistory::NameHistory::pre_batch_modify(qs, pre_cand, cand, me)?;
-        eckeygen::EcdhKeyGen::pre_batch_modify(qs, pre_cand, cand, me)?;
         // attr unique should always be last
         attrunique::AttrUnique::pre_batch_modify(qs, pre_cand, cand, me)
     }
diff --git a/server/lib/src/plugins/jwskeygen.rs b/server/lib/src/plugins/oauth2.rs
similarity index 59%
rename from server/lib/src/plugins/jwskeygen.rs
rename to server/lib/src/plugins/oauth2.rs
index 2221eef35..e297772e8 100644
--- a/server/lib/src/plugins/jwskeygen.rs
+++ b/server/lib/src/plugins/oauth2.rs
@@ -6,14 +6,14 @@ use crate::plugins::Plugin;
 use crate::prelude::*;
 use crate::utils::password_from_random;
 
-pub struct JwsKeygen {}
+pub struct OAuth2 {}
 
-impl Plugin for JwsKeygen {
+impl Plugin for OAuth2 {
     fn id() -> &'static str {
-        "plugin_jws_keygen"
+        "plugin_oauth2"
     }
 
-    #[instrument(level = "debug", name = "jwskeygen_pre_create_transform", skip_all)]
+    #[instrument(level = "debug", name = "oauth2_pre_create_transform", skip_all)]
     fn pre_create_transform(
         qs: &mut QueryServerWriteTransaction,
         cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
@@ -22,7 +22,7 @@ impl Plugin for JwsKeygen {
         Self::modify_inner(qs, cand)
     }
 
-    #[instrument(level = "debug", name = "jwskeygen_pre_modify", skip_all)]
+    #[instrument(level = "debug", name = "oauth2_pre_modify", skip_all)]
     fn pre_modify(
         qs: &mut QueryServerWriteTransaction,
         _pre_cand: &[Arc<EntrySealedCommitted>],
@@ -32,7 +32,7 @@ impl Plugin for JwsKeygen {
         Self::modify_inner(qs, cand)
     }
 
-    #[instrument(level = "debug", name = "jwskeygen_pre_batch_modify", skip_all)]
+    #[instrument(level = "debug", name = "oauth2_pre_batch_modify", skip_all)]
     fn pre_batch_modify(
         qs: &mut QueryServerWriteTransaction,
         _pre_cand: &[Arc<EntrySealedCommitted>],
@@ -43,52 +43,69 @@ impl Plugin for JwsKeygen {
     }
 }
 
-impl JwsKeygen {
+impl OAuth2 {
     fn modify_inner<T: Clone>(
-        _qs: &mut QueryServerWriteTransaction,
+        qs: &mut QueryServerWriteTransaction,
         cand: &mut [Entry<EntryInvalid, T>],
     ) -> Result<(), OperationError> {
-        cand.iter_mut().try_for_each(|e| {
-        if e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) &&
-            !e.attribute_pres(Attribute::OAuth2RsBasicSecret) {
-                security_info!("regenerating oauth2 basic secret");
-                let v = Value::SecretValue(password_from_random());
-                e.add_ava(Attribute::OAuth2RsBasicSecret, v);
-        }
+        let domain_level = qs.get_domain_version();
 
-        if e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) {
-            if !e.attribute_pres(Attribute::OAuth2RsTokenKey) {
-                security_info!("regenerating oauth2 token key");
-                let k = fernet::Fernet::generate_key();
-                let v = Value::new_secret_str(&k);
-                e.add_ava(Attribute::OAuth2RsTokenKey, v);
-            }
-            if !e.attribute_pres(Attribute::Es256PrivateKeyDer) {
-                security_info!("regenerating oauth2 es256 private key");
-                let der = JwsEs256Signer::generate_es256()
-                    .and_then(|jws| jws.private_key_to_der())
-                    .map_err(|e| {
-                        admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key");
-                        OperationError::CryptographyError
-                    })?;
-                let v = Value::new_privatebinary(&der);
-                e.add_ava(Attribute::Es256PrivateKeyDer, v);
-            }
-            if e.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false)
-                && !e.attribute_pres(Attribute::Rs256PrivateKeyDer) {
-                security_info!("regenerating oauth2 legacy rs256 private key");
-                let der = JwsRs256Signer::generate_legacy_rs256()
-                    .and_then(|jws| jws.private_key_to_der())
-                    .map_err(|e| {
-                        admin_error!(err = ?e, "Unable to generate Legacy RS256 JwsSigner private key");
-                        OperationError::CryptographyError
-                    })?;
-                let v = Value::new_privatebinary(&der);
-                e.add_ava(Attribute::Rs256PrivateKeyDer, v);
-            }
-        }
+        cand.iter_mut()
+            .filter(|entry| {
+                entry.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
+            })
+            .try_for_each(|entry| {
+                // Regenerate the basic secret, if needed
+                if entry.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) &&
+                    !entry.attribute_pres(Attribute::OAuth2RsBasicSecret) {
+                        security_info!("regenerating oauth2 basic secret");
+                        let v = Value::SecretValue(password_from_random());
+                        entry.add_ava(Attribute::OAuth2RsBasicSecret, v);
+                }
 
-        Ok(())
+            let has_rs256 = entry.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false);
+
+            if domain_level >= DOMAIN_LEVEL_10 {
+                debug!("Generating OAuth2 Key Object");
+                // OAuth2 now requires a KeyObject, configure it now.
+                entry.add_ava(Attribute::Class, EntryClass::KeyObject.to_value());
+                entry.add_ava(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value());
+                entry.add_ava(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value());
+                if has_rs256 {
+                    entry.add_ava(Attribute::Class, EntryClass::KeyObjectJwtRs256.to_value());
+                }
+            } else {
+                if !entry.attribute_pres(Attribute::OAuth2RsTokenKey) {
+                    security_info!("regenerating oauth2 token key");
+                    let k = password_from_random();
+                    let v = Value::new_secret_str(&k);
+                    entry.add_ava(Attribute::OAuth2RsTokenKey, v);
+                }
+                if !entry.attribute_pres(Attribute::Es256PrivateKeyDer) {
+                    security_info!("regenerating oauth2 es256 private key");
+                    let der = JwsEs256Signer::generate_es256()
+                        .and_then(|jws| jws.private_key_to_der())
+                        .map_err(|e| {
+                            admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key");
+                            OperationError::CryptographyError
+                        })?;
+                    let v = Value::new_privatebinary(&der);
+                    entry.add_ava(Attribute::Es256PrivateKeyDer, v);
+                }
+                    if has_rs256 && !entry.attribute_pres(Attribute::Rs256PrivateKeyDer) {
+                    security_info!("regenerating oauth2 legacy rs256 private key");
+                    let der = JwsRs256Signer::generate_legacy_rs256()
+                        .and_then(|jws| jws.private_key_to_der())
+                        .map_err(|e| {
+                            admin_error!(err = ?e, "Unable to generate Legacy RS256 JwsSigner private key");
+                            OperationError::CryptographyError
+                        })?;
+                    let v = Value::new_privatebinary(&der);
+                    entry.add_ava(Attribute::Rs256PrivateKeyDer, v);
+                }
+            }
+
+            Ok(())
         })
     }
 }
@@ -145,7 +162,6 @@ mod tests {
                     .internal_search_uuid(uuid)
                     .expect("failed to get oauth2 config");
                 assert!(e.attribute_pres(Attribute::OAuth2RsBasicSecret));
-                assert!(e.attribute_pres(Attribute::OAuth2RsTokenKey));
             }
         );
     }
@@ -186,8 +202,7 @@ mod tests {
             (
                 Attribute::OAuth2RsBasicSecret,
                 Value::new_secret_str("12345")
-            ),
-            (Attribute::OAuth2RsTokenKey, Value::new_secret_str("12345"))
+            )
         );
 
         let preload = vec![e];
@@ -207,10 +222,8 @@ mod tests {
                     .internal_search_uuid(uuid)
                     .expect("failed to get oauth2 config");
                 assert!(e.attribute_pres(Attribute::OAuth2RsBasicSecret));
-                assert!(e.attribute_pres(Attribute::OAuth2RsTokenKey));
                 // Check the values are different.
                 assert!(e.get_ava_single_secret(Attribute::OAuth2RsBasicSecret) != Some("12345"));
-                assert!(e.get_ava_single_secret(Attribute::OAuth2RsTokenKey) != Some("12345"));
             }
         );
     }
diff --git a/server/lib/src/server/keys/internal.rs b/server/lib/src/server/keys/internal.rs
index e925f64f0..2e265d719 100644
--- a/server/lib/src/server/keys/internal.rs
+++ b/server/lib/src/server/keys/internal.rs
@@ -1,24 +1,22 @@
 use super::object::{KeyObject, KeyObjectT};
 use super::KeyId;
 use crate::prelude::*;
-
-use smolset::SmolSet;
-
-use std::collections::{BTreeMap, BTreeSet};
-use std::sync::Arc;
-
+use crate::value::{KeyStatus, KeyUsage};
+use crate::valueset::{KeyInternalData, ValueSetKeyInternal};
 use compact_jwt::compact::{JweAlg, JweCompact, JweEnc};
-use compact_jwt::crypto::{JweA128GCMEncipher, JweA128KWEncipher};
+use compact_jwt::crypto::{
+    JweA128GCMEncipher, JweA128KWEncipher, JwsRs256Signer, JwsRs256Verifier,
+};
 use compact_jwt::jwe::Jwe;
 use compact_jwt::traits::*;
 use compact_jwt::{
-    JwaAlg, Jwk, Jws, JwsCompact, JwsEs256Signer, JwsEs256Verifier, JwsSigner, JwsSignerToVerifier,
+    JwaAlg, Jwk, JwkKeySet, Jws, JwsCompact, JwsEs256Signer, JwsEs256Verifier, JwsSigner,
+    JwsSignerToVerifier,
 };
-
+use smolset::SmolSet;
+use std::collections::{BTreeMap, BTreeSet};
 use std::ops::Bound::{Included, Unbounded};
-
-use crate::value::{KeyStatus, KeyUsage};
-use crate::valueset::{KeyInternalData, ValueSetKeyInternal};
+use std::sync::Arc;
 
 pub struct KeyProviderInternal {
     uuid: Uuid,
@@ -61,6 +59,7 @@ impl KeyProviderInternal {
             uuid,
             jws_es256: None,
             jwe_a128gcm: None,
+            jws_rs256: None,
         }))
     }
 
@@ -73,6 +72,7 @@ impl KeyProviderInternal {
         debug!(?uuid, "Loading key object ...");
 
         let mut jws_es256: Option<KeyObjectInternalJwtEs256> = None;
+        let mut jws_rs256: Option<KeyObjectInternalJwtRs256> = None;
         let mut jwe_a128gcm: Option<KeyObjectInternalJweA128GCM> = None;
 
         if let Some(key_internal_map) = entry
@@ -104,6 +104,18 @@ impl KeyProviderInternal {
                             *valid_from,
                         )?;
                     }
+                    KeyUsage::JwsRs256 => {
+                        let jws_rs256_ref =
+                            jws_rs256.get_or_insert_with(KeyObjectInternalJwtRs256::default);
+
+                        jws_rs256_ref.load(
+                            key_id,
+                            *status,
+                            status_cid.clone(),
+                            der,
+                            *valid_from,
+                        )?;
+                    }
                     KeyUsage::JweA128GCM => {
                         let jwe_a128gcm_ref =
                             jwe_a128gcm.get_or_insert_with(KeyObjectInternalJweA128GCM::default);
@@ -125,6 +137,7 @@ impl KeyProviderInternal {
             uuid,
             jws_es256,
             jwe_a128gcm,
+            jws_rs256,
         })))
     }
 
@@ -194,7 +207,7 @@ impl KeyObjectInternalJweA128GCM {
     fn assert_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> {
         if self.get_valid_cipher(valid_from).is_none() {
             // This means there is no active signing key, so we need to create one.
-            warn!("no active jwe a128gcm found, creating a new one ...");
+            debug!("no active jwe a128gcm found, creating a new one ...");
             self.new_active(valid_from, cid)
         } else {
             Ok(())
@@ -422,7 +435,7 @@ impl KeyObjectInternalJwtEs256 {
     fn assert_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> {
         if self.get_valid_signer(valid_from).is_none() {
             // This means there is no active signing key, so we need to create one.
-            warn!("no active jwt es256 found, creating a new one ...");
+            debug!("no active jwt es256 found, creating a new one ...");
             self.new_active(valid_from, cid)
         } else {
             Ok(())
@@ -437,8 +450,8 @@ impl KeyObjectInternalJwtEs256 {
     ) -> Result<(), OperationError> {
         let valid_from = valid_from.as_secs();
 
-        for der in import_keys {
-            let signer = JwsEs256Signer::from_es256_der(der).map_err(|err| {
+        for private_der in import_keys {
+            let signer = JwsEs256Signer::from_es256_der(private_der).map_err(|err| {
                 error!(?err, "Unable to load imported es256 DER signer");
                 OperationError::KP0028KeyObjectImportJwsEs256DerInvalid
             })?;
@@ -451,22 +464,19 @@ impl KeyObjectInternalJwtEs256 {
                 OperationError::KP0029KeyObjectSignerToVerifier
             })?;
 
-            let public_der = verifier.public_key_to_der().map_err(|jwt_error| {
-                error!(?jwt_error, "Unable to convert public key to DER");
-                OperationError::KP0030KeyObjectPublicToDer
-            })?;
-
             // We need to use the legacy KID for imported objects
             let kid = signer.get_legacy_kid().to_string();
             debug!(?kid, "imported key");
 
+            self.active.insert(valid_from, signer.clone());
+
             self.all.insert(
                 kid,
                 InternalJwtEs256 {
                     valid_from,
-                    status: InternalJwtEs256Status::Retained {
+                    status: InternalJwtEs256Status::Valid {
                         verifier,
-                        public_der,
+                        private_der: private_der.clone(),
                     },
                     status_cid: cid.clone(),
                 },
@@ -696,6 +706,25 @@ impl KeyObjectInternalJwtEs256 {
         }
     }
 
+    fn public_jwks(&self) -> JwkKeySet {
+        let keys = self
+            .all
+            .iter()
+            .filter_map(|(_, es256)| match &es256.status {
+                InternalJwtEs256Status::Valid { verifier, .. }
+                | InternalJwtEs256Status::Retained { verifier, .. } => verifier
+                    .public_key_as_jwk()
+                    .map_err(|err| {
+                        error!(?err);
+                    })
+                    .ok(),
+                InternalJwtEs256Status::Revoked { .. } => None,
+            })
+            .collect::<Vec<_>>();
+
+        JwkKeySet { keys }
+    }
+
     fn public_jwk(&self, key_id: &str) -> Result<Option<Jwk>, OperationError> {
         if let Some(key_to_check) = self.all.get(key_id) {
             match &key_to_check.status {
@@ -728,11 +757,387 @@ impl KeyObjectInternalJwtEs256 {
     }
 }
 
+#[derive(Clone)]
+enum InternalJwtRs256Status {
+    Valid {
+        verifier: JwsRs256Verifier,
+        private_der: Vec<u8>,
+    },
+    Retained {
+        verifier: JwsRs256Verifier,
+        public_der: Vec<u8>,
+    },
+    Revoked {
+        untrusted_verifier: JwsRs256Verifier,
+        public_der: Vec<u8>,
+    },
+}
+
+#[derive(Clone)]
+struct InternalJwtRs256 {
+    valid_from: u64,
+    status: InternalJwtRs256Status,
+    status_cid: Cid,
+}
+
+#[derive(Default, Clone)]
+struct KeyObjectInternalJwtRs256 {
+    // active signing keys are in a BTreeMap indexed by their valid_from
+    // time so that we can retrieve the active key.
+    //
+    // We don't need to worry about manipulating this at runtime, since any expiry
+    // event will cause the keyObject to reload, which will reflect to this map.
+    active: BTreeMap<u64, JwsRs256Signer>,
+
+    // All keys are stored by their KeyId for fast lookup. Keys internally have a
+    // current status which is checked for signature validation.
+    all: BTreeMap<KeyId, InternalJwtRs256>,
+}
+
+impl KeyObjectInternalJwtRs256 {
+    fn get_valid_signer(&self, time: Duration) -> Option<&JwsRs256Signer> {
+        let ct_secs = time.as_secs();
+
+        self.active
+            .range((Unbounded, Included(ct_secs)))
+            .next_back()
+            .map(|(_time, signer)| signer)
+    }
+
+    fn assert_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> {
+        if self.get_valid_signer(valid_from).is_none() {
+            // This means there is no active signing key, so we need to create one.
+            debug!("no active jwt rs256 found, creating a new one ...");
+            self.new_active(valid_from, cid)
+        } else {
+            Ok(())
+        }
+    }
+
+    fn import(
+        &mut self,
+        import_keys: &SmolSet<[Vec<u8>; 1]>,
+        valid_from: Duration,
+        cid: &Cid,
+    ) -> Result<(), OperationError> {
+        let valid_from = valid_from.as_secs();
+
+        for private_der in import_keys {
+            let signer = JwsRs256Signer::from_rs256_der(private_der).map_err(|err| {
+                error!(?err, "Unable to load imported rs256 DER signer");
+                OperationError::KP0045KeyObjectImportJwsRs256DerInvalid
+            })?;
+
+            let verifier = signer.get_verifier().map_err(|jwt_error| {
+                error!(
+                    ?jwt_error,
+                    "Unable to produce jwt rs256 verifier from signer"
+                );
+                OperationError::KP0046KeyObjectSignerToVerifier
+            })?;
+
+            // We need to use the legacy KID for imported objects
+            let kid = signer.get_legacy_kid().to_string();
+            debug!(?kid, "imported key");
+
+            self.active.insert(valid_from, signer.clone());
+
+            self.all.insert(
+                kid,
+                InternalJwtRs256 {
+                    valid_from,
+                    status: InternalJwtRs256Status::Valid {
+                        verifier,
+                        private_der: private_der.clone(),
+                    },
+                    status_cid: cid.clone(),
+                },
+            );
+        }
+
+        Ok(())
+    }
+
+    fn new_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> {
+        let valid_from = valid_from.as_secs();
+
+        let signer = JwsRs256Signer::generate_legacy_rs256().map_err(|jwt_error| {
+            error!(?jwt_error, "Unable to generate new jwt rs256 signing key");
+            OperationError::KP0048KeyObjectJwtRs256Generation
+        })?;
+
+        let verifier = signer.get_verifier().map_err(|jwt_error| {
+            error!(
+                ?jwt_error,
+                "Unable to produce jwt rs256 verifier from signer"
+            );
+            OperationError::KP0049KeyObjectSignerToVerifier
+        })?;
+
+        let private_der = signer.private_key_to_der().map_err(|jwt_error| {
+            error!(?jwt_error, "Unable to convert signing key to DER");
+            OperationError::KP0050KeyObjectPrivateToDer
+        })?;
+
+        self.active.insert(valid_from, signer.clone());
+
+        let kid = signer.get_kid().to_string();
+
+        self.all.insert(
+            kid,
+            InternalJwtRs256 {
+                valid_from,
+                status: InternalJwtRs256Status::Valid {
+                    // signer,
+                    verifier,
+                    private_der,
+                },
+                status_cid: cid.clone(),
+            },
+        );
+
+        Ok(())
+    }
+
+    fn revoke(&mut self, revoke_key_id: &KeyId, cid: &Cid) -> Result<bool, OperationError> {
+        if let Some(key_to_revoke) = self.all.get_mut(revoke_key_id) {
+            let untrusted_verifier = match &key_to_revoke.status {
+                InternalJwtRs256Status::Valid { verifier, .. }
+                | InternalJwtRs256Status::Retained { verifier, .. } => verifier,
+                InternalJwtRs256Status::Revoked {
+                    untrusted_verifier, ..
+                } => untrusted_verifier,
+            }
+            .clone();
+
+            let public_der = untrusted_verifier
+                .public_key_to_der()
+                .map_err(|jwt_error| {
+                    error!(?jwt_error, "Unable to convert public key to DER");
+                    OperationError::KP0051KeyObjectPublicToDer
+                })?;
+
+            key_to_revoke.status = InternalJwtRs256Status::Revoked {
+                untrusted_verifier,
+                public_der,
+            };
+            key_to_revoke.status_cid = cid.clone();
+
+            let valid_from = key_to_revoke.valid_from;
+
+            // Remove it from the active set.
+            self.active.remove(&valid_from);
+
+            Ok(true)
+        } else {
+            // We didn't revoke anything
+            Ok(false)
+        }
+    }
+
+    fn load(
+        &mut self,
+        id: &str,
+        status: KeyStatus,
+        status_cid: Cid,
+        der: &[u8],
+        valid_from: u64,
+    ) -> Result<(), OperationError> {
+        let id: KeyId = id.to_string();
+
+        let status = match status {
+            KeyStatus::Valid => {
+                let signer = JwsRs256Signer::from_rs256_der(der).map_err(|err| {
+                    error!(?err, ?id, "Unable to load rs256 DER signer");
+                    OperationError::KP0052KeyObjectJwsRs256DerInvalid
+                })?;
+
+                let verifier = signer.get_verifier().map_err(|err| {
+                    error!(?err, "Unable to retrieve verifier from signer");
+                    OperationError::KP0053KeyObjectSignerToVerifier
+                })?;
+
+                self.active.insert(valid_from, signer);
+
+                InternalJwtRs256Status::Valid {
+                    // signer,
+                    verifier,
+                    private_der: der.to_vec(),
+                }
+            }
+            KeyStatus::Retained => {
+                let verifier = JwsRs256Verifier::from_rs256_der(der).map_err(|err| {
+                    error!(?err, ?id, "Unable to load rs256 DER verifier");
+                    OperationError::KP0054KeyObjectJwsRs256DerInvalid
+                })?;
+
+                InternalJwtRs256Status::Retained {
+                    verifier,
+                    public_der: der.to_vec(),
+                }
+            }
+            KeyStatus::Revoked => {
+                let untrusted_verifier = JwsRs256Verifier::from_rs256_der(der).map_err(|err| {
+                    error!(?err, ?id, "Unable to load rs256 DER revoked verifier");
+                    OperationError::KP0055KeyObjectJwsRs256DerInvalid
+                })?;
+
+                InternalJwtRs256Status::Revoked {
+                    untrusted_verifier,
+                    public_der: der.to_vec(),
+                }
+            }
+        };
+
+        let internal_jwt = InternalJwtRs256 {
+            valid_from,
+            status,
+            status_cid,
+        };
+
+        self.all.insert(id, internal_jwt);
+
+        Ok(())
+    }
+
+    fn to_key_iter(&self) -> impl Iterator<Item = (KeyId, KeyInternalData)> + '_ {
+        self.all.iter().map(|(key_id, internal_jwt)| {
+            let usage = KeyUsage::JwsRs256;
+
+            let valid_from = internal_jwt.valid_from;
+            let status_cid = internal_jwt.status_cid.clone();
+
+            let (status, der) = match &internal_jwt.status {
+                InternalJwtRs256Status::Valid { private_der, .. } => {
+                    (KeyStatus::Valid, private_der.clone())
+                }
+                InternalJwtRs256Status::Retained { public_der, .. } => {
+                    (KeyStatus::Retained, public_der.clone())
+                }
+                InternalJwtRs256Status::Revoked { public_der, .. } => {
+                    (KeyStatus::Revoked, public_der.clone())
+                }
+            };
+
+            (
+                key_id.clone(),
+                KeyInternalData {
+                    usage,
+                    valid_from,
+                    der,
+                    status,
+                    status_cid,
+                },
+            )
+        })
+    }
+
+    fn sign<V: JwsSignable>(
+        &self,
+        jws: &V,
+        current_time: Duration,
+    ) -> Result<V::Signed, OperationError> {
+        let Some(signing_key) = self.get_valid_signer(current_time) else {
+            error!("No signing keys available. This may indicate that no keys are valid yet!");
+            return Err(OperationError::KP0061KeyObjectNoActiveSigningKeys);
+        };
+
+        signing_key.sign(jws).map_err(|jwt_err| {
+            error!(?jwt_err, "Unable to sign jws");
+            OperationError::KP0056KeyObjectJwsRs256Signature
+        })
+    }
+
+    fn verify<V: JwsVerifiable>(&self, jwsc: &V) -> Result<V::Verified, OperationError> {
+        let internal_jws = jwsc
+            .kid()
+            .and_then(|kid| {
+                debug!(?kid);
+                self.all.get(kid)
+            })
+            .ok_or_else(|| {
+                error!("JWS is signed by a key that is not present in this KeyObject");
+                for pres_kid in self.all.keys() {
+                    debug!(?pres_kid);
+                }
+                OperationError::KP0057KeyObjectJwsNotAssociated
+            })?;
+
+        match &internal_jws.status {
+            InternalJwtRs256Status::Valid { verifier, .. }
+            | InternalJwtRs256Status::Retained { verifier, .. } => {
+                verifier.verify(jwsc).map_err(|jwt_err| {
+                    error!(?jwt_err, "Failed to verify jws");
+                    OperationError::KP0058KeyObjectJwsInvalid
+                })
+            }
+            InternalJwtRs256Status::Revoked { .. } => {
+                error!("The key used to sign this JWS has been revoked.");
+                Err(OperationError::KP0059KeyObjectJwsKeyRevoked)
+            }
+        }
+    }
+
+    fn public_jwks(&self) -> JwkKeySet {
+        let keys = self
+            .all
+            .iter()
+            .filter_map(|(key_id, rs256)| {
+                error!(?key_id);
+                match &rs256.status {
+                    InternalJwtRs256Status::Valid { verifier, .. }
+                    | InternalJwtRs256Status::Retained { verifier, .. } => verifier
+                        .public_key_as_jwk()
+                        .map_err(|err| {
+                            error!(?err);
+                        })
+                        .ok(),
+                    InternalJwtRs256Status::Revoked { .. } => None,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        JwkKeySet { keys }
+    }
+
+    fn public_jwk(&self, key_id: &str) -> Result<Option<Jwk>, OperationError> {
+        if let Some(key_to_check) = self.all.get(key_id) {
+            match &key_to_check.status {
+                InternalJwtRs256Status::Valid { verifier, .. }
+                | InternalJwtRs256Status::Retained { verifier, .. } => {
+                    verifier.public_key_as_jwk().map(Some).map_err(|err| {
+                        error!(?err, "Unable to construct public JWK.");
+                        OperationError::KP0060KeyObjectJwsPublicJwk
+                    })
+                }
+                InternalJwtRs256Status::Revoked { .. } => Ok(None),
+            }
+        } else {
+            Ok(None)
+        }
+    }
+
+    #[cfg(test)]
+    fn kid_status(&self, key_id: &KeyId) -> Result<Option<KeyStatus>, OperationError> {
+        if let Some(key_to_check) = self.all.get(key_id) {
+            let status = match &key_to_check.status {
+                InternalJwtRs256Status::Valid { .. } => KeyStatus::Valid,
+                InternalJwtRs256Status::Retained { .. } => KeyStatus::Retained,
+                InternalJwtRs256Status::Revoked { .. } => KeyStatus::Revoked,
+            };
+            Ok(Some(status))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
 #[derive(Clone)]
 pub struct KeyObjectInternal {
     provider: Arc<KeyProviderInternal>,
     uuid: Uuid,
     jws_es256: Option<KeyObjectInternalJwtEs256>,
+    jws_rs256: Option<KeyObjectInternalJwtRs256>,
     jwe_a128gcm: Option<KeyObjectInternalJweA128GCM>,
     // If you add more types here you need to add these to rotate
     // and revoke.
@@ -769,6 +1174,10 @@ impl KeyObjectT for KeyObjectInternal {
             jws_es256_object.new_active(rotation_time, cid)?;
         }
 
+        if let Some(jws_rs256_object) = &mut self.jws_rs256 {
+            jws_rs256_object.new_active(rotation_time, cid)?;
+        }
+
         if let Some(jwe_a128_gcm) = &mut self.jwe_a128gcm {
             jwe_a128_gcm.new_active(rotation_time, cid)?;
         }
@@ -790,6 +1199,12 @@ impl KeyObjectT for KeyObjectInternal {
                 }
             };
 
+            if let Some(jws_rs256_object) = &mut self.jws_rs256 {
+                if jws_rs256_object.revoke(revoke_key_id, cid)? {
+                    has_revoked = true;
+                }
+            };
+
             if let Some(jwe_a128_gcm) = &mut self.jwe_a128gcm {
                 if jwe_a128_gcm.revoke(revoke_key_id, cid)? {
                     has_revoked = true;
@@ -831,6 +1246,14 @@ impl KeyObjectT for KeyObjectInternal {
                     Err(OperationError::KP0018KeyProviderNoSuchKey)
                 }
             }
+            JwaAlg::RS256 => {
+                if let Some(jws_rs256_object) = &self.jws_rs256 {
+                    jws_rs256_object.verify(jwsc)
+                } else {
+                    error!(provider_uuid = ?self.uuid, "jwt rs256 not available on this provider");
+                    Err(OperationError::KP0018KeyProviderNoSuchKey)
+                }
+            }
             unsupported_alg => {
                 // unsupported rn.
                 error!(provider_uuid = ?self.uuid, ?unsupported_alg, "algorithm not available on this provider");
@@ -839,12 +1262,26 @@ impl KeyObjectT for KeyObjectInternal {
         }
     }
 
-    fn jws_public_jwk(&self, kid: &str) -> Result<Option<Jwk>, OperationError> {
+    fn jws_es256_jwks(&self) -> Option<JwkKeySet> {
+        self.jws_es256
+            .as_ref()
+            .map(|jws_es256_object| jws_es256_object.public_jwks())
+    }
+
+    fn jws_public_jwk(&self, key_id: &str) -> Result<Option<Jwk>, OperationError> {
         if let Some(jws_es256_object) = &self.jws_es256 {
-            jws_es256_object.public_jwk(kid)
-        } else {
-            Ok(None)
+            if let Some(status) = jws_es256_object.public_jwk(key_id)? {
+                return Ok(Some(status));
+            }
         }
+
+        if let Some(jws_rs256_object) = &self.jws_rs256 {
+            if let Some(status) = jws_rs256_object.public_jwk(key_id)? {
+                return Ok(Some(status));
+            }
+        }
+
+        Ok(None)
     }
 
     fn jws_es256_import(
@@ -921,6 +1358,12 @@ impl KeyObjectT for KeyObjectInternal {
             }
         }
 
+        if let Some(jws_rs256_object) = &self.jws_rs256 {
+            if let Some(status) = jws_rs256_object.kid_status(key_id)? {
+                return Ok(Some(status));
+            }
+        }
+
         Ok(None)
     }
 
@@ -933,6 +1376,11 @@ impl KeyObjectT for KeyObjectInternal {
                 self.jwe_a128gcm
                     .iter()
                     .flat_map(|jwe_a128gcm| jwe_a128gcm.to_key_iter()),
+            )
+            .chain(
+                self.jws_rs256
+                    .iter()
+                    .flat_map(|jws_rs256| jws_rs256.to_key_iter()),
             );
         let key_vs = ValueSetKeyInternal::from_key_iter(key_iter)? as ValueSet;
 
@@ -948,6 +1396,46 @@ impl KeyObjectT for KeyObjectInternal {
             (Attribute::KeyInternalData, key_vs),
         ])
     }
+
+    fn jws_rs256_import(
+        &mut self,
+        import_keys: &SmolSet<[Vec<u8>; 1]>,
+        valid_from: Duration,
+        cid: &Cid,
+    ) -> Result<(), OperationError> {
+        let koi = self
+            .jws_rs256
+            .get_or_insert_with(KeyObjectInternalJwtRs256::default);
+
+        koi.import(import_keys, valid_from, cid)
+    }
+
+    fn jws_rs256_assert(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> {
+        let koi = self
+            .jws_rs256
+            .get_or_insert_with(KeyObjectInternalJwtRs256::default);
+
+        koi.assert_active(valid_from, cid)
+    }
+
+    fn jws_rs256_sign(
+        &self,
+        jws: &Jws,
+        current_time: Duration,
+    ) -> Result<JwsCompact, OperationError> {
+        if let Some(jws_rs256_object) = &self.jws_rs256 {
+            jws_rs256_object.sign(jws, current_time)
+        } else {
+            error!(provider_uuid = ?self.uuid, "jwt rs256 not available on this provider");
+            Err(OperationError::KP0062KeyProviderNoSuchKey)
+        }
+    }
+
+    fn jws_rs256_jwks(&self) -> Option<JwkKeySet> {
+        self.jws_rs256
+            .as_ref()
+            .map(|jws_rs256_object| jws_rs256_object.public_jwks())
+    }
 }
 
 #[cfg(test)]
diff --git a/server/lib/src/server/keys/object.rs b/server/lib/src/server/keys/object.rs
index 7640a5cec..4fca79d61 100644
--- a/server/lib/src/server/keys/object.rs
+++ b/server/lib/src/server/keys/object.rs
@@ -1,6 +1,6 @@
 use crate::prelude::*;
 use compact_jwt::{compact::JweCompact, jwe::Jwe};
-use compact_jwt::{Jwk, Jws, JwsCompact};
+use compact_jwt::{Jwk, JwkKeySet, Jws, JwsCompact};
 use smolset::SmolSet;
 use std::collections::BTreeSet;
 use uuid::Uuid;
@@ -29,6 +29,25 @@ pub trait KeyObjectT {
         current_time: Duration,
     ) -> Result<JwsCompact, OperationError>;
 
+    fn jws_es256_jwks(&self) -> Option<JwkKeySet>;
+
+    fn jws_rs256_import(
+        &mut self,
+        import_keys: &SmolSet<[Vec<u8>; 1]>,
+        valid_from: Duration,
+        cid: &Cid,
+    ) -> Result<(), OperationError>;
+
+    fn jws_rs256_assert(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError>;
+
+    fn jws_rs256_sign(
+        &self,
+        jws: &Jws,
+        current_time: Duration,
+    ) -> Result<JwsCompact, OperationError>;
+
+    fn jws_rs256_jwks(&self) -> Option<JwkKeySet>;
+
     fn jws_verify(&self, jwsc: &JwsCompact) -> Result<Jws, OperationError>;
 
     fn jws_public_jwk(&self, kid: &str) -> Result<Option<Jwk>, OperationError>;
diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs
index b53afa579..fd0bca8db 100644
--- a/server/lib/src/server/migrations.rs
+++ b/server/lib/src/server/migrations.rs
@@ -532,6 +532,78 @@ impl QueryServerWriteTransaction<'_> {
 
         self.reload()?;
 
+        // =========== OAuth2 Cryptography Migration ==============
+
+        debug!("START OAUTH2 MIGRATION");
+
+        // Load all the OAuth2 providers.
+        let all_oauth2_rs_entries = self.internal_search(filter!(f_eq(
+            Attribute::Class,
+            EntryClass::OAuth2ResourceServer.into()
+        )))?;
+
+        if !all_oauth2_rs_entries.is_empty() {
+            let entry_iter = all_oauth2_rs_entries.iter().map(|tgt_entry| {
+                let entry_uuid = tgt_entry.get_uuid();
+                let mut modlist = ModifyList::new_list(vec![
+                    Modify::Present(Attribute::Class, EntryClass::KeyObject.to_value()),
+                    Modify::Present(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value()),
+                    Modify::Present(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value()),
+                    // Delete the fernet key, rs256 if any, and the es256 key
+                    Modify::Purged(Attribute::OAuth2RsTokenKey),
+                    Modify::Purged(Attribute::Es256PrivateKeyDer),
+                    Modify::Purged(Attribute::Rs256PrivateKeyDer),
+                ]);
+
+                trace!(?tgt_entry);
+
+                // Import the ES256 Key
+                if let Some(es256_private_der) =
+                    tgt_entry.get_ava_single_private_binary(Attribute::Es256PrivateKeyDer)
+                {
+                    modlist.push_mod(Modify::Present(
+                        Attribute::KeyActionImportJwsEs256,
+                        Value::PrivateBinary(es256_private_der.to_vec()),
+                    ))
+                } else {
+                    warn!("Unable to migrate es256 key");
+                }
+
+                let has_rs256 = tgt_entry
+                    .get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable)
+                    .unwrap_or(false);
+
+                // If there is an rs256 key, import it.
+                // Import the RS256 Key
+                if has_rs256 {
+                    modlist.push_mod(Modify::Present(
+                        Attribute::Class,
+                        EntryClass::KeyObjectJwtEs256.to_value(),
+                    ));
+
+                    if let Some(rs256_private_der) =
+                        tgt_entry.get_ava_single_private_binary(Attribute::Rs256PrivateKeyDer)
+                    {
+                        modlist.push_mod(Modify::Present(
+                            Attribute::KeyActionImportJwsRs256,
+                            Value::PrivateBinary(rs256_private_der.to_vec()),
+                        ))
+                    } else {
+                        warn!("Unable to migrate rs256 key");
+                    }
+                }
+
+                (entry_uuid, modlist)
+            });
+
+            self.internal_batch_modify(entry_iter)?;
+        }
+
+        // Reload for new keys, and updated oauth2
+        self.reload()?;
+
+        // Done!
+
         Ok(())
     }
 
diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs
index fb3c27ca6..981b19718 100644
--- a/server/lib/src/value.rs
+++ b/server/lib/src/value.rs
@@ -1251,6 +1251,7 @@ pub struct Oauth2Session {
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum KeyUsage {
     JwsEs256,
+    JwsRs256,
     JweA128GCM,
 }
 
@@ -1261,6 +1262,7 @@ impl fmt::Display for KeyUsage {
             "{}",
             match self {
                 KeyUsage::JwsEs256 => "jws_es256",
+                KeyUsage::JwsRs256 => "jws_rs256",
                 KeyUsage::JweA128GCM => "jwe_a128gcm",
             }
         )
diff --git a/server/lib/src/valueset/key_internal.rs b/server/lib/src/valueset/key_internal.rs
index fb5034595..e657238b9 100644
--- a/server/lib/src/valueset/key_internal.rs
+++ b/server/lib/src/valueset/key_internal.rs
@@ -83,6 +83,7 @@ impl ValueSetKeyInternal {
                         let id: KeyId = id;
                         let usage = match usage {
                             DbValueKeyUsage::JwsEs256 => KeyUsage::JwsEs256,
+                            DbValueKeyUsage::JwsRs256 => KeyUsage::JwsRs256,
                             DbValueKeyUsage::JweA128GCM => KeyUsage::JweA128GCM,
                         };
                         let status_cid = status_cid.into();
@@ -131,6 +132,7 @@ impl ValueSetKeyInternal {
                     let id: String = id.clone();
                     let usage = match usage {
                         KeyUsage::JwsEs256 => DbValueKeyUsage::JwsEs256,
+                        KeyUsage::JwsRs256 => DbValueKeyUsage::JwsRs256,
                         KeyUsage::JweA128GCM => DbValueKeyUsage::JweA128GCM,
                     };
                     let status_cid = status_cid.into();
diff --git a/server/testkit/tests/testkit/oauth2_test.rs b/server/testkit/tests/testkit/oauth2_test.rs
index 8de7dab73..be0290940 100644
--- a/server/testkit/tests/testkit/oauth2_test.rs
+++ b/server/testkit/tests/testkit/oauth2_test.rs
@@ -1,9 +1,6 @@
 #![deny(warnings)]
-use std::collections::{BTreeMap, BTreeSet};
-use std::convert::TryFrom;
-use std::str::FromStr;
-
 use compact_jwt::{JwkKeySet, JwsEs256Verifier, JwsVerifier, OidcToken, OidcUnverified};
+use kanidm_client::{http::header, KanidmClient, StatusCode};
 use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
 use kanidm_proto::constants::*;
 use kanidm_proto::internal::Oauth2ClaimMapJoin;
@@ -14,18 +11,20 @@ use kanidm_proto::oauth2::{
 };
 use kanidmd_lib::constants::NAME_IDM_ALL_ACCOUNTS;
 use kanidmd_lib::prelude::Attribute;
-use oauth2_ext::PkceCodeChallenge;
-use reqwest::header::{HeaderValue, CONTENT_TYPE};
-use uri::{OAUTH2_TOKEN_ENDPOINT, OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
-use url::{form_urlencoded::parse as query_parse, Url};
-
-use kanidm_client::{http::header, KanidmClient, StatusCode};
 use kanidmd_testkit::{
     assert_no_cache, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, NOT_ADMIN_TEST_EMAIL,
     NOT_ADMIN_TEST_PASSWORD, NOT_ADMIN_TEST_USERNAME, TEST_INTEGRATION_RS_DISPLAY,
     TEST_INTEGRATION_RS_GROUP_ALL, TEST_INTEGRATION_RS_ID, TEST_INTEGRATION_RS_REDIRECT_URL,
     TEST_INTEGRATION_RS_URL,
 };
+use oauth2_ext::PkceCodeChallenge;
+use reqwest::header::{HeaderValue, CONTENT_TYPE};
+use std::collections::{BTreeMap, BTreeSet};
+use std::convert::TryFrom;
+use std::str::FromStr;
+use time::OffsetDateTime;
+use uri::{OAUTH2_TOKEN_ENDPOINT, OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
+use url::{form_urlencoded::parse as query_parse, Url};
 
 /// Tests an OAuth 2.0 / OpenID confidential client Authorisation Client flow.
 ///
@@ -91,10 +90,15 @@ async fn test_oauth2_openid_basic_flow_impl(
         .expect("Failed to configure account password");
 
     rsclient
-        .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true)
+        .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true)
         .await
         .expect("Failed to update oauth2 config");
 
+    rsclient
+        .idm_oauth2_rs_rotate_keys(TEST_INTEGRATION_RS_ID, OffsetDateTime::now_utc())
+        .await
+        .expect("Failed to rotate oauth2 keys");
+
     rsclient
         .idm_oauth2_rs_update_scope_map(
             TEST_INTEGRATION_RS_ID,
@@ -621,7 +625,7 @@ async fn test_oauth2_openid_public_flow_impl(
         .expect("Failed to configure account password");
 
     rsclient
-        .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true)
+        .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true)
         .await
         .expect("Failed to update oauth2 config");
 
diff --git a/server/testkit/tests/testkit/proto_v1_test.rs b/server/testkit/tests/testkit/proto_v1_test.rs
index b55dfcd30..fdcf15638 100644
--- a/server/testkit/tests/testkit/proto_v1_test.rs
+++ b/server/testkit/tests/testkit/proto_v1_test.rs
@@ -1,9 +1,7 @@
 #![deny(warnings)]
-use std::path::Path;
-use std::time::SystemTime;
-
+use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
+use kanidm_client::{ClientError, KanidmClient};
 use kanidm_proto::constants::{ATTR_GIDNUMBER, KSESSIONID};
-
 use kanidm_proto::internal::{
     ApiToken, CURegState, Filter, ImageValue, Modify, ModifyList, UatPurpose, UserAuthToken,
 };
@@ -13,19 +11,16 @@ use kanidm_proto::v1::{
 };
 use kanidmd_lib::constants::{NAME_IDM_ADMINS, NAME_SYSTEM_ADMINS};
 use kanidmd_lib::credential::totp::Totp;
-
 use kanidmd_lib::prelude::Attribute;
-use tracing::{debug, trace};
-
+use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
+use std::path::Path;
 use std::str::FromStr;
-
-use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
+use std::time::SystemTime;
+use time::OffsetDateTime;
+use tracing::{debug, trace};
 use webauthn_authenticator_rs::softpasskey::SoftPasskey;
 use webauthn_authenticator_rs::WebauthnAuthenticator;
 
-use kanidm_client::{ClientError, KanidmClient};
-use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
-
 const UNIX_TEST_PASSWORD: &str = "unix test user password";
 
 #[kanidmd_testkit::test]
@@ -851,8 +846,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) {
 
     assert_eq!(initial_configs.len(), 1);
 
-    // Get the value. Assert we have oauth2_rs_basic_secret,
-    // but can NOT see the token_secret.
+    // Get the value. Assert we have oauth2_rs_basic_secret.
     let oauth2_config = rsclient
         .idm_oauth2_rs_get("test_integration")
         .await
@@ -866,10 +860,6 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) {
     assert!(oauth2_config
         .attrs
         .contains_key(Attribute::OAuth2RsBasicSecret.as_str()));
-    // This is present, but redacted.
-    assert!(oauth2_config
-        .attrs
-        .contains_key(Attribute::OAuth2RsTokenKey.as_str()));
 
     // Mod delete the secret/key and check them again.
     // Check we can patch the oauth2_rs_name / oauth2_rs_origin
@@ -880,12 +870,15 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) {
             Some("Test Integration"),
             Some("https://new_demo.example.com"),
             true,
-            true,
-            true,
         )
         .await
         .expect("Failed to update config");
 
+    rsclient
+        .idm_oauth2_rs_rotate_keys("test_integration", OffsetDateTime::now_utc())
+        .await
+        .expect("Failed to rotate oauth2 keys");
+
     let oauth2_config_updated = rsclient
         .idm_oauth2_rs_get("test_integration")
         .await
diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml
index fdb9e2dfd..7832582eb 100644
--- a/tools/cli/Cargo.toml
+++ b/tools/cli/Cargo.toml
@@ -73,6 +73,7 @@ serde = { workspace = true }
 serde_json = { workspace = true }
 uuid = { workspace = true }
 url = { workspace = true }
+time = { workspace = true }
 
 ## See src/cli/webauthn/mod.rs for which features are
 ## required for which target_os here
diff --git a/tools/cli/src/cli/oauth2.rs b/tools/cli/src/cli/oauth2.rs
index d1064efe3..3f6a2383f 100644
--- a/tools/cli/src/cli/oauth2.rs
+++ b/tools/cli/src/cli/oauth2.rs
@@ -1,12 +1,11 @@
 use crate::common::OpType;
+use crate::Oauth2ClaimMapJoin;
 use crate::{handle_client_error, Oauth2Opt, OutputMode};
 use anyhow::{Context, Error};
+use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin};
 use std::fs::read;
 use std::process::exit;
 
-use crate::Oauth2ClaimMapJoin;
-use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin};
-
 impl Oauth2Opt {
     pub fn debug(&self) -> bool {
         match self {
@@ -47,7 +46,9 @@ impl Oauth2Opt {
             | Oauth2Opt::EnableStrictRedirectUri { copt, .. }
             | Oauth2Opt::DisableStrictRedirectUri { copt, .. }
             | Oauth2Opt::AddOrigin { copt, .. }
-            | Oauth2Opt::RemoveOrigin { copt, .. } => copt.debug,
+            | Oauth2Opt::RemoveOrigin { copt, .. }
+            | Oauth2Opt::RotateCryptographicKeys { copt, .. }
+            | Oauth2Opt::RevokeCryptographicKey { copt, .. } => copt.debug,
         }
     }
 
@@ -196,7 +197,7 @@ impl Oauth2Opt {
             Oauth2Opt::ResetSecrets(cbopt) => {
                 let client = cbopt.copt.to_client(OpType::Write).await;
                 match client
-                    .idm_oauth2_rs_update(cbopt.name.as_str(), None, None, None, true, true, true)
+                    .idm_oauth2_rs_update(cbopt.name.as_str(), None, None, None, true)
                     .await
                 {
                     Ok(_) => println!("Success"),
@@ -238,8 +239,6 @@ impl Oauth2Opt {
                         Some(cbopt.displayname.as_str()),
                         None,
                         false,
-                        false,
-                        false,
                     )
                     .await
                 {
@@ -256,8 +255,6 @@ impl Oauth2Opt {
                         None,
                         None,
                         false,
-                        false,
-                        false,
                     )
                     .await
                 {
@@ -268,15 +265,7 @@ impl Oauth2Opt {
             Oauth2Opt::SetLandingUrl { nopt, url } => {
                 let client = nopt.copt.to_client(OpType::Write).await;
                 match client
-                    .idm_oauth2_rs_update(
-                        nopt.name.as_str(),
-                        None,
-                        None,
-                        Some(url.as_str()),
-                        false,
-                        false,
-                        false,
-                    )
+                    .idm_oauth2_rs_update(nopt.name.as_str(), None, None, Some(url.as_str()), false)
                     .await
                 {
                     Ok(_) => println!("Success"),
@@ -522,6 +511,30 @@ impl Oauth2Opt {
                     Err(e) => handle_client_error(e, copt.output_mode),
                 }
             }
+            Oauth2Opt::RotateCryptographicKeys {
+                copt,
+                name,
+                rotate_at,
+            } => {
+                let client = copt.to_client(OpType::Write).await;
+                match client
+                    .idm_oauth2_rs_rotate_keys(name.as_str(), *rotate_at)
+                    .await
+                {
+                    Ok(_) => println!("Success"),
+                    Err(e) => handle_client_error(e, copt.output_mode),
+                }
+            }
+            Oauth2Opt::RevokeCryptographicKey { copt, name, key_id } => {
+                let client = copt.to_client(OpType::Write).await;
+                match client
+                    .idm_oauth2_rs_revoke_key(name.as_str(), key_id.as_str())
+                    .await
+                {
+                    Ok(_) => println!("Success"),
+                    Err(e) => handle_client_error(e, copt.output_mode),
+                }
+            }
         }
     }
 }
diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs
index 5e02a10ba..3a0f1386b 100644
--- a/tools/cli/src/opt/kanidm.rs
+++ b/tools/cli/src/opt/kanidm.rs
@@ -1,6 +1,12 @@
 use clap::{builder::PossibleValue, Args, Subcommand, ValueEnum};
 use kanidm_proto::internal::ImageType;
 use std::fmt;
+use time::format_description::well_known::Rfc3339;
+use time::OffsetDateTime;
+
+fn parse_rfc3339(input: &str) -> Result<OffsetDateTime, time::error::Parse > {
+    OffsetDateTime::parse(input, &Rfc3339)
+}
 
 #[derive(Debug, Args)]
 pub struct Named {
@@ -1131,8 +1137,9 @@ pub enum Oauth2Opt {
         group: String,
     },
 
-    #[clap(name = "reset-secrets")]
-    /// Reset the secrets associated to this client
+    #[clap(name = "reset-basic-secret")]
+    /// Reset the client basic secret. You will need to update your client after
+    /// executing this.
     ResetSecrets(Named),
     #[clap(name = "show-basic-secret")]
     /// Show the associated basic secret for this client
@@ -1143,7 +1150,7 @@ pub enum Oauth2Opt {
     /// Set a new display name for a client
     #[clap(name = "set-displayname")]
     SetDisplayname(Oauth2SetDisplayname),
-    /// Set a new name for this client. You may need to update
+    /// Set a new name for this client. You will need to update
     /// your integrated applications after this so that they continue to
     /// function correctly.
     #[clap(name = "set-name")]
@@ -1256,6 +1263,28 @@ pub enum Oauth2Opt {
     #[cfg(feature = "dev-oauth2-device-flow")]
     /// Disable OAuth2 Device Flow authentication
     DeviceFlowDisable(Named),
+    /// Rotate the signing and encryption keys used by this client. The rotation
+    /// will occur at the specified time, or immediately if the time is in the past.
+    /// Past signatures will continue to operate even after a rotation occurs. If you
+    /// have concerns a key is compromised, then you should revoke it instead.
+    #[clap(name = "rotate-cryptographic-keys")]
+    RotateCryptographicKeys {
+        #[clap(flatten)]
+        copt: CommonOpt,
+        name: String,
+        #[clap(value_parser = parse_rfc3339)]
+        rotate_at: OffsetDateTime,
+    },
+    /// Revoke the signing and encryption keys used by this client. This will immediately
+    /// trigger a rotation of the key in question, and signtatures or tokens issued by
+    /// the revoked key will not be considered valid.
+    #[clap(name = "revoke-cryptographic-key")]
+    RevokeCryptographicKey {
+        #[clap(flatten)]
+        copt: CommonOpt,
+        name: String,
+        key_id: String,
+    },
 }
 
 #[derive(Args, Debug)]