Skip to main content

ruma_client_api/session/
login.rs

1//! `POST /_matrix/client/*/login`
2//!
3//! Login to the homeserver.
4
5pub mod v3 {
6    //! `/v3/` ([spec])
7    //!
8    //! [spec]: https://spec.matrix.org/v1.18/client-server-api/#post_matrixclientv3login
9
10    use std::{borrow::Cow, fmt, time::Duration};
11
12    use as_variant::as_variant;
13    use ruma_common::{
14        OwnedDeviceId, OwnedServerName, OwnedUserId,
15        api::{auth_scheme::AppserviceTokenOptional, request, response},
16        metadata,
17        serde::JsonObject,
18    };
19    use serde::{
20        Deserialize, Deserializer, Serialize,
21        de::{self, DeserializeOwned},
22    };
23    use serde_json::Value as JsonValue;
24
25    use crate::uiaa::UserIdentifier;
26
27    metadata! {
28        method: POST,
29        rate_limited: true,
30        authentication: AppserviceTokenOptional,
31        history: {
32            1.0 => "/_matrix/client/r0/login",
33            1.1 => "/_matrix/client/v3/login",
34        }
35    }
36
37    /// Request type for the `login` endpoint.
38    #[request]
39    pub struct Request {
40        /// The authentication mechanism.
41        #[serde(flatten)]
42        pub login_info: LoginInfo,
43
44        /// ID of the client device
45        #[serde(skip_serializing_if = "Option::is_none")]
46        pub device_id: Option<OwnedDeviceId>,
47
48        /// A display name to assign to the newly-created device.
49        ///
50        /// Ignored if `device_id` corresponds to a known device.
51        #[serde(skip_serializing_if = "Option::is_none")]
52        pub initial_device_display_name: Option<String>,
53
54        /// If set to `true`, the client supports [refresh tokens].
55        ///
56        /// [refresh tokens]: https://spec.matrix.org/v1.18/client-server-api/#refreshing-access-tokens
57        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
58        pub refresh_token: bool,
59    }
60
61    /// Response type for the `login` endpoint.
62    #[response]
63    pub struct Response {
64        /// The fully-qualified Matrix ID that has been registered.
65        pub user_id: OwnedUserId,
66
67        /// An access token for the account.
68        pub access_token: String,
69
70        /// The hostname of the homeserver on which the account has been registered.
71        #[serde(skip_serializing_if = "Option::is_none")]
72        #[deprecated = "\
73            Since Matrix Client-Server API r0.4.0. Clients should instead use the \
74            `user_id.server_name()` method if they require it.\
75        "]
76        pub home_server: Option<OwnedServerName>,
77
78        /// ID of the logged-in device.
79        ///
80        /// Will be the same as the corresponding parameter in the request, if one was
81        /// specified.
82        pub device_id: OwnedDeviceId,
83
84        /// Client configuration provided by the server.
85        ///
86        /// If present, clients SHOULD use the provided object to reconfigure themselves.
87        #[serde(skip_serializing_if = "Option::is_none")]
88        pub well_known: Option<DiscoveryInfo>,
89
90        /// A [refresh token] for the account.
91        ///
92        /// This token can be used to obtain a new access token when it expires by calling the
93        /// [`refresh_token`] endpoint.
94        ///
95        /// [refresh token]: https://spec.matrix.org/v1.18/client-server-api/#refreshing-access-tokens
96        /// [`refresh_token`]: crate::session::refresh_token
97        #[serde(skip_serializing_if = "Option::is_none")]
98        pub refresh_token: Option<String>,
99
100        /// The lifetime of the access token, in milliseconds.
101        ///
102        /// Once the access token has expired, a new access token can be obtained by using the
103        /// provided refresh token. If no refresh token is provided, the client will need to
104        /// re-login to obtain a new access token.
105        ///
106        /// If this is `None`, the client can assume that the access token will not expire.
107        #[serde(
108            with = "ruma_common::serde::duration::opt_ms",
109            default,
110            skip_serializing_if = "Option::is_none",
111            rename = "expires_in_ms"
112        )]
113        pub expires_in: Option<Duration>,
114    }
115    impl Request {
116        /// Creates a new `Request` with the given login info.
117        pub fn new(login_info: LoginInfo) -> Self {
118            Self {
119                login_info,
120                device_id: None,
121                initial_device_display_name: None,
122                refresh_token: false,
123            }
124        }
125    }
126
127    impl Response {
128        /// Creates a new `Response` with the given user ID, access token and device ID.
129        #[allow(deprecated)]
130        pub fn new(user_id: OwnedUserId, access_token: String, device_id: OwnedDeviceId) -> Self {
131            Self {
132                user_id,
133                access_token,
134                home_server: None,
135                device_id,
136                well_known: None,
137                refresh_token: None,
138                expires_in: None,
139            }
140        }
141    }
142
143    /// The authentication mechanism.
144    #[derive(Clone, Serialize)]
145    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
146    #[serde(untagged)]
147    pub enum LoginInfo {
148        /// An identifier and password are supplied to authenticate.
149        Password(Password),
150
151        /// Token-based login.
152        Token(Token),
153
154        /// Application Service-specific login.
155        ApplicationService(ApplicationService),
156
157        #[doc(hidden)]
158        _Custom(CustomLoginInfo),
159    }
160
161    impl LoginInfo {
162        /// Creates a new `IncomingLoginInfo` with the given `login_type` string, session and data.
163        ///
164        /// Prefer to use the public variants of `IncomingLoginInfo` where possible; this
165        /// constructor is meant be used for unsupported authentication mechanisms only and
166        /// does not allow setting arbitrary data for supported ones.
167        ///
168        /// # Errors
169        ///
170        /// Returns an error if the `login_type` is known and serialization of `data` to the
171        /// corresponding `IncomingLoginInfo` variant fails.
172        pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
173            Ok(match login_type {
174                "m.login.password" => {
175                    Self::Password(serde_json::from_value(JsonValue::Object(data))?)
176                }
177                "m.login.token" => Self::Token(serde_json::from_value(JsonValue::Object(data))?),
178                "m.login.application_service" => {
179                    Self::ApplicationService(serde_json::from_value(JsonValue::Object(data))?)
180                }
181                _ => Self::_Custom(CustomLoginInfo { login_type: login_type.into(), data }),
182            })
183        }
184
185        /// The type of this `LoginInfo`.
186        pub fn login_type(&self) -> &str {
187            match self {
188                LoginInfo::Password(_) => "m.login.password",
189                LoginInfo::Token(_) => "m.login.token",
190                LoginInfo::ApplicationService(_) => "m.login.application_service",
191                LoginInfo::_Custom(c) => &c.login_type,
192            }
193        }
194
195        /// The data of this `LoginInfo`.
196        ///
197        /// Prefer to use the public variants of `LoginInfo` where possible; this method is meant to
198        /// be used for unsupported login types only.
199        pub fn data(&self) -> Cow<'_, JsonObject> {
200            fn serialize<T: Serialize>(obj: &T) -> JsonObject {
201                match serde_json::to_value(obj).expect("login info serialization to succeed") {
202                    JsonValue::Object(obj) => obj,
203                    _ => panic!("all login info variants must serialize to objects"),
204                }
205            }
206
207            match self {
208                Self::Password(d) => Cow::Owned(serialize(d)),
209                Self::Token(d) => Cow::Owned(serialize(d)),
210                Self::ApplicationService(d) => Cow::Owned(serialize(d)),
211                Self::_Custom(c) => Cow::Borrowed(&c.data),
212            }
213        }
214    }
215
216    impl fmt::Debug for LoginInfo {
217        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218            // Print `Password { .. }` instead of `Password(Password { .. })`
219            match self {
220                Self::Password(inner) => inner.fmt(f),
221                Self::Token(inner) => inner.fmt(f),
222                Self::ApplicationService(inner) => inner.fmt(f),
223                Self::_Custom(inner) => inner.fmt(f),
224            }
225        }
226    }
227
228    impl<'de> Deserialize<'de> for LoginInfo {
229        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230        where
231            D: Deserializer<'de>,
232        {
233            fn from_json_value<T: DeserializeOwned, E: de::Error>(val: JsonValue) -> Result<T, E> {
234                serde_json::from_value(val).map_err(E::custom)
235            }
236
237            // FIXME: Would be better to use serde_json::value::RawValue, but that would require
238            // implementing Deserialize manually for Request, bc. `#[serde(flatten)]` breaks things.
239            let mut data = JsonObject::deserialize(deserializer)?;
240
241            let login_type =
242                data["type"].as_str().ok_or_else(|| de::Error::missing_field("type"))?;
243            match login_type {
244                "m.login.password" => from_json_value(data.into()).map(Self::Password),
245                "m.login.token" => from_json_value(data.into()).map(Self::Token),
246                "m.login.application_service" => {
247                    from_json_value(data.into()).map(Self::ApplicationService)
248                }
249                _ => {
250                    let login_type = as_variant!(
251                        data.remove("type")
252                            .expect("we already checked that the object has a type field"),
253                        JsonValue::String
254                    )
255                    .expect("we already checked that the type field is a string");
256                    Ok(Self::_Custom(CustomLoginInfo { login_type, data }))
257                }
258            }
259        }
260    }
261
262    /// An identifier and password to supply as authentication.
263    #[derive(Clone, Deserialize, Serialize)]
264    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
265    #[serde(tag = "type", rename = "m.login.password")]
266    pub struct Password {
267        /// Identification information for the user.
268        #[serde(skip_serializing_if = "Option::is_none")]
269        pub identifier: Option<UserIdentifier>,
270
271        /// The password.
272        pub password: String,
273
274        /// Username for the user.
275        #[serde(skip_serializing_if = "Option::is_none")]
276        #[deprecated = "\
277            Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
278            instead.\
279        "]
280        pub user: Option<String>,
281
282        /// 3rd-party identifier address for the user.
283        #[serde(skip_serializing_if = "Option::is_none")]
284        #[deprecated = "\
285            Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
286            instead.\
287        "]
288        pub address: Option<String>,
289
290        /// 3rd-party identifier medium for the user.
291        #[serde(skip_serializing_if = "Option::is_none")]
292        #[deprecated = "\
293            Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
294            instead.\
295        "]
296        pub medium: Option<String>,
297    }
298
299    impl Password {
300        /// Creates a new `Password` with the given identifier and password.
301        #[allow(deprecated)]
302        pub fn new(identifier: UserIdentifier, password: String) -> Self {
303            Self { identifier: Some(identifier), password, user: None, address: None, medium: None }
304        }
305    }
306
307    impl fmt::Debug for Password {
308        #[allow(deprecated)]
309        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310            let Self { identifier, password: _, user, address, medium } = self;
311            f.debug_struct("Password")
312                .field("identifier", identifier)
313                .field("user", user)
314                .field("address", address)
315                .field("medium", medium)
316                .finish_non_exhaustive()
317        }
318    }
319
320    /// A token to supply as authentication.
321    #[derive(Clone, Deserialize, Serialize)]
322    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
323    #[serde(tag = "type", rename = "m.login.token")]
324    pub struct Token {
325        /// The token.
326        pub token: String,
327    }
328
329    impl Token {
330        /// Creates a new `Token` with the given token.
331        pub fn new(token: String) -> Self {
332            Self { token }
333        }
334    }
335
336    impl fmt::Debug for Token {
337        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338            let Self { token: _ } = self;
339            f.debug_struct("Token").finish_non_exhaustive()
340        }
341    }
342
343    /// An identifier to supply for Application Service authentication.
344    #[derive(Clone, Debug, Deserialize, Serialize)]
345    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
346    #[serde(tag = "type", rename = "m.login.application_service")]
347    pub struct ApplicationService {
348        /// Identification information for the user.
349        pub identifier: Option<UserIdentifier>,
350
351        /// Username for the user.
352        #[serde(skip_serializing_if = "Option::is_none")]
353        #[deprecated = "\
354            Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
355            instead.\
356        "]
357        pub user: Option<String>,
358    }
359
360    impl ApplicationService {
361        /// Creates a new `ApplicationService` with the given identifier.
362        #[allow(deprecated)]
363        pub fn new(identifier: UserIdentifier) -> Self {
364            Self { identifier: Some(identifier), user: None }
365        }
366    }
367
368    #[doc(hidden)]
369    #[derive(Clone, Serialize)]
370    #[non_exhaustive]
371    pub struct CustomLoginInfo {
372        #[serde(rename = "type")]
373        login_type: String,
374        #[serde(flatten)]
375        data: JsonObject,
376    }
377
378    impl fmt::Debug for CustomLoginInfo {
379        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380            let Self { login_type, data: _ } = self;
381            f.debug_struct("CustomLoginInfo")
382                .field("login_type", login_type)
383                .finish_non_exhaustive()
384        }
385    }
386
387    /// Client configuration provided by the server.
388    #[derive(Clone, Debug, Deserialize, Serialize)]
389    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
390    pub struct DiscoveryInfo {
391        /// Information about the homeserver to connect to.
392        #[serde(rename = "m.homeserver")]
393        pub homeserver: HomeserverInfo,
394
395        /// Information about the identity server to connect to.
396        #[serde(rename = "m.identity_server")]
397        pub identity_server: Option<IdentityServerInfo>,
398    }
399
400    impl DiscoveryInfo {
401        /// Create a new `DiscoveryInfo` with the given homeserver.
402        pub fn new(homeserver: HomeserverInfo) -> Self {
403            Self { homeserver, identity_server: None }
404        }
405    }
406
407    /// Information about the homeserver to connect to.
408    #[derive(Clone, Debug, Deserialize, Serialize)]
409    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
410    pub struct HomeserverInfo {
411        /// The base URL for the homeserver for client-server connections.
412        pub base_url: String,
413    }
414
415    impl HomeserverInfo {
416        /// Create a new `HomeserverInfo` with the given base url.
417        pub fn new(base_url: String) -> Self {
418            Self { base_url }
419        }
420    }
421
422    /// Information about the identity server to connect to.
423    #[derive(Clone, Debug, Deserialize, Serialize)]
424    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
425    pub struct IdentityServerInfo {
426        /// The base URL for the identity server for client-server connections.
427        pub base_url: String,
428    }
429
430    impl IdentityServerInfo {
431        /// Create a new `IdentityServerInfo` with the given base url.
432        pub fn new(base_url: String) -> Self {
433            Self { base_url }
434        }
435    }
436
437    #[cfg(test)]
438    mod tests {
439        use assert_matches2::assert_matches;
440        use ruma_common::canonical_json::assert_to_canonical_json_eq;
441        use serde_json::{from_value as from_json_value, json};
442
443        use super::{LoginInfo, Token};
444        use crate::uiaa::UserIdentifier;
445
446        #[test]
447        fn deserialize_login_type() {
448            assert_matches!(
449                from_json_value(json!({
450                    "type": "m.login.password",
451                    "identifier": {
452                        "type": "m.id.user",
453                        "user": "cheeky_monkey"
454                    },
455                    "password": "ilovebananas"
456                }))
457                .unwrap(),
458                LoginInfo::Password(login)
459            );
460            assert_matches!(login.identifier, Some(UserIdentifier::Matrix(id)));
461            assert_eq!(id.user, "cheeky_monkey");
462            assert_eq!(login.password, "ilovebananas");
463
464            assert_matches!(
465                from_json_value(json!({
466                    "type": "m.login.token",
467                    "token": "1234567890abcdef"
468                }))
469                .unwrap(),
470                LoginInfo::Token(Token { token })
471            );
472            assert_eq!(token, "1234567890abcdef");
473        }
474
475        #[test]
476        fn login_info_serialize_roundtrip() {
477            let json = json!({
478                "type": "local.dev.custom",
479                "identifier": "dGhpcy5pcy5tZQ",
480            });
481
482            let login_info = from_json_value::<LoginInfo>(json.clone()).unwrap();
483
484            assert_eq!(login_info.login_type(), "local.dev.custom");
485            let data = login_info.data();
486            assert_eq!(data.len(), 1);
487            assert_eq!(data.get("identifier").unwrap().as_str(), Some("dGhpcy5pcy5tZQ"));
488
489            assert_to_canonical_json_eq!(login_info, json);
490        }
491
492        #[test]
493        #[cfg(feature = "client")]
494        fn serialize_login_request_body() {
495            use std::borrow::Cow;
496
497            use ruma_common::api::{
498                MatrixVersion, OutgoingRequest, SupportedVersions, auth_scheme::SendAccessToken,
499            };
500            use serde_json::Value as JsonValue;
501
502            use super::{LoginInfo, Password, Request, Token};
503            use crate::uiaa::{EmailUserIdentifier, UserIdentifier};
504
505            let supported = SupportedVersions {
506                versions: [MatrixVersion::V1_1].into(),
507                features: Default::default(),
508            };
509
510            let req: http::Request<Vec<u8>> = Request {
511                login_info: LoginInfo::Token(Token { token: "0xdeadbeef".to_owned() }),
512                device_id: None,
513                initial_device_display_name: Some("test".to_owned()),
514                refresh_token: false,
515            }
516            .try_into_http_request(
517                "https://homeserver.tld",
518                SendAccessToken::None,
519                Cow::Borrowed(&supported),
520            )
521            .unwrap();
522
523            let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
524            assert_eq!(
525                req_body_value,
526                json!({
527                    "type": "m.login.token",
528                    "token": "0xdeadbeef",
529                    "initial_device_display_name": "test",
530                })
531            );
532
533            let req: http::Request<Vec<u8>> = Request {
534                #[allow(deprecated)]
535                login_info: LoginInfo::Password(Password {
536                    identifier: Some(UserIdentifier::Email(EmailUserIdentifier::new(
537                        "hello@example.com".to_owned(),
538                    ))),
539                    password: "deadbeef".to_owned(),
540                    user: None,
541                    address: None,
542                    medium: None,
543                }),
544                device_id: None,
545                initial_device_display_name: Some("test".to_owned()),
546                refresh_token: false,
547            }
548            .try_into_http_request(
549                "https://homeserver.tld",
550                SendAccessToken::None,
551                Cow::Borrowed(&supported),
552            )
553            .unwrap();
554
555            let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
556            assert_eq!(
557                req_body_value,
558                json!({
559                    "identifier": {
560                        "type": "m.id.thirdparty",
561                        "medium": "email",
562                        "address": "hello@example.com"
563                    },
564                    "type": "m.login.password",
565                    "password": "deadbeef",
566                    "initial_device_display_name": "test",
567                })
568            );
569        }
570    }
571}