ruma_client_api/session/
get_login_types.rs

1//! `GET /_matrix/client/*/login`
2//!
3//! Gets the homeserver's supported login types to authenticate users. Clients should pick one of
4//! these and supply it as the type when logging in.
5
6pub mod v3 {
7    //! `/v3/` ([spec])
8    //!
9    //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login
10
11    use std::borrow::Cow;
12
13    use ruma_common::{
14        OwnedMxcUri,
15        api::{auth_scheme::NoAuthentication, request, response},
16        metadata,
17        serde::{JsonObject, StringEnum},
18    };
19    use serde::{Deserialize, Serialize, de::DeserializeOwned};
20    use serde_json::Value as JsonValue;
21
22    use crate::PrivOwnedStr;
23
24    metadata! {
25        method: GET,
26        rate_limited: true,
27        authentication: NoAuthentication,
28        history: {
29            1.0 => "/_matrix/client/r0/login",
30            1.1 => "/_matrix/client/v3/login",
31        }
32    }
33
34    /// Request type for the `get_login_types` endpoint.
35    #[request(error = crate::Error)]
36    #[derive(Default)]
37    pub struct Request {}
38
39    /// Response type for the `get_login_types` endpoint.
40    #[response(error = crate::Error)]
41    pub struct Response {
42        /// The homeserver's supported login types.
43        pub flows: Vec<LoginType>,
44    }
45
46    impl Request {
47        /// Creates an empty `Request`.
48        pub fn new() -> Self {
49            Self {}
50        }
51    }
52
53    impl Response {
54        /// Creates a new `Response` with the given login types.
55        pub fn new(flows: Vec<LoginType>) -> Self {
56            Self { flows }
57        }
58    }
59
60    /// An authentication mechanism.
61    #[derive(Clone, Debug, Serialize)]
62    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
63    #[serde(untagged)]
64    pub enum LoginType {
65        /// A password is supplied to authenticate.
66        Password(PasswordLoginType),
67
68        /// Token-based login.
69        Token(TokenLoginType),
70
71        /// SSO-based login.
72        Sso(SsoLoginType),
73
74        /// Application Service login.
75        ApplicationService(ApplicationServiceLoginType),
76
77        /// Custom login type.
78        #[doc(hidden)]
79        _Custom(Box<CustomLoginType>),
80    }
81
82    impl LoginType {
83        /// Creates a new `LoginType` with the given `login_type` string and data.
84        ///
85        /// Prefer to use the public variants of `LoginType` where possible; this constructor is
86        /// meant be used for unsupported login types only and does not allow setting
87        /// arbitrary data for supported ones.
88        pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
89            fn from_json_object<T: DeserializeOwned>(obj: JsonObject) -> serde_json::Result<T> {
90                serde_json::from_value(JsonValue::Object(obj))
91            }
92
93            Ok(match login_type {
94                "m.login.password" => Self::Password(from_json_object(data)?),
95                "m.login.token" => Self::Token(from_json_object(data)?),
96                "m.login.sso" => Self::Sso(from_json_object(data)?),
97                "m.login.application_service" => Self::ApplicationService(from_json_object(data)?),
98                _ => {
99                    Self::_Custom(Box::new(CustomLoginType { type_: login_type.to_owned(), data }))
100                }
101            })
102        }
103
104        /// Returns a reference to the `login_type` string.
105        pub fn login_type(&self) -> &str {
106            match self {
107                Self::Password(_) => "m.login.password",
108                Self::Token(_) => "m.login.token",
109                Self::Sso(_) => "m.login.sso",
110                Self::ApplicationService(_) => "m.login.application_service",
111                Self::_Custom(c) => &c.type_,
112            }
113        }
114
115        /// Returns the associated data.
116        ///
117        /// Prefer to use the public variants of `LoginType` where possible; this method is meant to
118        /// be used for unsupported login types only.
119        pub fn data(&self) -> Cow<'_, JsonObject> {
120            fn serialize<T: Serialize>(obj: &T) -> JsonObject {
121                match serde_json::to_value(obj).expect("login type serialization to succeed") {
122                    JsonValue::Object(obj) => obj,
123                    _ => panic!("all login types must serialize to objects"),
124                }
125            }
126
127            match self {
128                Self::Password(d) => Cow::Owned(serialize(d)),
129                Self::Token(d) => Cow::Owned(serialize(d)),
130                Self::Sso(d) => Cow::Owned(serialize(d)),
131                Self::ApplicationService(d) => Cow::Owned(serialize(d)),
132                Self::_Custom(c) => Cow::Borrowed(&c.data),
133            }
134        }
135    }
136
137    /// The payload for password login.
138    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
139    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
140    #[serde(tag = "type", rename = "m.login.password")]
141    pub struct PasswordLoginType {}
142
143    impl PasswordLoginType {
144        /// Creates a new `PasswordLoginType`.
145        pub fn new() -> Self {
146            Self {}
147        }
148    }
149
150    /// The payload for token-based login.
151    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
152    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
153    #[serde(tag = "type", rename = "m.login.token")]
154    pub struct TokenLoginType {
155        /// Whether the homeserver supports the `POST /login/get_token` endpoint.
156        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
157        pub get_login_token: bool,
158    }
159
160    impl TokenLoginType {
161        /// Creates a new `TokenLoginType`.
162        pub fn new() -> Self {
163            Self { get_login_token: false }
164        }
165    }
166
167    /// The payload for SSO login.
168    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
169    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
170    #[serde(tag = "type", rename = "m.login.sso")]
171    pub struct SsoLoginType {
172        /// The identity provider choices.
173        #[serde(default, skip_serializing_if = "Vec::is_empty")]
174        pub identity_providers: Vec<IdentityProvider>,
175
176        /// Whether this flow is preferred over other flows.
177        ///
178        /// If this is `true`, [OAuth 2.0 aware] clients must only offer this flow to the user.
179        ///
180        /// [OAuth 2.0 aware]: https://github.com/matrix-org/matrix-spec-proposals/pull/3824
181        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
182        pub oauth_aware_preferred: bool,
183    }
184
185    impl SsoLoginType {
186        /// Creates a new `SsoLoginType`.
187        pub fn new() -> Self {
188            Self::default()
189        }
190    }
191
192    /// An SSO login identity provider.
193    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
194    #[derive(Clone, Debug, Deserialize, Serialize)]
195    pub struct IdentityProvider {
196        /// The ID of the provider.
197        pub id: String,
198
199        /// The display name of the provider.
200        pub name: String,
201
202        /// The icon for the provider.
203        pub icon: Option<OwnedMxcUri>,
204
205        /// The brand identifier for the provider.
206        pub brand: Option<IdentityProviderBrand>,
207    }
208
209    impl IdentityProvider {
210        /// Creates an `IdentityProvider` with the given `id` and `name`.
211        pub fn new(id: String, name: String) -> Self {
212            Self { id, name, icon: None, brand: None }
213        }
214    }
215
216    /// An SSO login identity provider brand identifier.
217    ///
218    /// The predefined ones can be found in the matrix-spec-proposals repo in a [separate
219    /// document][matrix-spec-proposals].
220    ///
221    /// [matrix-spec-proposals]: https://github.com/matrix-org/matrix-spec-proposals/blob/v1.1/informal/idp-brands.md
222    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
223    #[derive(Clone, StringEnum)]
224    #[ruma_enum(rename_all = "lowercase")]
225    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
226    pub enum IdentityProviderBrand {
227        /// The [Apple] brand.
228        ///
229        /// [Apple]: https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/
230        Apple,
231
232        /// The [Facebook](https://developers.facebook.com/docs/facebook-login/web/login-button/) brand.
233        Facebook,
234
235        /// The [GitHub](https://github.com/logos) brand.
236        GitHub,
237
238        /// The [GitLab](https://about.gitlab.com/press/press-kit/) brand.
239        GitLab,
240
241        /// The [Google](https://developers.google.com/identity/branding-guidelines) brand.
242        Google,
243
244        /// The [Twitter] brand.
245        ///
246        /// [Twitter]: https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter#tab1
247        Twitter,
248
249        /// A custom brand.
250        #[doc(hidden)]
251        _Custom(PrivOwnedStr),
252    }
253
254    /// The payload for Application Service login.
255    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
256    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
257    #[serde(tag = "type", rename = "m.login.application_service")]
258    pub struct ApplicationServiceLoginType {}
259
260    impl ApplicationServiceLoginType {
261        /// Creates a new `ApplicationServiceLoginType`.
262        pub fn new() -> Self {
263            Self::default()
264        }
265    }
266
267    /// A custom login payload.
268    #[doc(hidden)]
269    #[derive(Clone, Debug, Deserialize, Serialize)]
270    #[allow(clippy::exhaustive_structs)]
271    pub struct CustomLoginType {
272        /// A custom type
273        ///
274        /// This field is named `type_` instead of `type` because the latter is a reserved
275        /// keyword in Rust.
276        #[serde(rename = "type")]
277        pub type_: String,
278
279        /// Remaining type content
280        #[serde(flatten)]
281        pub data: JsonObject,
282    }
283
284    mod login_type_serde {
285        use ruma_common::serde::from_raw_json_value;
286        use serde::{Deserialize, de};
287        use serde_json::value::RawValue as RawJsonValue;
288
289        use super::LoginType;
290
291        /// Helper struct to determine the type from a `serde_json::value::RawValue`
292        #[derive(Debug, Deserialize)]
293        struct LoginTypeDeHelper {
294            /// The login type field
295            #[serde(rename = "type")]
296            type_: String,
297        }
298
299        impl<'de> Deserialize<'de> for LoginType {
300            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
301            where
302                D: de::Deserializer<'de>,
303            {
304                let json = Box::<RawJsonValue>::deserialize(deserializer)?;
305                let LoginTypeDeHelper { type_ } = from_raw_json_value(&json)?;
306
307                Ok(match type_.as_ref() {
308                    "m.login.password" => Self::Password(from_raw_json_value(&json)?),
309                    "m.login.token" => Self::Token(from_raw_json_value(&json)?),
310                    "m.login.sso" => Self::Sso(from_raw_json_value(&json)?),
311                    "m.login.application_service" => {
312                        Self::ApplicationService(from_raw_json_value(&json)?)
313                    }
314                    _ => Self::_Custom(from_raw_json_value(&json)?),
315                })
316            }
317        }
318    }
319
320    #[cfg(test)]
321    mod tests {
322        use assert_matches2::assert_matches;
323        use ruma_common::{canonical_json::assert_to_canonical_json_eq, mxc_uri};
324        use serde::{Deserialize, Serialize};
325        use serde_json::{Value as JsonValue, from_value as from_json_value, json};
326
327        use super::{
328            IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
329        };
330
331        #[derive(Debug, Deserialize, Serialize)]
332        struct Wrapper {
333            flows: Vec<LoginType>,
334        }
335
336        #[test]
337        fn deserialize_password_login_type() {
338            let wrapper = from_json_value::<Wrapper>(json!({
339                "flows": [
340                    { "type": "m.login.password" }
341                ],
342            }))
343            .unwrap();
344            assert_eq!(wrapper.flows.len(), 1);
345            assert_matches!(&wrapper.flows[0], LoginType::Password(_));
346        }
347
348        #[test]
349        fn deserialize_custom_login_type() {
350            let wrapper = from_json_value::<Wrapper>(json!({
351                "flows": [
352                    {
353                        "type": "io.ruma.custom",
354                        "color": "green",
355                    }
356                ],
357            }))
358            .unwrap();
359            assert_eq!(wrapper.flows.len(), 1);
360            assert_matches!(&wrapper.flows[0], LoginType::_Custom(custom));
361            assert_eq!(custom.type_, "io.ruma.custom");
362            assert_eq!(custom.data.len(), 1);
363            assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
364        }
365
366        #[test]
367        fn deserialize_sso_login_type() {
368            let wrapper = from_json_value::<Wrapper>(json!({
369                "flows": [
370                    {
371                        "type": "m.login.sso",
372                        "identity_providers": [
373                            {
374                                "id": "oidc-gitlab",
375                                "name": "GitLab",
376                                "icon": "mxc://localhost/gitlab-icon",
377                                "brand": "gitlab"
378                            },
379                            {
380                                "id": "custom",
381                                "name": "Custom",
382                            }
383                        ]
384                    }
385                ],
386            }))
387            .unwrap();
388            assert_eq!(wrapper.flows.len(), 1);
389            let flow = &wrapper.flows[0];
390
391            assert_matches!(
392                flow,
393                LoginType::Sso(SsoLoginType { identity_providers, oauth_aware_preferred: false })
394            );
395            assert_eq!(identity_providers.len(), 2);
396
397            let provider = &identity_providers[0];
398            assert_eq!(provider.id, "oidc-gitlab");
399            assert_eq!(provider.name, "GitLab");
400            assert_eq!(provider.icon.as_deref(), Some(mxc_uri!("mxc://localhost/gitlab-icon")));
401            assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
402
403            let provider = &identity_providers[1];
404            assert_eq!(provider.id, "custom");
405            assert_eq!(provider.name, "Custom");
406            assert_eq!(provider.icon, None);
407            assert_eq!(provider.brand, None);
408        }
409
410        #[test]
411        fn serialize_sso_login_type() {
412            let wrapper = Wrapper {
413                flows: vec![
414                    LoginType::Token(TokenLoginType::new()),
415                    LoginType::Sso(SsoLoginType {
416                        identity_providers: vec![IdentityProvider {
417                            id: "oidc-github".into(),
418                            name: "GitHub".into(),
419                            icon: Some("mxc://localhost/github-icon".into()),
420                            brand: Some(IdentityProviderBrand::GitHub),
421                        }],
422                        oauth_aware_preferred: false,
423                    }),
424                ],
425            };
426
427            assert_to_canonical_json_eq!(
428                wrapper,
429                json!({
430                    "flows": [
431                        {
432                            "type": "m.login.token"
433                        },
434                        {
435                            "type": "m.login.sso",
436                            "identity_providers": [
437                                {
438                                    "id": "oidc-github",
439                                    "name": "GitHub",
440                                    "icon": "mxc://localhost/github-icon",
441                                    "brand": "github"
442                                },
443                            ]
444                        }
445                    ],
446                })
447            );
448        }
449    }
450}