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.18/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.18/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_matches2::{assert_let, assert_matches};
326        use ruma_common::{canonical_json::assert_to_canonical_json_eq, mxc_uri};
327        use serde::{Deserialize, Serialize};
328        use serde_json::{Value as JsonValue, from_value as from_json_value, json};
329
330        use super::{
331            IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
332        };
333
334        #[derive(Debug, Deserialize, Serialize)]
335        struct Wrapper {
336            flows: Vec<LoginType>,
337        }
338
339        #[test]
340        fn deserialize_password_login_type() {
341            let wrapper = from_json_value::<Wrapper>(json!({
342                "flows": [
343                    { "type": "m.login.password" }
344                ],
345            }))
346            .unwrap();
347            assert_eq!(wrapper.flows.len(), 1);
348            assert_matches!(&wrapper.flows[0], LoginType::Password(_));
349        }
350
351        #[test]
352        fn custom_login_type_serialize_roundtrip() {
353            let json = json!({
354                "flows": [
355                    {
356                        "type": "io.ruma.custom",
357                        "color": "green",
358                    }
359                ],
360            });
361
362            let wrapper = from_json_value::<Wrapper>(json.clone()).unwrap();
363            assert_eq!(wrapper.flows.len(), 1);
364            assert_let!(LoginType::_Custom(custom) = &wrapper.flows[0]);
365            assert_eq!(custom.login_type, "io.ruma.custom");
366            assert_eq!(custom.data.len(), 1);
367            assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
368
369            assert_to_canonical_json_eq!(wrapper, json);
370        }
371
372        #[test]
373        fn deserialize_sso_login_type() {
374            let wrapper = from_json_value::<Wrapper>(json!({
375                "flows": [
376                    {
377                        "type": "m.login.sso",
378                        "identity_providers": [
379                            {
380                                "id": "oidc-gitlab",
381                                "name": "GitLab",
382                                "icon": "mxc://localhost/gitlab-icon",
383                                "brand": "gitlab"
384                            },
385                            {
386                                "id": "custom",
387                                "name": "Custom",
388                            }
389                        ]
390                    }
391                ],
392            }))
393            .unwrap();
394            assert_eq!(wrapper.flows.len(), 1);
395            let flow = &wrapper.flows[0];
396
397            assert_let!(
398                LoginType::Sso(SsoLoginType { identity_providers, oauth_aware_preferred: false }) =
399                    flow
400            );
401            assert_eq!(identity_providers.len(), 2);
402
403            let provider = &identity_providers[0];
404            assert_eq!(provider.id, "oidc-gitlab");
405            assert_eq!(provider.name, "GitLab");
406            assert_eq!(provider.icon.as_deref(), Some(mxc_uri!("mxc://localhost/gitlab-icon")));
407            assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
408
409            let provider = &identity_providers[1];
410            assert_eq!(provider.id, "custom");
411            assert_eq!(provider.name, "Custom");
412            assert_eq!(provider.icon, None);
413            assert_eq!(provider.brand, None);
414        }
415
416        #[test]
417        fn serialize_sso_login_type() {
418            let wrapper = Wrapper {
419                flows: vec![
420                    LoginType::Token(TokenLoginType::new()),
421                    LoginType::Sso(SsoLoginType {
422                        identity_providers: vec![IdentityProvider {
423                            id: "oidc-github".into(),
424                            name: "GitHub".into(),
425                            icon: Some("mxc://localhost/github-icon".into()),
426                            brand: Some(IdentityProviderBrand::GitHub),
427                        }],
428                        oauth_aware_preferred: false,
429                    }),
430                ],
431            };
432
433            assert_to_canonical_json_eq!(
434                wrapper,
435                json!({
436                    "flows": [
437                        {
438                            "type": "m.login.token"
439                        },
440                        {
441                            "type": "m.login.sso",
442                            "identity_providers": [
443                                {
444                                    "id": "oidc-github",
445                                    "name": "GitHub",
446                                    "icon": "mxc://localhost/github-icon",
447                                    "brand": "github"
448                                },
449                            ]
450                        }
451                    ],
452                })
453            );
454        }
455    }
456}