Skip to main content

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