diff --git a/ruma-client-api/CHANGELOG.md b/ruma-client-api/CHANGELOG.md index 7d048e1d..2350d7ed 100644 --- a/ruma-client-api/CHANGELOG.md +++ b/ruma-client-api/CHANGELOG.md @@ -78,6 +78,7 @@ Breaking changes: search::{search_events, search_users} } ``` +* Change `r0::session::get_login_types::LoginType` to a non-exhaustive enum of structs. Improvements: @@ -97,6 +98,9 @@ Improvements: get_content_thumbnail } ``` +* Implement MSC2858 - Multiple SSO Identity Providers under the `unstable-pre-spec` feature flag: + * Add the `r0::session::get_login_types::{IdentityProvider, IdentityProviderBrand}` types + * Add the `r0::session::sso_login_with_provider` endpoint # 0.9.0 diff --git a/ruma-client-api/src/r0/session.rs b/ruma-client-api/src/r0/session.rs index e1df60b8..905a28d2 100644 --- a/ruma-client-api/src/r0/session.rs +++ b/ruma-client-api/src/r0/session.rs @@ -5,3 +5,4 @@ pub mod login; pub mod logout; pub mod logout_all; pub mod sso_login; +pub mod sso_login_with_provider; diff --git a/ruma-client-api/src/r0/session/get_login_types.rs b/ruma-client-api/src/r0/session/get_login_types.rs index 2c8a032e..fa034529 100644 --- a/ruma-client-api/src/r0/session/get_login_types.rs +++ b/ruma-client-api/src/r0/session/get_login_types.rs @@ -1,7 +1,16 @@ //! [GET /_matrix/client/r0/login](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-login) +use std::borrow::Cow; + use ruma_api::ruma_api; +#[cfg(feature = "unstable-pre-spec")] +use ruma_identifiers::MxcUri; +#[cfg(feature = "unstable-pre-spec")] use ruma_serde::StringEnum; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +type JsonObject = serde_json::Map; ruma_api! { metadata: { @@ -18,7 +27,6 @@ ruma_api! { response: { /// The homeserver's supported login types. - #[serde(with = "login_type_list_serde")] pub flows: Vec, } @@ -40,51 +48,327 @@ impl Response { } /// An authentication mechanism. -#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(untagged)] pub enum LoginType { /// A password is supplied to authenticate. - #[ruma_enum(rename = "m.login.password")] - Password, + Password(PasswordLoginType), /// Token-based login. - #[ruma_enum(rename = "m.login.token")] - Token, + Token(TokenLoginType), /// SSO-based login. - #[ruma_enum(rename = "m.login.sso")] - Sso, + Sso(SsoLoginType), + /// Custom login type. + #[doc(hidden)] + _Custom(CustomLoginType), +} + +impl LoginType { + /// Creates a `LoginType` with the given `login_type` string and data. + /// + /// Prefer to use the public variants of `LoginType` where possible; this constructor is meant + /// be used for unsupported login types only and does not allow setting arbitrary data for + /// supported ones. + pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result { + fn from_json_object(obj: JsonObject) -> serde_json::Result { + serde_json::from_value(JsonValue::Object(obj)) + } + + Ok(match login_type { + "m.login.password" => Self::Password(from_json_object(data)?), + "m.login.token" => Self::Token(from_json_object(data)?), + "m.login.sso" => Self::Sso(from_json_object(data)?), + _ => Self::_Custom(CustomLoginType { type_: login_type.to_owned(), data }), + }) + } + + /// Returns a reference to the `login_type` string. + pub fn login_type(&self) -> &str { + match self { + Self::Password(_) => "m.login.password", + Self::Token(_) => "m.login.token", + Self::Sso(_) => "m.login.sso", + Self::_Custom(c) => &c.type_, + } + } + + /// Returns the associated data. + /// + /// Prefer to use the public variants of `LoginType` where possible; this method is meant to + /// be used for unsupported login types only. + pub fn data(&self) -> Cow<'_, JsonObject> { + fn serialize(obj: &T) -> JsonObject { + match serde_json::to_value(obj).expect("login type serialization to succeed") { + JsonValue::Object(obj) => obj, + _ => panic!("all login types must serialize to objects"), + } + } + + match self { + Self::Password(d) => Cow::Owned(serialize(d)), + Self::Token(d) => Cow::Owned(serialize(d)), + Self::Sso(d) => Cow::Owned(serialize(d)), + Self::_Custom(c) => Cow::Borrowed(&c.data), + } + } +} + +/// The payload for password login. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "type", rename = "m.login.password")] +pub struct PasswordLoginType {} + +impl PasswordLoginType { + /// Creates a new `PasswordLoginType`. + pub fn new() -> Self { + Self {} + } +} + +/// The payload for token-based login. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "type", rename = "m.login.token")] +pub struct TokenLoginType {} + +impl TokenLoginType { + /// Creates a new `PasswordLoginType`. + pub fn new() -> Self { + Self {} + } +} + +/// The payload for SSO login. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "type", rename = "m.login.sso")] +pub struct SsoLoginType { + /// The identity provider choices. + /// + /// This uses the unstable prefix in + /// [MSC2858](https://github.com/matrix-org/matrix-doc/pull/2858). + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + #[serde( + default, + rename = "org.matrix.msc2858.identity_providers", + skip_serializing_if = "Vec::is_empty" + )] + pub identity_providers: Vec, +} + +impl SsoLoginType { + /// Creates a new `PasswordLoginType`. + pub fn new() -> Self { + Self::default() + } +} + +/// An SSO login identity provider. +#[cfg(feature = "unstable-pre-spec")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct IdentityProvider { + /// The ID of the provider. + id: String, + + /// The display name of the provider. + name: String, + + /// The icon for the provider. + icon: Option, + + /// The brand identifier for the provider. + brand: Option, +} + +#[cfg(feature = "unstable-pre-spec")] +impl IdentityProvider { + /// Creates an `IdentityProvider` with the given `id` and `name`. + pub fn new(id: String, name: String) -> Self { + Self { id, name, icon: None, brand: None } + } +} + +/// An SSO login identity provider brand identifier. +/// +/// This uses the unstable prefix in +/// [MSC2858](https://github.com/matrix-org/matrix-doc/pull/2858). +#[cfg(feature = "unstable-pre-spec")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] +#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub enum IdentityProviderBrand { + /// The [Apple] brand. + /// + /// [Apple]: https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/ + #[ruma_enum(rename = "org.matrix.apple")] + Apple, + + /// The [Facebook](https://developers.facebook.com/docs/facebook-login/web/login-button/) brand. + #[ruma_enum(rename = "org.matrix.facebook")] + Facebook, + + /// The [GitHub](https://github.com/logos) brand. + #[ruma_enum(rename = "org.matrix.github")] + GitHub, + + /// The [GitLab](https://about.gitlab.com/press/press-kit/) brand. + #[ruma_enum(rename = "org.matrix.gitlab")] + GitLab, + + /// The [Google](https://developers.google.com/identity/branding-guidelines) brand. + #[ruma_enum(rename = "org.matrix.google")] + Google, + + /// The [Twitter] brand. + /// + /// [Twitter]: https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter#tab1 + #[ruma_enum(rename = "org.matrix.twitter")] + Twitter, + + /// A custom brand. #[doc(hidden)] _Custom(String), } -mod login_type_list_serde; +/// A custom login payload. +#[doc(hidden)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct CustomLoginType { + /// A custom type + /// + /// This field is named `type_` instead of `type` because the latter is a reserved + /// keyword in Rust. + #[serde(rename = "override")] + pub type_: String, + + /// Remaining type content + #[serde(flatten)] + pub data: JsonObject, +} + +mod login_type_serde; #[cfg(test)] mod tests { - use matches::assert_matches; - use serde::Deserialize; + use serde::{Deserialize, Serialize}; + #[cfg(feature = "unstable-pre-spec")] + use serde_json::to_value as to_json_value; use serde_json::{from_value as from_json_value, json}; - use super::{login_type_list_serde, LoginType}; + #[cfg(feature = "unstable-pre-spec")] + use super::{IdentityProvider, IdentityProviderBrand, SsoLoginType, TokenLoginType}; + use super::{LoginType, PasswordLoginType}; - #[derive(Debug, Deserialize)] + #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] struct Foo { - #[serde(with = "login_type_list_serde")] pub flows: Vec, } #[test] - fn deserialize_login_type() { - assert_matches!( + fn deserialize_password_login_type() { + assert_eq!( from_json_value::(json!({ "flows": [ { "type": "m.login.password" } ], - })), - Ok(Foo { flows }) - if flows.len() == 1 - && flows[0] == LoginType::Password + })) + .unwrap(), + Foo { flows: vec![LoginType::Password(PasswordLoginType {})] } + ); + } + + #[test] + #[cfg(feature = "unstable-pre-spec")] + fn deserialize_sso_login_type() { + let foo = from_json_value::(json!({ + "flows": [ + { + "type": "m.login.sso", + "org.matrix.msc2858.identity_providers": [ + { + "id": "oidc-gitlab", + "name": "GitLab", + "icon": "mxc://localhost/gitlab-icon", + "brand": "org.matrix.gitlab" + }, + { + "id": "custom", + "name": "Custom", + } + ] + } + ], + })) + .unwrap(); + + assert_eq!( + foo, + Foo { + flows: vec![LoginType::Sso(SsoLoginType { + identity_providers: vec![ + IdentityProvider { + id: "oidc-gitlab".into(), + name: "GitLab".into(), + icon: Some("mxc://localhost/gitlab-icon".into()), + brand: Some(IdentityProviderBrand::GitLab) + }, + IdentityProvider { + id: "custom".into(), + name: "Custom".into(), + icon: None, + brand: None + } + ] + })] + } + ) + } + + #[test] + #[cfg(feature = "unstable-pre-spec")] + fn serialize_sso_login_type() { + let foo = to_json_value(Foo { + flows: vec![ + LoginType::Token(TokenLoginType {}), + LoginType::Sso(SsoLoginType { + identity_providers: vec![IdentityProvider { + id: "oidc-github".into(), + name: "GitHub".into(), + icon: Some("mxc://localhost/github-icon".into()), + brand: Some(IdentityProviderBrand::GitHub), + }], + }), + ], + }) + .unwrap(); + + assert_eq!( + foo, + json!({ + "flows": [ + { + "type": "m.login.token" + }, + { + "type": "m.login.sso", + "org.matrix.msc2858.identity_providers": [ + { + "id": "oidc-github", + "name": "GitHub", + "icon": "mxc://localhost/github-icon", + "brand": "org.matrix.github" + }, + ] + } + ], + }) ); } } diff --git a/ruma-client-api/src/r0/session/get_login_types/login_type_list_serde.rs b/ruma-client-api/src/r0/session/get_login_types/login_type_list_serde.rs deleted file mode 100644 index 00ed78f2..00000000 --- a/ruma-client-api/src/r0/session/get_login_types/login_type_list_serde.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use super::LoginType; - -pub fn serialize(login_types: &[LoginType], serializer: S) -> Result -where - S: Serializer, -{ - #[derive(Serialize)] - struct Wrap<'a> { - #[serde(rename = "type")] - inner: &'a LoginType, - } - - serializer.collect_seq(login_types.iter().map(|ty| Wrap { inner: ty })) -} - -pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - struct Wrap { - #[serde(rename = "type")] - inner: LoginType, - } - - // Could be optimized by using a visitor, but that's a bunch of extra code - let vec = Vec::::deserialize(deserializer)?; - Ok(vec.into_iter().map(|w| w.inner).collect()) -} diff --git a/ruma-client-api/src/r0/session/get_login_types/login_type_serde.rs b/ruma-client-api/src/r0/session/get_login_types/login_type_serde.rs new file mode 100644 index 00000000..7758de5a --- /dev/null +++ b/ruma-client-api/src/r0/session/get_login_types/login_type_serde.rs @@ -0,0 +1,31 @@ +use serde::{de, Deserialize}; +use serde_json::value::RawValue as RawJsonValue; + +use ruma_events::from_raw_json_value; + +use super::LoginType; + +/// Helper struct to determine the type from a `serde_json::value::RawValue` +#[derive(Debug, Deserialize)] +struct LoginTypeDeHelper { + /// The login type field + #[serde(rename = "type")] + type_: String, +} + +impl<'de> de::Deserialize<'de> for LoginType { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = Box::::deserialize(deserializer)?; + let LoginTypeDeHelper { type_ } = from_raw_json_value(&json)?; + + Ok(match type_.as_ref() { + "m.login.password" => Self::Password(from_raw_json_value(&json)?), + "m.login.token" => Self::Token(from_raw_json_value(&json)?), + "m.login.sso" => Self::Sso(from_raw_json_value(&json)?), + _ => Self::_Custom(from_raw_json_value(&json)?), + }) + } +} diff --git a/ruma-client-api/src/r0/session/sso_login_with_provider.rs b/ruma-client-api/src/r0/session/sso_login_with_provider.rs new file mode 100644 index 00000000..c7e26919 --- /dev/null +++ b/ruma-client-api/src/r0/session/sso_login_with_provider.rs @@ -0,0 +1,71 @@ +//! [GET /_matrix/client/r0/login/sso/redirect/{idp_id}](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2858-Multiple-SSO-Identity-Providers.md) +//! +//! This uses the unstable prefix in [MSC2858](https://github.com/matrix-org/matrix-doc/pull/2858). +#![cfg(feature = "unstable-pre-spec")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + +use ruma_api::ruma_api; + +ruma_api! { + metadata: { + description: "Get the SSO login identity provider url.", + method: GET, + name: "sso_login_with_provider", + path: "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect/:idp_id", + rate_limited: false, + authentication: None, + } + + request: { + /// The ID of the provider to use for SSO login. + #[ruma_api(path)] + pub idp_id: &'a str, + + /// URL to which the homeserver should return the user after completing + /// authentication with the SSO identity provider. + #[ruma_api(query)] + #[serde(rename = "redirectUrl")] + pub redirect_url: &'a str, + } + + response: { + /// Redirect URL to the SSO identity provider. + #[ruma_api(header = LOCATION)] + pub location: String, + } + + error: crate::Error +} + +impl<'a> Request<'a> { + /// Creates a new `Request` with the given identity provider ID and redirect URL. + pub fn new(idp_id: &'a str, redirect_url: &'a str) -> Self { + Self { idp_id, redirect_url } + } +} + +impl Response { + /// Creates a new `Response` with the given SSO URL. + pub fn new(location: String) -> Self { + Self { location } + } +} + +#[cfg(test)] +mod tests { + use ruma_api::OutgoingRequest; + + use super::Request; + + #[test] + fn serialize_sso_login_with_provider_request_uri() { + let req = Request { idp_id: "provider", redirect_url: "https://example.com/sso" } + .try_into_http_request("https://homeserver.tld", None) + .unwrap(); + + assert_eq!( + req.uri().to_string(), + "https://homeserver.tld/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect/provider?redirectUrl=https%3A%2F%2Fexample.com%2Fsso" + ); + } +}