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

+22
View File
@@ -1412,6 +1412,28 @@ class TestRefreshRoute:
assert response.get_json() == {"error": "token_revoked"}
assert "oidc:refresh:jti-1" in fake_redis.store
def test_revocation_during_grant_blocks_renewal(self, client, fake_redis):
# is_denied passes the pre-grant check, but a back-channel logout lands
# during the IdP grant; the pre-mint re-check (anchored on the old iat)
# must catch it instead of minting a session that escapes the watermark.
self.denylist.is_denied.side_effect = [False, True]
token = make_session_token()
fake_redis.store["oidc:refresh:jti-1"] = b"rt-old"
id_claims = id_token_claims(sid="sess-1")
del id_claims["nonce"]
response, _ = self._refresh(
client,
token,
idp_response={
"access_token": "at-2",
"refresh_token": "rt-new",
"id_token": sign_id_token(id_claims),
},
)
assert response.status_code == 401
assert response.get_json() == {"error": "token_revoked"}
def test_disabled_user_rejected(self, client, fake_redis):
self.db.users.get.return_value = {"user_id": "oidc-user-1", "active": False}
token = make_session_token()