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