Repository navigation
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 134 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
32eaef5 to
6c4da8b
Compare
|
Could you mark this finding as a false positive and rerun Codacy? It occurs in an integration test: the executable name is fixed to redis-server, arguments are passed without a shell, and the socket path comes from t.TempDir(). No untrusted request input reaches this call. |
|
This means you're logged out from every session on every browser/device, right? |
the scope depends on the logout token. When it contains a sid, only the tracked tokens belonging to that session are revoked; other sessions of the same user remain authenticated. When it contains only sub, all tracked sessions for that user are targeted, as specified by oidc back-channel logout. |
|
Thanks for the clarification! |
|
@zerox80 Could you provide some more details about what issue this PR is actually trying to address? Under what circumstances (config) is there actually a problem? |
|
@rhafer The issue is in the proxy's backchannel logout handling. It can happen with the default OIDC config ( With |
rhafer
left a comment
There was a problem hiding this comment.
@zerox80 Thanks again for bringing this up. And sorry for having it hanging around for so long. I think I understand the issue(s) now. However, I somehow have the feeling we're making our lives more complicated that needed here (partially already before your change).
The main issue you're bringing up is valid. Multiple tokens for the same session can still be valid at a given time (e.g. if a client refresh it's token before it expires). And our current implementation is not safe in that case (the old token stays in the userinfo cache).
I’m less sure we need a per-token index and revocation list to handle it, though. I think it might be simpler to just record logout markers by sessionid and/or subject and check those during authentication:
Session-specific logout
- On a verified backchannel logout containing sid, write a marker such for that session id
- On every authentication, obtain the token’s verified session ID and check for that marker.
- Reject the token if the marker exists.
That covers the case where multiple valid tokens exist. Even for tokens that OpenCloud has not seen before. It also avoids enumerating tokens.
This assumes the IdP supplies a trustworthy sid in access tokens and assigns a new sid to a new login. Which I think is fine. We should just document that requirement.
Subject-only logout
A logout can contain sub without sid, meaning all sessions for that subject should end. A permanent subject marker would also block future logins, so this needs a cutoff:
- Record a verified logout time for the subject.
- Reject tokens for that subject issued at or before that time.
- Allow tokens from later logins.
This requires a trusted token issuance time and handling of clock skew. Concurrent or replayed logout messages must not move the cutoff backwards.
Tokens without a trustworthy iat for this to work. (Again something we'd need to document. I think it is fine if we do not support backchannel logout when opaque (i.e. non-jwt) access tokens are used
Retention and storage
Markers must remain until no token they cover can still be accepted. A practical simplification would be to require a configured maximum accepted token lifetime (+ some clock skew buffer maybe)
Other
- I don't think we need any migration here. In a multi instance setup we don't aim to support having multiple instance of the same service running in different version for a prolonged time. We should however switch the database name to start with a fresh cache after this change.
- As mentioned above it's fine to restrict backchannel logout to
PROXY_OIDC_ACCESS_TOKEN_VERIFY_METHOD=jwtsetups - It is also fine to have the documented requirement that the access token needs to contain a
sidclaim - As a side effect this PR also fixes an issue that makes the userinfo cache invalidation fail when publishing the BackchannelLogoutEvent fails (we should not forget about that).
@zerox80 Do you feel comfortable implementing this?
|
@rhafer Thanks for the review. Yes, I can implement this, and I agree that markers are simpler than tracking every token. I tested the assumptions against Keycloak 25.0.0 and 26.8.1, using a copy of the realm from I also ran the logout end to end through a real OpenCloud server, using current
The marker design isn't implemented yet, so it hasn't been tested end to end. One result changes the design, because a logged-out
A plain session marker would reject those tokens until it expires. I suggest one rule for both marker types: reject a token if A few points I'd like to confirm:
Keycloak also sends no logout token in these cases, so the proxy can't react to them. These gaps exist today and this change won't fix them, so I'd document them:
|
4c925b6 to
bb38647
Compare
[..]
Hm, interesting corner case. I am not sure about the suggested solution, would also be interested to see how other implementations handle this case. In general my assumption would be that if a backchannel logout with a specific
Yes.
Hm, I think we should issue a WARN level log message for these cases, for now. Also for cases like this it would IMO make sense to require
Yes.
Not sure I understand this. Please elaborate.
Would it help if we just fell back to an online userinfo lookup if reading the marker fails (only for failures caused by service outage of the store)
It's probably also worth checking why that is.
agreed. Some general remarks:
|
|
@rhafer Thanks! The branch of this PR already contains a fresh implementation on top of
For normal sessions, your assumption holds: refreshing fails after the logout, and the marker rejects the tokens of that session. Offline tokens are the exception, and the desktop client requests I could also honor Token lifetime: I'll change this to a WARN log and document Logout event: The markers only decide whether a token is rejected. The Store errors: The userinfo fallback makes sense, and I'll implement it for store read failures. With Backchannel URL: Only I'll open bug issues for the problems this PR addresses and link them here. |
Add a store-backed record of verified backchannel logouts. A marker holds the iat of the logout token as a cutoff, for a session or, if the logout token has no sid, for a subject. A token is covered if it belongs to the marked session or subject and was issued at or before the cutoff. Tokens issued later stay valid. Keycloak keeps issuing tokens with the same sid to a client with an offline token after a logout, and reuses the sid for a new login after a consent revocation, so a plain session marker would lock those clients out until it expires. Using the iat of the logout token means both timestamps come from the IDP clock, and replaying a logout token cannot change the cutoff.
Multiple access tokens can be valid for one session, for example after a refresh. A logout only removed the cached claims of the token in the session lookup, so older tokens stayed accepted. With PROXY_OIDC_SKIP_USER_INFO=true, even the removed token was accepted again, because a still valid JWT was verified anew. Write a marker on every verified logout and check it on every authenticated request, for cached and uncached tokens. This also covers tokens the proxy has not seen before the logout. If the marker store cannot be read, the cached claims are not used and the userinfo endpoint decides about the token. With PROXY_OIDC_SKIP_USER_INFO=true there is no such check, so the request is rejected. A corrupt marker always rejects. The cached record now keeps the verified subject, session and issue time, because a userinfo response can overwrite them in the claims. The cache database is renamed to start with a fresh cache. Markers get their own database because nats-js-kv only supports one TTL per bucket. They are kept for the new PROXY_OIDC_MAX_ACCESS_TOKEN_LIFETIME plus a buffer. A JWT access token with a longer lifetime or without an expiration can outlive its marker, so the proxy logs a warning for it. Logout tokens without iat are now rejected, as the spec requires the claim. Publishing the logout event and removing cached records are now best effort, so a failed publish no longer prevents the logout.
bb38647 to
7b183f2
Compare
Description
Multiple access tokens can be valid for one session, for example after a refresh. A backchannel logout only removed the cached claims of the token in the session lookup, so older tokens stayed accepted. With
PROXY_OIDC_SKIP_USER_INFO=true, even the removed token was accepted again, because a still valid JWT was verified anew.Each verified logout now writes a marker for the session or, if the logout token has no
sid, for the subject. The marker holds the logout token'siatas a cutoff. Every authenticated request is checked against the markers, and a token is rejected if a marker covers it andiat <= cutoff. This also covers tokens the proxy hasn't seen before the logout.This is the marker design @rhafer suggested, with two differences:
iatcutoff. Keycloak keeps issuing tokens with the samesidto a client with an offline token after a logout, and reuses thesidfor a new login after a consent revocation. A plain marker would reject those tokens until it expires.iat, not the time of receipt. Both timestamps then come from the IDP clock, and replaying a logout token doesn't change the cutoff.Behavior changes
iatare rejected.sid,subandiat. A token withoutsidisn't covered by a session logout. A token withoutiatis rejected if a logout covers it.PROXY_OIDC_MAX_ACCESS_TOKEN_LIFETIME(default 24h). Markers are kept for this duration plus 5 minutes. A JWT access token that lives longer or has noexpis still accepted, and the proxy logs a warning because the token can outlive its marker.PROXY_OIDC_SKIP_USER_INFO=truethere is nothing to ask, so the request is rejected. A corrupt marker always rejects.cache-userinfotocache-oidc-tokens, which starts with a fresh cache. Markers use their own database with the suffix-logout-markers, because nats-js-kv only supports one TTL per bucket.The README lists
PROXY_OIDC_SKIP_USER_INFO=falseas a requirement for a reliable logout.Cost
Every authenticated request reads up to two markers, one for the session and one for the subject, also on a cache hit. With nats-js-kv these are two more round trips per request. I haven't measured this.
Open questions
iatcutoff for session markers. My comment from Nov 4 has the comparison with other implementations.revoke_offline_accessin the logout token?webhas a backchannel logout URL in the realms of this repo and ofopencloud-compose. Should the desktop and mobile clients get one?Commit sequence
Related Issue
Motivation and Context
Backchannel logout must invalidate the tokens of the affected session, including tokens issued before a refresh and tokens verified without a userinfo request.
Keycloak also sends no logout token in some cases, so the proxy can't react to them. These gaps exist today and this PR doesn't fix them: clients without a backchannel URL, a realm-wide "logout all", and on 26.8.1 a browser logout of a session with only an offline client.
How Has This Been Tested?
Unit and HTTP regression tests:
End to end with a real OpenCloud server and Keycloak 25.0.0 and 26.8.1. On current
main, tokens stay accepted after logout. With this PR they are rejected, and other sessions and users aren't affected. These runs are from before the last two changes, the lifetime warning and the userinfo fallback. Both are covered by unit and HTTP tests only.End-to-end runs in detail
Test copy of the realm (no LDAP, plain HTTP, extra test clients), scripted clients. Memory, NATS and noop stores, each with
PROXY_OIDC_SKIP_USER_INFOon and off. In all 12 runs:sid.PROXY_OIDC_ACCESS_TOKEN_VERIFY_METHOD=none, results are identical tomain.Not covered or not resolved:
backchannel-logoutevent didn't arrive on an SSE stream, onmainor with this PR. I don't know the cause yet. A unit test confirms the event is published with the right user and session.Types of changes
Checklist
Assisted-By: LLM