client-api: Implement MSC2858 - Multiple SSO Identity Providers

This commit is contained in:
Kévin Commaille 2021-04-14 17:17:55 +02:00 committed by GitHub
parent c1693569f1
commit 51951082d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 411 additions and 51 deletions

View File

@ -78,6 +78,7 @@ Breaking changes:
search::{search_events, search_users} search::{search_events, search_users}
} }
``` ```
* Change `r0::session::get_login_types::LoginType` to a non-exhaustive enum of structs.
Improvements: Improvements:
@ -97,6 +98,9 @@ Improvements:
get_content_thumbnail 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 # 0.9.0

View File

@ -5,3 +5,4 @@ pub mod login;
pub mod logout; pub mod logout;
pub mod logout_all; pub mod logout_all;
pub mod sso_login; pub mod sso_login;
pub mod sso_login_with_provider;

View File

@ -1,7 +1,16 @@
//! [GET /_matrix/client/r0/login](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-login) //! [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; use ruma_api::ruma_api;
#[cfg(feature = "unstable-pre-spec")]
use ruma_identifiers::MxcUri;
#[cfg(feature = "unstable-pre-spec")]
use ruma_serde::StringEnum; use ruma_serde::StringEnum;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
type JsonObject = serde_json::Map<String, JsonValue>;
ruma_api! { ruma_api! {
metadata: { metadata: {
@ -18,7 +27,6 @@ ruma_api! {
response: { response: {
/// The homeserver's supported login types. /// The homeserver's supported login types.
#[serde(with = "login_type_list_serde")]
pub flows: Vec<LoginType>, pub flows: Vec<LoginType>,
} }
@ -40,51 +48,327 @@ impl Response {
} }
/// An authentication mechanism. /// 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 { pub enum LoginType {
/// A password is supplied to authenticate. /// A password is supplied to authenticate.
#[ruma_enum(rename = "m.login.password")] Password(PasswordLoginType),
Password,
/// Token-based login. /// Token-based login.
#[ruma_enum(rename = "m.login.token")] Token(TokenLoginType),
Token,
/// SSO-based login. /// SSO-based login.
#[ruma_enum(rename = "m.login.sso")] Sso(SsoLoginType),
Sso,
/// 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<Self> {
fn from_json_object<T: DeserializeOwned>(obj: JsonObject) -> serde_json::Result<T> {
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<T: 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<IdentityProvider>,
}
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<MxcUri>,
/// The brand identifier for the provider.
brand: Option<IdentityProviderBrand>,
}
#[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)] #[doc(hidden)]
_Custom(String), _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)] #[cfg(test)]
mod tests { mod tests {
use matches::assert_matches; use serde::{Deserialize, Serialize};
use serde::Deserialize; #[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 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 { struct Foo {
#[serde(with = "login_type_list_serde")]
pub flows: Vec<LoginType>, pub flows: Vec<LoginType>,
} }
#[test] #[test]
fn deserialize_login_type() { fn deserialize_password_login_type() {
assert_matches!( assert_eq!(
from_json_value::<Foo>(json!({ from_json_value::<Foo>(json!({
"flows": [ "flows": [
{ "type": "m.login.password" } { "type": "m.login.password" }
], ],
})), }))
Ok(Foo { flows }) .unwrap(),
if flows.len() == 1 Foo { flows: vec![LoginType::Password(PasswordLoginType {})] }
&& flows[0] == LoginType::Password );
}
#[test]
#[cfg(feature = "unstable-pre-spec")]
fn deserialize_sso_login_type() {
let foo = from_json_value::<Foo>(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"
},
]
}
],
})
); );
} }
} }

View File

@ -1,31 +0,0 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::LoginType;
pub fn serialize<S>(login_types: &[LoginType], serializer: S) -> Result<S::Ok, S::Error>
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<Vec<LoginType>, 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::<Wrap>::deserialize(deserializer)?;
Ok(vec.into_iter().map(|w| w.inner).collect())
}

View File

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::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)?),
})
}
}

View File

@ -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"
);
}
}