Skip to main content

matrix_sdk/authentication/matrix/
mod.rs

1// Copyright 2020 Damir Jelić
2// Copyright 2020 The Matrix.org Foundation C.I.C.
3// Copyright 2022 Famedly GmbH
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17//! Types to interact with the native Matrix authentication API.
18
19#[cfg(feature = "sso-login")]
20use std::future::Future;
21use std::{borrow::Cow, fmt};
22
23use matrix_sdk_base::{SessionMeta, store::RoomLoadSettings};
24use ruma::{
25    api::{
26        OutgoingRequest,
27        auth_scheme::SendAccessToken,
28        client::{
29            account::register,
30            session::{
31                get_login_types, login, logout, refresh_token, sso_login, sso_login_with_provider,
32            },
33            uiaa::UserIdentifier,
34        },
35    },
36    serde::JsonObject,
37};
38use serde::{Deserialize, Serialize};
39use thiserror::Error;
40use tracing::{debug, error, info, instrument};
41
42use crate::{
43    Client, Error, RefreshTokenError, Result,
44    authentication::AuthData,
45    client::SessionChange,
46    error::{HttpError, HttpResult},
47    utils::UrlOrQuery,
48};
49
50mod login_builder;
51
52pub use self::login_builder::LoginBuilder;
53#[cfg(feature = "sso-login")]
54pub use self::login_builder::SsoLoginBuilder;
55use super::SessionTokens;
56
57/// A high-level API to interact with the native Matrix authentication API.
58///
59/// To access this API, use [`Client::matrix_auth()`].
60#[derive(Debug, Clone)]
61pub struct MatrixAuth {
62    client: Client,
63}
64
65/// Errors that can occur when using the SSO API.
66#[derive(Debug, Error)]
67pub enum SsoError {
68    /// The supplied callback URL used to complete SSO is invalid.
69    #[error("callback URL invalid")]
70    CallbackUrlInvalid,
71}
72
73impl MatrixAuth {
74    pub(crate) fn new(client: Client) -> Self {
75        Self { client }
76    }
77
78    /// Gets the homeserver’s supported login types.
79    ///
80    /// This should be the first step when trying to log in so you can call the
81    /// appropriate method for the next step.
82    pub async fn get_login_types(&self) -> HttpResult<get_login_types::v3::Response> {
83        let request = get_login_types::v3::Request::new();
84        self.client.send(request).await
85    }
86
87    /// Get the URL to use to log in via Single Sign-On.
88    ///
89    /// Returns a URL that should be opened in a web browser to let the user
90    /// log in.
91    ///
92    /// After a successful login, the loginToken received at the redirect URL
93    /// should be used to log in with [`login_token`].
94    ///
95    /// # Arguments
96    ///
97    /// * `redirect_url` - The URL that will receive a `loginToken` after a
98    ///   successful SSO login.
99    ///
100    /// * `idp_id` - The optional ID of the identity provider to log in with.
101    ///
102    /// [`login_token`]: #method.login_token
103    pub async fn get_sso_login_url(
104        &self,
105        redirect_url: &str,
106        idp_id: Option<&str>,
107    ) -> Result<String> {
108        let homeserver = self.client.homeserver();
109        let supported_versions = self.client.supported_versions().await?;
110
111        let request = if let Some(id) = idp_id {
112            sso_login_with_provider::v3::Request::new(id.to_owned(), redirect_url.to_owned())
113                .try_into_http_request::<Vec<u8>>(
114                    homeserver.as_str(),
115                    SendAccessToken::None,
116                    Cow::Owned(supported_versions),
117                )
118        } else {
119            sso_login::v3::Request::new(redirect_url.to_owned()).try_into_http_request::<Vec<u8>>(
120                homeserver.as_str(),
121                SendAccessToken::None,
122                Cow::Owned(supported_versions),
123            )
124        };
125
126        match request {
127            Ok(req) => Ok(req.uri().to_string()),
128            Err(err) => Err(Error::from(HttpError::IntoHttp(err))),
129        }
130    }
131
132    /// Log into the server with a username and password.
133    ///
134    /// This can be used for the first login as well as for subsequent logins,
135    /// note that if the device ID isn't provided a new device will be created.
136    ///
137    /// If this isn't the first login, a device ID should be provided through
138    /// [`LoginBuilder::device_id`] to restore the correct stores.
139    ///
140    /// Alternatively the [`restore_session`] method can be used to restore a
141    /// logged-in client without the password.
142    ///
143    /// # Arguments
144    ///
145    /// * `user` - The user ID or user ID localpart of the user that should be
146    ///   logged into the homeserver.
147    ///
148    /// * `password` - The password of the user.
149    ///
150    /// # Examples
151    ///
152    /// ```no_run
153    /// # use url::Url;
154    /// # let homeserver = Url::parse("http://example.com").unwrap();
155    /// # futures_executor::block_on(async {
156    /// use matrix_sdk::Client;
157    ///
158    /// let client = Client::new(homeserver).await?;
159    /// let user = "example";
160    ///
161    /// let response = client
162    ///     .matrix_auth()
163    ///     .login_username(user, "wordpass")
164    ///     .initial_device_display_name("My bot")
165    ///     .await?;
166    ///
167    /// println!(
168    ///     "Logged in as {user}, got device_id {} and access_token {}",
169    ///     response.device_id, response.access_token,
170    /// );
171    /// # anyhow::Ok(()) });
172    /// ```
173    ///
174    /// [`restore_session`]: #method.restore_session
175    pub fn login_username(&self, id: impl AsRef<str>, password: &str) -> LoginBuilder {
176        self.login_identifier(UserIdentifier::UserIdOrLocalpart(id.as_ref().to_owned()), password)
177    }
178
179    /// Log into the server with a user identifier and password.
180    ///
181    /// This is a more general form of [`login_username`][Self::login_username]
182    /// that also accepts third-party identifiers instead of just the user ID or
183    /// its localpart.
184    pub fn login_identifier(&self, id: UserIdentifier, password: &str) -> LoginBuilder {
185        LoginBuilder::new_password(self.clone(), id, password.to_owned())
186    }
187
188    /// Log into the server with a custom login type.
189    ///
190    /// # Arguments
191    ///
192    /// * `login_type` - Identifier of the custom login type, e.g.
193    ///   `org.matrix.login.jwt`
194    ///
195    /// * `data` - The additional data which should be attached to the login
196    ///   request.
197    ///
198    /// # Examples
199    ///
200    /// ```no_run
201    /// # use url::Url;
202    /// # let homeserver = Url::parse("http://example.com").unwrap();
203    /// # async {
204    /// use matrix_sdk::Client;
205    ///
206    /// let client = Client::new(homeserver).await?;
207    /// let user = "example";
208    ///
209    /// let response = client
210    ///     .matrix_auth()
211    ///     .login_custom(
212    ///         "org.matrix.login.jwt",
213    ///         [("token".to_owned(), "jwt_token_content".into())]
214    ///             .into_iter()
215    ///             .collect(),
216    ///     )?
217    ///     .initial_device_display_name("My bot")
218    ///     .await?;
219    ///
220    /// println!(
221    ///     "Logged in as {user}, got device_id {} and access_token {}",
222    ///     response.device_id, response.access_token,
223    /// );
224    /// # anyhow::Ok(()) };
225    /// ```
226    pub fn login_custom(
227        &self,
228        login_type: &str,
229        data: JsonObject,
230    ) -> serde_json::Result<LoginBuilder> {
231        LoginBuilder::new_custom(self.clone(), login_type, data)
232    }
233
234    /// Log into the server with a token.
235    ///
236    /// This token is usually received in the SSO flow after following the URL
237    /// provided by [`get_sso_login_url`], note that this is not the access
238    /// token of a session.
239    ///
240    /// This should only be used for the first login.
241    ///
242    /// The [`restore_session`] method should be used to restore a logged-in
243    /// client after the first login.
244    ///
245    /// A device ID should be provided through [`LoginBuilder::device_id`] to
246    /// restore the correct stores, if the device ID isn't provided a new
247    /// device will be created.
248    ///
249    /// # Arguments
250    ///
251    /// * `token` - A login token.
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// use matrix_sdk::Client;
257    /// # use url::Url;
258    /// # let homeserver = Url::parse("https://example.com").unwrap();
259    /// # let redirect_url = "http://localhost:1234";
260    /// # let login_token = "token";
261    /// # async {
262    /// let client = Client::new(homeserver).await.unwrap();
263    /// let auth = client.matrix_auth();
264    /// let sso_url = auth.get_sso_login_url(redirect_url, None);
265    ///
266    /// // Let the user authenticate at the SSO URL.
267    /// // Receive the loginToken param at the redirect_url.
268    ///
269    /// let response = auth
270    ///     .login_token(login_token)
271    ///     .initial_device_display_name("My app")
272    ///     .await
273    ///     .unwrap();
274    ///
275    /// println!(
276    ///     "Logged in as {}, got device_id {} and access_token {}",
277    ///     response.user_id, response.device_id, response.access_token,
278    /// );
279    /// # };
280    /// ```
281    ///
282    /// [`get_sso_login_url`]: #method.get_sso_login_url
283    /// [`restore_session`]: #method.restore_session
284    pub fn login_token(&self, token: &str) -> LoginBuilder {
285        LoginBuilder::new_token(self.clone(), token.to_owned())
286    }
287
288    /// A higher level wrapper around the methods to complete an SSO login after
289    /// the user has logged in through a webview. This method should be used
290    /// in tandem with [`MatrixAuth::get_sso_login_url`].
291    ///
292    /// # Arguments
293    ///
294    /// * `url_or_query` - The full callback URL carrying the login token, or
295    ///   only its query string.
296    ///
297    /// # Examples
298    ///
299    /// ```no_run
300    /// use matrix_sdk::Client;
301    /// # use matrix_sdk::utils::UrlOrQuery;
302    /// # use url::Url;
303    /// # let homeserver = Url::parse("https://example.com").unwrap();
304    /// # let redirect_url = "http://localhost:1234";
305    /// # let url_or_query = UrlOrQuery::Query("loginToken=token".to_owned());
306    /// # async {
307    /// let client = Client::new(homeserver).await.unwrap();
308    /// let auth = client.matrix_auth();
309    /// let sso_url = auth.get_sso_login_url(redirect_url, None);
310    ///
311    /// // Let the user authenticate at the SSO URL.
312    /// // Receive the callback url or query string.
313    ///
314    /// let response = auth
315    ///     .login_with_sso_callback(url_or_query)
316    ///     .unwrap()
317    ///     .initial_device_display_name("My app")
318    ///     .await
319    ///     .unwrap();
320    ///
321    /// println!(
322    ///     "Logged in as {}, got device_id {} and access_token {}",
323    ///     response.user_id, response.device_id, response.access_token,
324    /// );
325    /// # };
326    /// ```
327    pub fn login_with_sso_callback(
328        &self,
329        url_or_query: UrlOrQuery,
330    ) -> Result<LoginBuilder, SsoError> {
331        #[derive(Deserialize)]
332        struct QueryParameters {
333            #[serde(rename = "loginToken")]
334            login_token: Option<String>,
335        }
336
337        let query_string = url_or_query.query().unwrap_or_default();
338        let query: QueryParameters =
339            serde_html_form::from_str(query_string).map_err(|_| SsoError::CallbackUrlInvalid)?;
340        let token = query.login_token.ok_or(SsoError::CallbackUrlInvalid)?;
341
342        Ok(self.login_token(token.as_str()))
343    }
344
345    /// Log into the server via Single Sign-On.
346    ///
347    /// This takes care of the whole SSO flow:
348    ///   * Spawn a local http server
349    ///   * Provide a callback to open the SSO login URL in a web browser
350    ///   * Wait for the local http server to get the loginToken
351    ///   * Call [`login_token`]
352    ///
353    /// If cancellation is needed the method should be wrapped in a cancellable
354    /// task. **Note** that users with root access to the system have the
355    /// ability to snoop in on the data/token that is passed to the local
356    /// HTTP server that will be spawned.
357    ///
358    /// If you need more control over the SSO login process, you should use
359    /// [`get_sso_login_url`] and [`login_token`] directly.
360    ///
361    /// This should only be used for the first login.
362    ///
363    /// The [`restore_session`] method should be used to restore a logged-in
364    /// client after the first login.
365    ///
366    /// # Arguments
367    ///
368    /// * `use_sso_login_url` - A callback that will receive the SSO Login URL.
369    ///   It should usually be used to open the SSO URL in a browser and must
370    ///   return `Ok(())` if the URL was successfully opened. If it returns
371    ///   `Err`, the error will be forwarded.
372    ///
373    /// # Examples
374    ///
375    /// ```no_run
376    /// use matrix_sdk::Client;
377    /// # use url::Url;
378    /// # let homeserver = Url::parse("https://example.com").unwrap();
379    /// # async {
380    /// let client = Client::new(homeserver).await.unwrap();
381    ///
382    /// let response = client
383    ///     .matrix_auth()
384    ///     .login_sso(|sso_url| async move {
385    ///         // Open sso_url
386    ///         Ok(())
387    ///     })
388    ///     .initial_device_display_name("My app")
389    ///     .await
390    ///     .unwrap();
391    ///
392    /// println!(
393    ///     "Logged in as {}, got device_id {} and access_token {}",
394    ///     response.user_id, response.device_id, response.access_token
395    /// );
396    /// # };
397    /// ```
398    ///
399    /// [`get_sso_login_url`]: #method.get_sso_login_url
400    /// [`login_token`]: #method.login_token
401    /// [`restore_session`]: #method.restore_session
402    #[cfg(feature = "sso-login")]
403    pub fn login_sso<F, Fut>(&self, use_sso_login_url: F) -> SsoLoginBuilder<F>
404    where
405        F: FnOnce(String) -> Fut + Send,
406        Fut: Future<Output = Result<()>> + Send,
407    {
408        SsoLoginBuilder::new(self.clone(), use_sso_login_url)
409    }
410
411    /// Is the client logged in using the native Matrix authentication API.
412    pub fn logged_in(&self) -> bool {
413        self.client
414            .auth_ctx()
415            .auth_data
416            .get()
417            .is_some_and(|auth_data| matches!(auth_data, AuthData::Matrix))
418    }
419
420    /// Refresh the access token.
421    ///
422    /// When support for [refreshing access tokens] is activated on both the
423    /// homeserver and the client, access tokens have an expiration date and
424    /// need to be refreshed periodically. To activate support for refresh
425    /// tokens in the [`Client`], it needs to be done at login with the
426    /// [`LoginBuilder::request_refresh_token()`] method, or during account
427    /// registration.
428    ///
429    /// This method doesn't need to be called if
430    /// [`ClientBuilder::handle_refresh_tokens()`] is called during construction
431    /// of the `Client`. Otherwise, it should be called once when a refresh
432    /// token is available and an [`UnknownToken`] error is received.
433    /// If this call fails with another [`UnknownToken`] error, it means that
434    /// the session needs to be logged in again.
435    ///
436    /// It can also be called at any time when a refresh token is available, it
437    /// will invalidate the previous access token.
438    ///
439    /// The new tokens in the response will be used by the `Client` and should
440    /// be persisted to be able to [restore the session]. The response will
441    /// always contain an access token that replaces the previous one. It
442    /// can also contain a refresh token, in which case it will also replace
443    /// the previous one.
444    ///
445    /// This method is protected behind a lock, so calling this method several
446    /// times at once will only call the endpoint once and all subsequent calls
447    /// will wait for the result of the first call. The first call will
448    /// return `Ok(Some(response))` or the [`HttpError`] returned by the
449    /// endpoint, while the others will return `Ok(None)` if the token was
450    /// refreshed by the first call or a [`RefreshTokenError`] error, if it
451    /// failed.
452    ///
453    /// # Examples
454    ///
455    /// ```no_run
456    /// use matrix_sdk::{Client, Error};
457    /// use url::Url;
458    /// # async {
459    /// # fn get_credentials() -> (&'static str, &'static str) { ("", "") };
460    /// # fn persist_session(_: Option<matrix_sdk::AuthSession>) {};
461    ///
462    /// let homeserver = Url::parse("http://example.com")?;
463    /// let client = Client::new(homeserver).await?;
464    ///
465    /// let (user, password) = get_credentials();
466    /// let response = client
467    ///     .matrix_auth()
468    ///     .login_username(user, password)
469    ///     .initial_device_display_name("My App")
470    ///     .request_refresh_token()
471    ///     .send()
472    ///     .await?;
473    ///
474    /// persist_session(client.session());
475    ///
476    /// // Handle when an `M_UNKNOWN_TOKEN` error is encountered.
477    /// async fn on_unknown_token_err(client: &Client) -> Result<(), Error> {
478    ///     let auth = client.matrix_auth();
479    ///
480    ///     if client
481    ///         .session_tokens()
482    ///         .and_then(|tokens| tokens.refresh_token)
483    ///         .is_some()
484    ///         && auth.refresh_access_token().await.is_ok()
485    ///     {
486    ///         persist_session(client.session());
487    ///         return Ok(());
488    ///     }
489    ///
490    ///     let (user, password) = get_credentials();
491    ///     auth.login_username(user, password)
492    ///         .request_refresh_token()
493    ///         .send()
494    ///         .await?;
495    ///
496    ///     persist_session(client.session());
497    ///
498    ///     Ok(())
499    /// }
500    /// # anyhow::Ok(()) };
501    /// ```
502    ///
503    /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
504    /// [`UnknownToken`]: ruma::api::client::error::ErrorKind::UnknownToken
505    /// [restore the session]: Client::restore_session
506    /// [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens
507    pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> {
508        macro_rules! fail {
509            ($lock:expr, $err:expr) => {
510                let error = $err;
511                *$lock = Err(error.clone());
512                return Err(error);
513            };
514        }
515
516        let refresh_token_lock = &self.client.auth_ctx().refresh_token_lock;
517        let Ok(mut guard) = refresh_token_lock.try_lock() else {
518            // Somebody else is also doing a token refresh; wait for it to finish first.
519            return refresh_token_lock.lock().await.clone();
520        };
521
522        let Some(mut session_tokens) = self.client.session_tokens() else {
523            fail!(guard, RefreshTokenError::RefreshTokenRequired);
524        };
525        let Some(refresh_token) = session_tokens.refresh_token.clone() else {
526            fail!(guard, RefreshTokenError::RefreshTokenRequired);
527        };
528
529        let request = refresh_token::v3::Request::new(refresh_token);
530        let res = self.client.send_inner(request, None, Default::default()).await;
531
532        match res {
533            Ok(res) => {
534                *guard = Ok(());
535
536                session_tokens.access_token = res.access_token;
537                if let Some(refresh_token) = res.refresh_token {
538                    session_tokens.refresh_token = Some(refresh_token);
539                }
540
541                self.client.auth_ctx().set_session_tokens(session_tokens);
542
543                if let Some(save_session_callback) =
544                    self.client.inner.auth_ctx.save_session_callback.get()
545                    && let Err(err) = save_session_callback(self.client.clone())
546                {
547                    error!("when saving session after refresh: {err}");
548                }
549
550                _ = self
551                    .client
552                    .inner
553                    .auth_ctx
554                    .session_change_sender
555                    .send(SessionChange::TokensRefreshed);
556
557                Ok(())
558            }
559            Err(error) => {
560                let error = RefreshTokenError::MatrixAuth(error.into());
561                fail!(guard, error);
562            }
563        }
564    }
565
566    /// Register a user to the server.
567    ///
568    /// If registration was successful and a session token was returned by the
569    /// server, the client session is set (the client is logged in).
570    ///
571    /// # Arguments
572    ///
573    /// * `registration` - The easiest way to create this request is using the
574    ///   [`register::v3::Request`] itself.
575    ///
576    /// # Examples
577    ///
578    /// ```no_run
579    /// use matrix_sdk::{
580    ///     Client,
581    ///     ruma::api::client::{
582    ///         account::register::v3::Request as RegistrationRequest, uiaa,
583    ///     },
584    /// };
585    /// # use url::Url;
586    /// # let homeserver = Url::parse("http://example.com").unwrap();
587    /// # async {
588    ///
589    /// let mut request = RegistrationRequest::new();
590    /// request.username = Some("user".to_owned());
591    /// request.password = Some("password".to_owned());
592    /// request.auth = Some(uiaa::AuthData::FallbackAcknowledgement(
593    ///     uiaa::FallbackAcknowledgement::new("foobar".to_owned()),
594    /// ));
595    ///
596    /// let client = Client::new(homeserver).await.unwrap();
597    /// client.matrix_auth().register(request).await;
598    /// # };
599    /// ```
600    #[instrument(skip_all)]
601    pub async fn register(&self, request: register::v3::Request) -> Result<register::v3::Response> {
602        let homeserver = self.client.homeserver();
603        info!("Registering to {homeserver}");
604
605        #[cfg(feature = "e2e-encryption")]
606        let login_info = match (&request.username, &request.password) {
607            (Some(u), Some(p)) => Some(login::v3::LoginInfo::Password(login::v3::Password::new(
608                UserIdentifier::UserIdOrLocalpart(u.into()),
609                p.clone(),
610            ))),
611            _ => None,
612        };
613
614        let response = self.client.send(request).await?;
615        if let Some(session) = MatrixSession::from_register_response(&response) {
616            let _ = self
617                .set_session(
618                    session,
619                    RoomLoadSettings::default(),
620                    #[cfg(feature = "e2e-encryption")]
621                    login_info,
622                )
623                .await;
624        }
625        Ok(response)
626    }
627    /// Log out the current user.
628    pub async fn logout(&self) -> HttpResult<logout::v3::Response> {
629        let request = logout::v3::Request::new();
630        self.client.send(request).await
631    }
632
633    /// Get the whole native Matrix authentication session info of this client.
634    ///
635    /// Will be `None` if the client has not been logged in with the native
636    /// Matrix Authentication API.
637    ///
638    /// Can be used with [`MatrixAuth::restore_session`] to restore a previously
639    /// logged-in session.
640    pub fn session(&self) -> Option<MatrixSession> {
641        let meta = self.client.session_meta()?;
642        let tokens = self.client.session_tokens()?;
643        Some(MatrixSession { meta: meta.to_owned(), tokens })
644    }
645
646    /// Restore a previously logged in session.
647    ///
648    /// This can be used to restore the client to a logged in state, loading all
649    /// the stored state and encryption keys.
650    ///
651    /// Alternatively, if the whole session isn't stored the [`login`] method
652    /// can be used with a device ID.
653    ///
654    /// # Arguments
655    ///
656    /// * `session` - A session that the user already has from a previous login
657    ///   call.
658    ///
659    /// * `room_load_settings` — Specify how many rooms must be restored; use
660    ///   `::default()` if you don't know which value to pick.
661    ///
662    /// # Panics
663    ///
664    /// Panics if a session was already restored or logged in.
665    ///
666    /// # Examples
667    ///
668    /// ```no_run
669    /// use matrix_sdk::{
670    ///     Client, SessionMeta, SessionTokens,
671    ///     authentication::matrix::MatrixSession,
672    ///     ruma::{owned_device_id, owned_user_id},
673    /// };
674    /// # use url::Url;
675    /// # async {
676    ///
677    /// let homeserver = Url::parse("http://example.com")?;
678    /// let client = Client::new(homeserver).await?;
679    ///
680    /// let session = MatrixSession {
681    ///     meta: SessionMeta {
682    ///         user_id: owned_user_id!("@example:localhost"),
683    ///         device_id: owned_device_id!("MYDEVICEID"),
684    ///     },
685    ///     tokens: SessionTokens {
686    ///         access_token: "My-Token".to_owned(),
687    ///         refresh_token: None,
688    ///     },
689    /// };
690    ///
691    /// client.restore_session(session).await?;
692    /// # anyhow::Ok(()) };
693    /// ```
694    ///
695    /// The `MatrixSession` object can also be created from the response the
696    /// [`LoginBuilder::send()`] method returns:
697    ///
698    /// ```no_run
699    /// use matrix_sdk::{Client, store::RoomLoadSettings};
700    /// use url::Url;
701    /// # async {
702    ///
703    /// let homeserver = Url::parse("http://example.com")?;
704    /// let client = Client::new(homeserver).await?;
705    /// let auth = client.matrix_auth();
706    ///
707    /// let response = auth.login_username("example", "my-password").send().await?;
708    ///
709    /// // Persist the `MatrixSession` so it can later be used to restore the login.
710    ///
711    /// auth.restore_session((&response).into(), RoomLoadSettings::default())
712    ///     .await?;
713    /// # anyhow::Ok(()) };
714    /// ```
715    ///
716    /// [`login`]: #method.login
717    /// [`LoginBuilder::send()`]: crate::authentication::matrix::LoginBuilder::send
718    #[instrument(skip_all)]
719    pub async fn restore_session(
720        &self,
721        session: MatrixSession,
722        room_load_settings: RoomLoadSettings,
723    ) -> Result<()> {
724        debug!("Restoring Matrix auth session");
725        self.set_session(
726            session,
727            room_load_settings,
728            #[cfg(feature = "e2e-encryption")]
729            None,
730        )
731        .await?;
732        debug!("Done restoring Matrix auth session");
733        Ok(())
734    }
735
736    /// Receive a login response and update the homeserver and the base client
737    /// if needed.
738    ///
739    /// # Arguments
740    ///
741    /// * `response` - A successful login response.
742    pub(crate) async fn receive_login_response(
743        &self,
744        response: &login::v3::Response,
745        #[cfg(feature = "e2e-encryption")] login_info: Option<login::v3::LoginInfo>,
746    ) -> Result<()> {
747        self.client.maybe_update_login_well_known(response.well_known.as_ref());
748
749        self.set_session(
750            response.into(),
751            RoomLoadSettings::default(),
752            #[cfg(feature = "e2e-encryption")]
753            login_info,
754        )
755        .await?;
756
757        Ok(())
758    }
759
760    /// Set the Matrix authentication session.
761    ///
762    /// # Arguments
763    ///
764    /// * `session` — The session being opened.
765    ///
766    /// * `room_load_settings` — Specify how much rooms must be restored; use
767    ///   `::default()` if you don't know which value to pick.
768    ///
769    /// # Panic
770    ///
771    /// Panics if authentication data was already set.
772    async fn set_session(
773        &self,
774        session: MatrixSession,
775        room_load_settings: RoomLoadSettings,
776        #[cfg(feature = "e2e-encryption")] login_info: Option<login::v3::LoginInfo>,
777    ) -> Result<()> {
778        // This API doesn't have any data but by setting this variant we protect the
779        // user from using both authentication APIs at once.
780        self.client
781            .auth_ctx()
782            .auth_data
783            .set(AuthData::Matrix)
784            .expect("Client authentication data was already set");
785        self.client.auth_ctx().set_session_tokens(session.tokens);
786        self.client
787            .base_client()
788            .activate(
789                session.meta,
790                room_load_settings,
791                #[cfg(feature = "e2e-encryption")]
792                None,
793            )
794            .await?;
795
796        #[cfg(feature = "e2e-encryption")]
797        {
798            use ruma::api::client::uiaa::{AuthData, Password};
799
800            let auth_data = match login_info {
801                Some(login::v3::LoginInfo::Password(login::v3::Password {
802                    identifier: Some(identifier),
803                    password,
804                    ..
805                })) => Some(AuthData::Password(Password::new(identifier, password))),
806                // Other methods can't be immediately translated to an auth.
807                _ => None,
808            };
809
810            self.client.encryption().spawn_initialization_task(auth_data).await;
811        }
812
813        Ok(())
814    }
815}
816
817/// A user session using the native Matrix authentication API.
818///
819/// # Examples
820///
821/// ```
822/// use matrix_sdk::{
823///     SessionMeta, SessionTokens, authentication::matrix::MatrixSession,
824/// };
825/// use ruma::{owned_device_id, owned_user_id};
826///
827/// let session = MatrixSession {
828///     meta: SessionMeta {
829///         user_id: owned_user_id!("@example:localhost"),
830///         device_id: owned_device_id!("MYDEVICEID"),
831///     },
832///     tokens: SessionTokens {
833///         access_token: "My-Token".to_owned(),
834///         refresh_token: None,
835///     },
836/// };
837///
838/// assert_eq!(session.meta.device_id, "MYDEVICEID");
839/// ```
840#[derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
841pub struct MatrixSession {
842    /// The Matrix user session info.
843    #[serde(flatten)]
844    pub meta: SessionMeta,
845
846    /// The tokens used for authentication.
847    #[serde(flatten)]
848    pub tokens: SessionTokens,
849}
850
851#[cfg(not(tarpaulin_include))]
852impl fmt::Debug for MatrixSession {
853    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854        f.debug_struct("MatrixSession").field("meta", &self.meta).finish_non_exhaustive()
855    }
856}
857
858impl From<&login::v3::Response> for MatrixSession {
859    fn from(response: &login::v3::Response) -> Self {
860        let login::v3::Response { user_id, access_token, device_id, refresh_token, .. } = response;
861        Self {
862            meta: SessionMeta { user_id: user_id.clone(), device_id: device_id.clone() },
863            tokens: SessionTokens {
864                access_token: access_token.clone(),
865                refresh_token: refresh_token.clone(),
866            },
867        }
868    }
869}
870
871impl MatrixSession {
872    #[allow(clippy::question_mark)] // clippy falsely complains about the let-unpacking
873    fn from_register_response(response: &register::v3::Response) -> Option<Self> {
874        let register::v3::Response { user_id, access_token, device_id, refresh_token, .. } =
875            response;
876        Some(Self {
877            meta: SessionMeta { user_id: user_id.clone(), device_id: device_id.clone()? },
878            tokens: SessionTokens {
879                access_token: access_token.clone()?,
880                refresh_token: refresh_token.clone(),
881            },
882        })
883    }
884}