-
-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backchannel logout #70
base: main
Are you sure you want to change the base?
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
d7f325e
to
348f500
Compare
now that I merged #69, mind rebasing and force pushing (or merging |
d52976c
to
9ab4304
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR!
As part of this feature, I think it's worth adding the backchannel_logout_supported
and backchannel_logout_session_supported
fields to ProviderMetadata
(per Section 2.1). Since the contents of that struct are private and we only need to add new getters and setters, this change should be backward compatible.
Similarly, the spec defines backchannel_logout_uri
and backchannel_logout_session_required
for ClientMetadata
.
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading the spec correctly, I think this struct should be parsed as a JWT rather than raw JSON, using similar code to IdToken
:
OPs send a JWT similar to an ID Token to RPs called a Logout Token to request that they log out.
The code should also verify the JWT signature:
A Logout Token MUST be signed and MAY also be encrypted
I'd suggest structuring the code similarly to what this crate does for ID tokens:
LogoutTokenClaims
struct similar toIdTokenClaims
(implementing bothDeserialize
andSerialize
so that both the RP and OP code paths are supported)LogoutToken
struct similar toIdToken
that wrapsJsonWebToken
LogoutTokenVerifier
struct similar toIdTokenVerifier
orUserInfoVerifier
that contains aJwtClaimsVerifier
and makes sure that anonce
is omitted. I think I would just add an optionalnonce
field (annotated withserde(skip_serializing)
so that it doesn't get serialized by an OP) toLogoutTokenClaims
have the verifier check that it'sNone
. There are some other required verification steps in the spec, including foriat
(seeIdTokenVerifier::iat_verifier_fn
).JwtClaimsVerifier
will take care ofiss
andaud
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading the spec correctly, I think this struct should be parsed as a JWT rather than raw JSON
You're totally right and it's because I planned on implementing this a bit differently than IdToken
but guess I'll better keep the style of this crate then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not feeling so good implementing an extra LogoutTokenVerifier
because it's basically copy-paste except for the nonce
part. Maybe there's a way to be more generics about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm... I tried to move all of the shared JWT verification logic to JwtClaimsVerifier
. The duplicated part here would mostly be the factory methods (e.g., new_public_client
) and getters/setters I think. I'm not sure how to get around that extra boilerplate. There shouldn't be too much duplicated logic though, other than verifying iat
, which is simple enough.
I think I'd start by implementing this verifier with all of the needed functionality, and then maybe it'll be clear whether there are any opportunities to factor out common code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright then, I'll see what I can do tomorrow.
src/backchannel_logout.rs
Outdated
} | ||
|
||
#[derive(Deserialize)] | ||
struct Repr { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the suggestions above, I think the duplicate Repr
and custom Deserialize
implementation can probably be removed in favor of serde annotations where needed
jti: String, | ||
#[serde(flatten)] | ||
identifier: Identifier, | ||
events: HashMap<String, serde_json::Value>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using types, we can have serde check the URL key for us:
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct BackchannelLogoutEvent {}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct LogoutTokenEvents {
#[serde(rename = "http://schemas.openid.net/event/backchannel-logout")]
backchannel_logout: BackchannelLogoutEvent,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I thought about this and came to the conclusion that it's not a good idea if not done properly. But in order to to this right, I guess we need to completely implement the SET standard and have the different events typed up in something like an enum. Also, I think this
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] // <--- would violate the spec
struct BackchannelLogoutEvent {}
since the spec says about this (Section 2.4):
The corresponding member value MUST be a JSON object and SHOULD be the empty JSON object {}.
it only SHOULD not MUST be empty, so we can't deny unknown fields, can we?
I've only added the check here to ensure that we only parse tokens that are valid to the spec. I've even considered completely ignoring this field apart from making sure it complies to the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, you are correct! Let's remove deny_unknown_fields
then. What if we leave the BackchannelLogoutEvent
struct empty for now but use #[non_exhaustive]
so that we can add fields in the future without a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it make sense to use a enum for LogoutTokenEvents
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enums seem appropriate when we need to distinguish between mutually-exclusive types. In this case, don't we know that it should always contain an event with the http://schemas.openid.net/event/backchannel-logout
key? What other info do you envision needing to represent with LogoutTokenEvents
?
If extensibility is the issue, we could add a generic flattened additional_claims
field. Since there aren't any other flattened fields here, we won't need FilteredFlatten
.
Similarly, we could make backchannel_logout
a generic type instead of defining it as the empty BackchannelLogoutEvent
struct. It depends how extensible we want to get and what future functionality users might want without needing to introduce breaking changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing with this is that it might include other "events" than http://schemas.openid.net/event/backchannel-logout
and as much as I'd like to have all of these typed in an enum or something, it's out of scope for this crate.
I think just exposing the HashMap like it is currently should be fine since I don't think anyone would actually care about this and even if someone does, they could just use serde_json::from_value
to parse their structs.
What other info do you envision needing to represent with LogoutTokenEvents?
Forget that, I thought we could do something like this, but that ain't gonna work:
enum Event {
#[serde(rename = "http://schemas.openid.net/event/backchannel-logout")]
Backchannel {},
// other possible SET event
#[serde(rename = "https://schemas.openid.net/secevent/risc/event-type/account-disabled")]
AccountDisabled {
reason: String
}
}
…pported` to `ProviderMeta`
Codecov Report
@@ Coverage Diff @@
## main #70 +/- ##
==========================================
- Coverage 77.32% 76.96% -0.36%
==========================================
Files 16 17 +1
Lines 3493 3569 +76
==========================================
+ Hits 2701 2747 +46
- Misses 792 822 +30
Continue to review full report at Codecov.
|
Also, I think we should add support for the |
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm... I tried to move all of the shared JWT verification logic to JwtClaimsVerifier
. The duplicated part here would mostly be the factory methods (e.g., new_public_client
) and getters/setters I think. I'm not sure how to get around that extra boilerplate. There shouldn't be too much duplicated logic though, other than verifying iat
, which is simple enough.
I think I'd start by implementing this verifier with all of the needed functionality, and then maybe it'll be clear whether there are any opportunities to factor out common code.
jti: String, | ||
#[serde(flatten)] | ||
identifier: Identifier, | ||
events: HashMap<String, serde_json::Value>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enums seem appropriate when we need to distinguish between mutually-exclusive types. In this case, don't we know that it should always contain an event with the http://schemas.openid.net/event/backchannel-logout
key? What other info do you envision needing to represent with LogoutTokenEvents
?
If extensibility is the issue, we could add a generic flattened additional_claims
field. Since there aren't any other flattened fields here, we won't need FilteredFlatten
.
Similarly, we could make backchannel_logout
a generic type instead of defining it as the empty BackchannelLogoutEvent
struct. It depends how extensible we want to get and what future functionality users might want without needing to introduce breaking changes.
That's worth considering. Feel free to file a separate issue for tracking that. I think it'll be a breaking change though since some users may be relying on |
Yup, I thought exactly about that because I implemented that using an additional claim in one of my projects. |
Hi @Erik1000 / @ramosbugs, I'd like to move this ahead, I'm currently implementing backchannel logout and have started adding functionality to the work done here. Any concerns around the approach or has development stopped because of other work? |
Hi, work here has stopped because the JWT implementation has some issues (including missing support for JWE and it requires some unnecessary code duplication). A friend of mine and me have started a JOSE implementation that tries to solve these problems with a better, more extensible, and powerful way including an rust only crypto backend (which solves #58). We’re aiming for our first stable release within the next two weeks (see minkan-chat/jose#30 and also look at the other branches). Current state is that JWS is working with a great api and we just want to implement an better way for payloads (needed because of detached content). |
Thanks @Erik1000. I've made some progress with implementation of the logout token but will look forward to having a look at your JOSE implementation. |
Ps., but I've seen exactly what you mean, and was why I wanted to implement the backchannel logout within the library, for JWE. |
@KevinNaidoo well, JWE is quite complex and isn't implemented by |
Feels like I'm following in your footsteps... I've been asking the question of but what if the JWT is encrypted, but many people/libraries aren't seeming to be doing that. But it is complex, look forward to your API, makes sense to release without first. I've also decided to proceed without JWEs for now, |
Resolves #68