fix(oidc): close re-review gaps in the hardening commit

Follow-up to the OIDC security hardening, from a max-effort re-review:

- Refresh now re-checks the denylist immediately before minting, against the
  (possibly remapped) identity but anchored on the original session `iat` — so a
  back-channel logout / SCIM deny that lands during the IdP grant, or one
  targeting the refreshed sub/sid, still blocks renewal instead of being escaped
  by the renewed token's fresh iat. Completes the watermark revocation fix.
- SCIM PUT `userName` immutability check is now case-insensitive, matching the
  case-insensitive list/create — a differently-cased userName echo no longer
  400s "userName is immutable" and blocks deprovision. Completes the SCIM
  case-insensitivity fix.
- Drop the unconditional state-cookie deletion on every callback exit: it let
  one tab's callback clear another in-flight tab's cookie, breaking concurrent
  logins. The cookie self-expires (max_age) and the Redis state is single-use,
  so the delete wasn't needed.

Tests added for the refresh revocation re-check and the case-insensitive PUT.
This commit is contained in:
Alex committed 2026-06-10 14:15:32 +01:00
1 parent a3d09e8890
commit 321d88d5be
4 files changed
+59 -5

No files matched your search

+16
View File
@@ -393,6 +393,22 @@ class TestReplaceUser:
"alice@example.com", "scim_deactivated", metadata={"via": "scim"}
)
def test_put_differently_cased_username_deprovisions(self, client, scim_settings, scim_mocks):
# userName is case-insensitive: a PUT echoing the stored name under
# different casing must deactivate, not 400 "userName is immutable".
row = _user_row(active=True) # user_id == "alice@example.com"
scim_mocks.users.get_by_pk.return_value = row
scim_mocks.users.set_active.return_value = {**row, "active": False}
response = client.put(
f"/scim/v2/Users/{USER_PK}",
headers=AUTH,
json={"userName": "Alice@Example.com", "active": False},
)
assert response.status_code == 200
assert response.get_json()["active"] is False
scim_mocks.users.set_active.assert_called_once_with(USER_PK, False)
scim_mocks.deny_user.assert_called_once_with("alice@example.com")
def test_put_unchanged_active_has_no_side_effects(self, client, scim_settings, scim_mocks):
scim_mocks.users.get_by_pk.return_value = _user_row(active=True)
response = client.put(