From 65807dfb735398cd653e91d6dc81e371c2c267aa Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Sun, 27 Mar 2022 23:10:10 +0200 Subject: [PATCH 01/14] add new `SessionIdentifier` type for `sid` claim --- src/types.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/types.rs b/src/types.rs index c04b2cd6..0cd84145 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1055,6 +1055,16 @@ new_type![ SubjectIdentifier(String) ]; +new_type![ + /// + /// String identifier for a Session. This represents a Session of a User + /// Agent or device for a logged-in End-User at an RP. Different sid values + /// are used to identify distinct sessions at an OP. The sid value need + /// only be unique in the context of a particular issuer. + #[derive(Deserialize, Eq, Hash, Ord, PartialOrd, Serialize)] + SessionIdentifier(String) +]; + new_url_type![ /// /// URL for the relying party's Terms of Service. From 3a10ef36b482cc7834f437c1e7e99a75a28a113d Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Sun, 27 Mar 2022 23:10:45 +0200 Subject: [PATCH 02/14] add feature flag for backchannel logout --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index abf54390..8eae52f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["reqwest", "rustls-tls"] +default = ["reqwest", "rustls-tls", "backchannel-logout"] curl = ["oauth2/curl"] reqwest = ["oauth2/reqwest"] ureq = ["oauth2/ureq"] @@ -23,6 +23,7 @@ native-tls = ["oauth2/native-tls"] rustls-tls = ["oauth2/rustls-tls"] accept-rfc3339-timestamps = [] nightly = [] +backchannel-logout = [] [dependencies] base64 = "0.13" From 091b675b0322154b8d5c0ddf0d3324589bc2c344 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Sun, 27 Mar 2022 23:14:04 +0200 Subject: [PATCH 03/14] implement logout token deserialization --- src/lib.rs | 6 + src/logout_token.rs | 472 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 478 insertions(+) create mode 100644 src/logout_token.rs diff --git a/src/lib.rs b/src/lib.rs index 45bbf2c8..aa023fe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -596,6 +596,12 @@ pub use oauth2::{ TokenResponse as OAuth2TokenResponse, TokenType, TokenUrl, }; +#[cfg(feature = "backchannel-logout")] +mod logout_token; +#[cfg(feature = "backchannel-logout")] +#[doc(inline)] +pub use logout_token::*; + /// /// Public re-exports of types used for HTTP client interfaces. /// diff --git a/src/logout_token.rs b/src/logout_token.rs new file mode 100644 index 00000000..79d4ce68 --- /dev/null +++ b/src/logout_token.rs @@ -0,0 +1,472 @@ +//! This module implements components needed for [back-channel logout] +//! +//! [back-channel logout]: + +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{de::Error, Deserialize}; + +use crate::{ + types::helpers::{deserialize_string_or_vec, serde_utc_seconds}, + types::SessionIdentifier, + Audience, IssuerUrl, SubjectIdentifier, +}; + +/// The Logout Token as defined in [section 2.4] of the [OpenID Connect Back-Channel Logout spec][1] +/// +/// [section 2.4]: +/// [1]: +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogoutToken { + /// The issuer of this token + iss: IssuerUrl, + /// The audience this token is intended for + aud: Vec, + /// Time at which this token was issued + iat: DateTime, + /// The unique identifier for this token. This can be used to detect + /// replay attacks. + jti: String, + identifier: Identifier, + events: HashMap, +} + +impl LogoutToken { + /// The `iss` claim + pub fn issuer(&self) -> &IssuerUrl { + &self.iss + } + + /// The `aud` claim + pub fn audiences(&self) -> impl Iterator { + self.aud.iter() + } + + /// The `iat` claim + pub fn issue_time(&self) -> DateTime { + self.iat + } + + /// The `jti` claim. It's the unique identifier for this token and can be + /// used to detect replay attacks. + pub fn jti(&self) -> &str { + &self.jti + } + + /// As per spec, a [`LogoutToken`] MUST either have the `sub` or `sid` + /// claim and MAY contain both. You can match the [`Identifier`] to detect + /// which claims are present. + pub fn identifier(&self) -> &Identifier { + &self.identifier + } + + /// A [`LogoutToken`] is compatible with the [SET standard from RFC 8417][1] + /// + /// [1]: + pub fn events(&self) -> &HashMap { + &self.events + } +} +impl<'de> Deserialize<'de> for LogoutToken { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; + if let serde_json::Value::Object(ref map) = value { + if map.contains_key("nonce") { + return Err(::custom("nonce claim is prohibited")); + } + } + + #[derive(Deserialize)] + struct Repr { + /// The issuer of this token + iss: IssuerUrl, + /// The audience this token is intended for + #[serde(deserialize_with = "deserialize_string_or_vec")] + aud: Vec, + /// Time at which this token was issued + #[serde(with = "serde_utc_seconds")] + iat: DateTime, + /// The unique identifier for this token. This can be used to detect + /// replay attacks. + jti: String, + #[serde(flatten)] + identifier: Identifier, + events: HashMap, + } + + let token: Repr = serde_json::from_value(value).map_err(::custom)?; + + token + .events + // according to the spec, this event must be included in the mapping + .get("http://schemas.openid.net/event/backchannel-logout") + .ok_or_else(|| { + ::custom("token is missing correct JSON Object in events claim") + })? + // and it must be a JSON object and MAY BE empty but is allowed to + // contain fields + .as_object() + .ok_or_else(|| ::custom("not a JSON Object"))?; + Ok(LogoutToken { + iss: token.iss, + aud: token.aud, + iat: token.iat, + jti: token.jti, + identifier: token.identifier, + events: token.events, + }) + } +} + +/// A [`LogoutToken`] MUST contain either a `sub` or a `sid` claim and MAY +/// contain both. This enum represents these three possibilities. +#[derive(Debug, Hash, Clone, PartialEq, Eq)] +pub enum Identifier { + /// Both, the `sid` and `sub` claims are present + Both { + /// The `sub` claim as in [`Identifier::Subject`] + subject: SubjectIdentifier, + /// The `sid` claim as in [`Identifier::Subject`] + session: SessionIdentifier, + }, + /// Only the `sid` claim is present + Session(SessionIdentifier), + /// Only the `sub` claim is present + Subject(SubjectIdentifier), +} + +impl Identifier { + /// Directly return the [`SubjectIdentifier`] if the variant is either + /// [`Identifier::Subject`] or [`Identifier::Both`] + pub fn subject(&self) -> Option<&SubjectIdentifier> { + match self { + Self::Subject(s) => Some(s), + Self::Both { + subject, + session: _, + } => Some(subject), + Self::Session(_) => None, + } + } + + /// Directly return the [`SessionIdentifier`] if the variant is either + /// [`Identifier::Session`] or [`Identifier::Both`] + pub fn session(&self) -> Option<&SessionIdentifier> { + match self { + Self::Subject(_) => None, + Self::Both { + subject: _, + session, + } => Some(session), + Self::Session(s) => Some(s), + } + } +} + +// serde does not have #[serde(flatten)] on enums with struct variants, so +impl<'de> Deserialize<'de> for Identifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Both claims are set + #[derive(Deserialize)] + struct Both { + sub: SubjectIdentifier, + sid: SessionIdentifier, + } + + // Only one claim is set + #[derive(Deserialize)] + enum SidOrSub { + #[serde(rename = "sid")] + Session(SessionIdentifier), + #[serde(rename = "sub")] + Subject(SubjectIdentifier), + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum Either { + Both(Both), + Single(SidOrSub), + } + + Ok(match Either::deserialize(deserializer)? { + Either::Both(both) => Identifier::Both { + subject: both.sub, + session: both.sid, + }, + Either::Single(s) => match s { + SidOrSub::Subject(s) => Identifier::Subject(s), + SidOrSub::Session(s) => Identifier::Session(s), + }, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::Identifier; + + use super::LogoutToken; + + #[test] + fn deserialize_only_sid() { + let t: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + "http://schemas.openid.net/event/backchannel-logout": {} + } + } + "#, + ) + .unwrap(); + assert!(matches!(t.identifier(), Identifier::Session(_))); + } + + #[test] + fn deserialize_only_sub() { + let t: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "events": { + "http://schemas.openid.net/event/backchannel-logout": {} + } + } + "#, + ) + .unwrap(); + assert!(matches!(t.identifier(), Identifier::Subject(_))); + } + + #[test] + #[should_panic] + fn deserialize_missing_identifier() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "events": { + "http://schemas.openid.net/event/backchannel-logout": {} + } + } + "#, + ) + .unwrap(); + } + + #[test] + fn deserialize_valid() { + let t: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + "http://schemas.openid.net/event/backchannel-logout": {} + } + } + "#, + ) + .unwrap(); + println!("{:#?}", t.identifier()); + assert!(matches!( + t.identifier(), + Identifier::Both { + subject: _, + session: _ + } + )) + } + + #[test] + #[should_panic] + fn deserialize_events_empty() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + } + } + "#, + ) + .unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_events_empty_array() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": [] + } + "#, + ) + .unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_events_missing() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02" + } + "#, + ) + .unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_events_array() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": [ + {"http://schemas.openid.net/event/backchannel-logout": {}} + ], + "nonce": "snsuigdbnfcjkn" + } + "#, + ) + .unwrap(); + } + #[test] + #[should_panic] + fn deserialize_nonce() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + "http://schemas.openid.net/event/backchannel-logout": {} + }, + "nonce": "snsuigdbnfcjkn" + } + "#, + ) + .unwrap(); + } + + #[test] + fn deserialize_extra_field() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + "http://schemas.openid.net/event/backchannel-logout": { + "foo": "bar" + } + } + } + "#, + ) + .unwrap(); + } + + #[test] + fn deserialize_multiple_events() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + "http://schemas.openid.net/event/backchannel-logout": {}, + "http://schemas.example.org/event/foo": {} + } + } + "#, + ) + .unwrap(); + } + + #[test] + fn deserialize_multiple_events_extra_fields() { + let _: LogoutToken = serde_json::from_str( + r#" + { + "iss": "https://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "iat": 1471566154, + "jti": "bWJq", + "sid": "08a5019c-17e1-4977-8f42-65a12843ea02", + "events": { + "http://schemas.openid.net/event/backchannel-logout": { + "foo": "bar" + }, + "http://schemas.example.org/events/something": { + "faz": true + } + } + } + "#, + ) + .unwrap(); + } +} From 9ab43047b4f9667e58ef5fb97bda8ae6c1cd1b50 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Sun, 27 Mar 2022 23:16:02 +0200 Subject: [PATCH 04/14] remove print from test case --- src/logout_token.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/logout_token.rs b/src/logout_token.rs index 79d4ce68..0190081d 100644 --- a/src/logout_token.rs +++ b/src/logout_token.rs @@ -292,7 +292,6 @@ mod tests { "#, ) .unwrap(); - println!("{:#?}", t.identifier()); assert!(matches!( t.identifier(), Identifier::Both { From 3785aa5aba3743aca041a33764e3401ba7815cf2 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Sun, 27 Mar 2022 23:52:04 +0200 Subject: [PATCH 05/14] rename logout_token module and remove re-export --- src/{logout_token.rs => backchannel_logout.rs} | 4 +--- src/lib.rs | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) rename src/{logout_token.rs => backchannel_logout.rs} (99%) diff --git a/src/logout_token.rs b/src/backchannel_logout.rs similarity index 99% rename from src/logout_token.rs rename to src/backchannel_logout.rs index 0190081d..4e878e58 100644 --- a/src/logout_token.rs +++ b/src/backchannel_logout.rs @@ -211,9 +211,7 @@ impl<'de> Deserialize<'de> for Identifier { #[cfg(test)] mod tests { - use crate::Identifier; - - use super::LogoutToken; + use super::{Identifier, LogoutToken}; #[test] fn deserialize_only_sid() { diff --git a/src/lib.rs b/src/lib.rs index aa023fe4..def5ccef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -597,10 +597,7 @@ pub use oauth2::{ }; #[cfg(feature = "backchannel-logout")] -mod logout_token; -#[cfg(feature = "backchannel-logout")] -#[doc(inline)] -pub use logout_token::*; +mod backchannel_logout; /// /// Public re-exports of types used for HTTP client interfaces. From d184068442381e3e6ee1dd9ae54eb3ade5facf3a Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Sun, 27 Mar 2022 23:55:01 +0200 Subject: [PATCH 06/14] make `backchannel_logout` module public --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index def5ccef..94a17950 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -597,7 +597,7 @@ pub use oauth2::{ }; #[cfg(feature = "backchannel-logout")] -mod backchannel_logout; +pub mod backchannel_logout; /// /// Public re-exports of types used for HTTP client interfaces. From f0c82e5b38e4c19452526a43010739c6e907580b Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Thu, 31 Mar 2022 18:41:27 +0200 Subject: [PATCH 07/14] add `backchannel_logout_supported` and `backchannel_logout_session_supported` to `ProviderMeta` --- src/discovery.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/discovery.rs b/src/discovery.rs index b7a58951..3a395554 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -173,6 +173,13 @@ where #[serde(skip_serializing_if = "Option::is_none")] op_tos_uri: Option, + // backchannel logout support + // see + #[serde(skip_serializing_if = "Option::is_none")] + backchannel_logout_supported: Option, + #[serde(skip_serializing_if = "Option::is_none")] + backchannel_logout_session_supported: Option, + #[serde(bound(deserialize = "A: AdditionalProviderMetadata"), flatten)] additional_metadata: A, @@ -247,6 +254,8 @@ where require_request_uri_registration: None, op_policy_uri: None, op_tos_uri: None, + backchannel_logout_supported: None, + backchannel_logout_session_supported: None, additional_metadata, _phantom_jt: PhantomData, } @@ -302,6 +311,8 @@ where set_require_request_uri_registration -> require_request_uri_registration[Option], set_op_policy_uri -> op_policy_uri[Option], set_op_tos_uri -> op_tos_uri[Option], + set_backchannel_logout_supported -> backchannel_logout_supported[Option], + set_backchannel_logout_session_supported -> backchannel_logout_session_supported[Option], } ]; From a377bd8d2ae22cc46f0cdd313e0e744460e34a28 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Thu, 31 Mar 2022 18:46:32 +0200 Subject: [PATCH 08/14] move pub mod down to core --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 94a17950..2fdd4739 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -596,9 +596,6 @@ pub use oauth2::{ TokenResponse as OAuth2TokenResponse, TokenType, TokenUrl, }; -#[cfg(feature = "backchannel-logout")] -pub mod backchannel_logout; - /// /// Public re-exports of types used for HTTP client interfaces. /// @@ -658,6 +655,9 @@ mod macros; /// Baseline OpenID Connect implementation and types. pub mod core; +#[cfg(feature = "backchannel-logout")] +pub mod backchannel_logout; + /// OpenID Connect Dynamic Client Registration. pub mod registration; From cde3bc2dc5d1f465bec6feac52880f9f15840a72 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Thu, 31 Mar 2022 19:22:26 +0200 Subject: [PATCH 09/14] use JsonWebKeyId instead of String for jti claim --- src/backchannel_logout.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backchannel_logout.rs b/src/backchannel_logout.rs index 4e878e58..d1e70a2c 100644 --- a/src/backchannel_logout.rs +++ b/src/backchannel_logout.rs @@ -10,7 +10,7 @@ use serde::{de::Error, Deserialize}; use crate::{ types::helpers::{deserialize_string_or_vec, serde_utc_seconds}, types::SessionIdentifier, - Audience, IssuerUrl, SubjectIdentifier, + Audience, IssuerUrl, JsonWebKeyId, SubjectIdentifier, }; /// The Logout Token as defined in [section 2.4] of the [OpenID Connect Back-Channel Logout spec][1] @@ -27,7 +27,7 @@ pub struct LogoutToken { iat: DateTime, /// The unique identifier for this token. This can be used to detect /// replay attacks. - jti: String, + jti: JsonWebKeyId, identifier: Identifier, events: HashMap, } @@ -50,7 +50,7 @@ impl LogoutToken { /// The `jti` claim. It's the unique identifier for this token and can be /// used to detect replay attacks. - pub fn jti(&self) -> &str { + pub fn jti(&self) -> &JsonWebKeyId { &self.jti } @@ -92,7 +92,7 @@ impl<'de> Deserialize<'de> for LogoutToken { iat: DateTime, /// The unique identifier for this token. This can be used to detect /// replay attacks. - jti: String, + jti: JsonWebKeyId, #[serde(flatten)] identifier: Identifier, events: HashMap, From 742f1085028c70cf94e3ccff46f743a18ebd1164 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Fri, 1 Apr 2022 14:50:15 +0200 Subject: [PATCH 10/14] rename `Identifier` to `LogoutIdentifier` --- src/backchannel_logout.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/backchannel_logout.rs b/src/backchannel_logout.rs index d1e70a2c..d6443f2c 100644 --- a/src/backchannel_logout.rs +++ b/src/backchannel_logout.rs @@ -28,7 +28,7 @@ pub struct LogoutToken { /// The unique identifier for this token. This can be used to detect /// replay attacks. jti: JsonWebKeyId, - identifier: Identifier, + identifier: LogoutIdentifier, events: HashMap, } @@ -57,7 +57,7 @@ impl LogoutToken { /// As per spec, a [`LogoutToken`] MUST either have the `sub` or `sid` /// claim and MAY contain both. You can match the [`Identifier`] to detect /// which claims are present. - pub fn identifier(&self) -> &Identifier { + pub fn identifier(&self) -> &LogoutIdentifier { &self.identifier } @@ -94,7 +94,7 @@ impl<'de> Deserialize<'de> for LogoutToken { /// replay attacks. jti: JsonWebKeyId, #[serde(flatten)] - identifier: Identifier, + identifier: LogoutIdentifier, events: HashMap, } @@ -125,7 +125,7 @@ impl<'de> Deserialize<'de> for LogoutToken { /// A [`LogoutToken`] MUST contain either a `sub` or a `sid` claim and MAY /// contain both. This enum represents these three possibilities. #[derive(Debug, Hash, Clone, PartialEq, Eq)] -pub enum Identifier { +pub enum LogoutIdentifier { /// Both, the `sid` and `sub` claims are present Both { /// The `sub` claim as in [`Identifier::Subject`] @@ -139,7 +139,7 @@ pub enum Identifier { Subject(SubjectIdentifier), } -impl Identifier { +impl LogoutIdentifier { /// Directly return the [`SubjectIdentifier`] if the variant is either /// [`Identifier::Subject`] or [`Identifier::Both`] pub fn subject(&self) -> Option<&SubjectIdentifier> { @@ -168,7 +168,7 @@ impl Identifier { } // serde does not have #[serde(flatten)] on enums with struct variants, so -impl<'de> Deserialize<'de> for Identifier { +impl<'de> Deserialize<'de> for LogoutIdentifier { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -197,13 +197,13 @@ impl<'de> Deserialize<'de> for Identifier { } Ok(match Either::deserialize(deserializer)? { - Either::Both(both) => Identifier::Both { + Either::Both(both) => LogoutIdentifier::Both { subject: both.sub, session: both.sid, }, Either::Single(s) => match s { - SidOrSub::Subject(s) => Identifier::Subject(s), - SidOrSub::Session(s) => Identifier::Session(s), + SidOrSub::Subject(s) => LogoutIdentifier::Subject(s), + SidOrSub::Session(s) => LogoutIdentifier::Session(s), }, }) } @@ -211,7 +211,7 @@ impl<'de> Deserialize<'de> for Identifier { #[cfg(test)] mod tests { - use super::{Identifier, LogoutToken}; + use super::{LogoutIdentifier, LogoutToken}; #[test] fn deserialize_only_sid() { @@ -230,7 +230,7 @@ mod tests { "#, ) .unwrap(); - assert!(matches!(t.identifier(), Identifier::Session(_))); + assert!(matches!(t.identifier(), LogoutIdentifier::Session(_))); } #[test] @@ -250,7 +250,7 @@ mod tests { "#, ) .unwrap(); - assert!(matches!(t.identifier(), Identifier::Subject(_))); + assert!(matches!(t.identifier(), LogoutIdentifier::Subject(_))); } #[test] @@ -292,7 +292,7 @@ mod tests { .unwrap(); assert!(matches!( t.identifier(), - Identifier::Both { + LogoutIdentifier::Both { subject: _, session: _ } From 94213f0449b89fb5d6829af15579f42b543e1900 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Fri, 1 Apr 2022 14:56:35 +0200 Subject: [PATCH 11/14] rename `LogoutToken` to `LogoutTokenClaims` to be consistence with `IdTokenClaims` --- src/backchannel_logout.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/backchannel_logout.rs b/src/backchannel_logout.rs index d6443f2c..0a757f9d 100644 --- a/src/backchannel_logout.rs +++ b/src/backchannel_logout.rs @@ -18,7 +18,7 @@ use crate::{ /// [section 2.4]: /// [1]: #[derive(Debug, Clone, PartialEq, Eq)] -pub struct LogoutToken { +pub struct LogoutTokenClaims { /// The issuer of this token iss: IssuerUrl, /// The audience this token is intended for @@ -32,7 +32,7 @@ pub struct LogoutToken { events: HashMap, } -impl LogoutToken { +impl LogoutTokenClaims { /// The `iss` claim pub fn issuer(&self) -> &IssuerUrl { &self.iss @@ -68,7 +68,7 @@ impl LogoutToken { &self.events } } -impl<'de> Deserialize<'de> for LogoutToken { +impl<'de> Deserialize<'de> for LogoutTokenClaims { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -111,7 +111,7 @@ impl<'de> Deserialize<'de> for LogoutToken { // contain fields .as_object() .ok_or_else(|| ::custom("not a JSON Object"))?; - Ok(LogoutToken { + Ok(LogoutTokenClaims { iss: token.iss, aud: token.aud, iat: token.iat, @@ -211,11 +211,11 @@ impl<'de> Deserialize<'de> for LogoutIdentifier { #[cfg(test)] mod tests { - use super::{LogoutIdentifier, LogoutToken}; + use super::{LogoutIdentifier, LogoutTokenClaims}; #[test] fn deserialize_only_sid() { - let t: LogoutToken = serde_json::from_str( + let t: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -235,7 +235,7 @@ mod tests { #[test] fn deserialize_only_sub() { - let t: LogoutToken = serde_json::from_str( + let t: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -256,7 +256,7 @@ mod tests { #[test] #[should_panic] fn deserialize_missing_identifier() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -274,7 +274,7 @@ mod tests { #[test] fn deserialize_valid() { - let t: LogoutToken = serde_json::from_str( + let t: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -302,7 +302,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_empty() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -322,7 +322,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_empty_array() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -341,7 +341,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_missing() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -359,7 +359,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_array() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -380,7 +380,7 @@ mod tests { #[test] #[should_panic] fn deserialize_nonce() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -401,7 +401,7 @@ mod tests { #[test] fn deserialize_extra_field() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -423,7 +423,7 @@ mod tests { #[test] fn deserialize_multiple_events() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -444,7 +444,7 @@ mod tests { #[test] fn deserialize_multiple_events_extra_fields() { - let _: LogoutToken = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", From f9c62e8ca7dd3d3f2930fb0e78cfcf7cb5f82492 Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Fri, 1 Apr 2022 15:20:04 +0200 Subject: [PATCH 12/14] allow additional claims in `LogoutTokenClaims` --- src/backchannel_logout.rs | 64 +++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/backchannel_logout.rs b/src/backchannel_logout.rs index 0a757f9d..13010184 100644 --- a/src/backchannel_logout.rs +++ b/src/backchannel_logout.rs @@ -8,17 +8,18 @@ use chrono::{DateTime, Utc}; use serde::{de::Error, Deserialize}; use crate::{ + helpers::{FilteredFlatten, FlattenFilter}, types::helpers::{deserialize_string_or_vec, serde_utc_seconds}, types::SessionIdentifier, - Audience, IssuerUrl, JsonWebKeyId, SubjectIdentifier, + AdditionalClaims, Audience, IssuerUrl, JsonWebKeyId, SubjectIdentifier, }; /// The Logout Token as defined in [section 2.4] of the [OpenID Connect Back-Channel Logout spec][1] /// /// [section 2.4]: /// [1]: -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LogoutTokenClaims { +#[derive(Debug, Clone, PartialEq)] +pub struct LogoutTokenClaims { /// The issuer of this token iss: IssuerUrl, /// The audience this token is intended for @@ -30,9 +31,25 @@ pub struct LogoutTokenClaims { jti: JsonWebKeyId, identifier: LogoutIdentifier, events: HashMap, + additional_claims: FilteredFlatten, } -impl LogoutTokenClaims { +impl FlattenFilter for LogoutTokenClaims +where + AC: AdditionalClaims, +{ + fn should_include(field_name: &str) -> bool { + !matches!( + field_name, + "iss" | "aud" | "iat" | "jti" | "sub" | "sid" | "events" + ) + } +} + +impl LogoutTokenClaims +where + AC: AdditionalClaims, +{ /// The `iss` claim pub fn issuer(&self) -> &IssuerUrl { &self.iss @@ -68,7 +85,10 @@ impl LogoutTokenClaims { &self.events } } -impl<'de> Deserialize<'de> for LogoutTokenClaims { +impl<'de, AC> Deserialize<'de> for LogoutTokenClaims +where + AC: AdditionalClaims, +{ fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -81,7 +101,7 @@ impl<'de> Deserialize<'de> for LogoutTokenClaims { } #[derive(Deserialize)] - struct Repr { + struct Repr { /// The issuer of this token iss: IssuerUrl, /// The audience this token is intended for @@ -96,9 +116,12 @@ impl<'de> Deserialize<'de> for LogoutTokenClaims { #[serde(flatten)] identifier: LogoutIdentifier, events: HashMap, + #[serde(bound = "AC: AdditionalClaims")] + #[serde(flatten)] + additional_claims: FilteredFlatten, AC>, } - let token: Repr = serde_json::from_value(value).map_err(::custom)?; + let token: Repr = serde_json::from_value(value).map_err(::custom)?; token .events @@ -118,6 +141,7 @@ impl<'de> Deserialize<'de> for LogoutTokenClaims { jti: token.jti, identifier: token.identifier, events: token.events, + additional_claims: token.additional_claims, }) } } @@ -211,11 +235,13 @@ impl<'de> Deserialize<'de> for LogoutIdentifier { #[cfg(test)] mod tests { + use crate::EmptyAdditionalClaims; + use super::{LogoutIdentifier, LogoutTokenClaims}; #[test] fn deserialize_only_sid() { - let t: LogoutTokenClaims = serde_json::from_str( + let t: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -235,7 +261,7 @@ mod tests { #[test] fn deserialize_only_sub() { - let t: LogoutTokenClaims = serde_json::from_str( + let t: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -256,7 +282,7 @@ mod tests { #[test] #[should_panic] fn deserialize_missing_identifier() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -274,7 +300,7 @@ mod tests { #[test] fn deserialize_valid() { - let t: LogoutTokenClaims = serde_json::from_str( + let t: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -302,7 +328,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_empty() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -322,7 +348,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_empty_array() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -341,7 +367,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_missing() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -359,7 +385,7 @@ mod tests { #[test] #[should_panic] fn deserialize_events_array() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -380,7 +406,7 @@ mod tests { #[test] #[should_panic] fn deserialize_nonce() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -401,7 +427,7 @@ mod tests { #[test] fn deserialize_extra_field() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -423,7 +449,7 @@ mod tests { #[test] fn deserialize_multiple_events() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", @@ -444,7 +470,7 @@ mod tests { #[test] fn deserialize_multiple_events_extra_fields() { - let _: LogoutTokenClaims = serde_json::from_str( + let _: LogoutTokenClaims = serde_json::from_str( r#" { "iss": "https://server.example.com", From 351cda09f76e42ab9b76f11f66e3bf64beb3877a Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Fri, 1 Apr 2022 17:44:35 +0200 Subject: [PATCH 13/14] implement serde serialization implementations for backchannel logout --- src/backchannel_logout.rs | 56 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/src/backchannel_logout.rs b/src/backchannel_logout.rs index 13010184..1311f814 100644 --- a/src/backchannel_logout.rs +++ b/src/backchannel_logout.rs @@ -5,30 +5,58 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; -use serde::{de::Error, Deserialize}; +use serde::{de::Error, ser::SerializeMap, Deserialize, Serialize}; use crate::{ helpers::{FilteredFlatten, FlattenFilter}, + jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde}, types::helpers::{deserialize_string_or_vec, serde_utc_seconds}, types::SessionIdentifier, - AdditionalClaims, Audience, IssuerUrl, JsonWebKeyId, SubjectIdentifier, + AdditionalClaims, Audience, IssuerUrl, JsonWebKeyId, JsonWebKeyType, + JweContentEncryptionAlgorithm, JwsSigningAlgorithm, SubjectIdentifier, }; -/// The Logout Token as defined in [section 2.4] of the [OpenID Connect Back-Channel Logout spec][1] +/// Back-Channel Logout Token +/// +/// Parses a JWT as a Logout Token as definied in [section 2.4] /// /// [section 2.4]: -/// [1]: #[derive(Debug, Clone, PartialEq)] +pub struct LogoutToken( + JsonWebToken, JsonWebTokenJsonPayloadSerde>, +) +where + AC: AdditionalClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType; + +impl LogoutToken +where + AC: AdditionalClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + // TODO: implement signature verification & friends +} + +/// The Logout Token Claims as defined in [section 2.4] of the [OpenID Connect Back-Channel Logout spec][1] +/// +/// [1]: +#[derive(Debug, Clone, PartialEq, Serialize)] pub struct LogoutTokenClaims { /// The issuer of this token iss: IssuerUrl, /// The audience this token is intended for aud: Vec, /// Time at which this token was issued + #[serde(with = "serde_utc_seconds")] iat: DateTime, /// The unique identifier for this token. This can be used to detect /// replay attacks. jti: JsonWebKeyId, + #[serde(flatten)] identifier: LogoutIdentifier, events: HashMap, additional_claims: FilteredFlatten, @@ -233,6 +261,26 @@ impl<'de> Deserialize<'de> for LogoutIdentifier { } } +impl Serialize for LogoutIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let len = self.session().is_some() as usize + self.subject().is_some() as usize; + + let mut map = serializer.serialize_map(Some(len))?; + + if let Some(s) = self.session() { + map.serialize_entry("sid", s)?; + } + + if let Some(s) = self.subject() { + map.serialize_entry("sub", s)?; + } + + map.end() + } +} #[cfg(test)] mod tests { use crate::EmptyAdditionalClaims; From 126fa10abc68bbdcc5c687948d31e69792e8d0ab Mon Sep 17 00:00:00 2001 From: Erik Tesar Date: Fri, 1 Apr 2022 23:35:49 +0200 Subject: [PATCH 14/14] filter `LogoutIdentifier` instead of whole `LogoutTokenClaims` --- src/backchannel_logout.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/backchannel_logout.rs b/src/backchannel_logout.rs index 1311f814..4839bcd0 100644 --- a/src/backchannel_logout.rs +++ b/src/backchannel_logout.rs @@ -59,19 +59,7 @@ pub struct LogoutTokenClaims { #[serde(flatten)] identifier: LogoutIdentifier, events: HashMap, - additional_claims: FilteredFlatten, -} - -impl FlattenFilter for LogoutTokenClaims -where - AC: AdditionalClaims, -{ - fn should_include(field_name: &str) -> bool { - !matches!( - field_name, - "iss" | "aud" | "iat" | "jti" | "sub" | "sid" | "events" - ) - } + additional_claims: FilteredFlatten, } impl LogoutTokenClaims @@ -146,7 +134,7 @@ where events: HashMap, #[serde(bound = "AC: AdditionalClaims")] #[serde(flatten)] - additional_claims: FilteredFlatten, AC>, + additional_claims: FilteredFlatten, } let token: Repr = serde_json::from_value(value).map_err(::custom)?; @@ -219,6 +207,11 @@ impl LogoutIdentifier { } } +impl FlattenFilter for LogoutIdentifier { + fn should_include(field_name: &str) -> bool { + !matches!(field_name, "sub" | "sid") + } +} // serde does not have #[serde(flatten)] on enums with struct variants, so impl<'de> Deserialize<'de> for LogoutIdentifier { fn deserialize(deserializer: D) -> Result