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        OutgoingRequestExt,
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::{MatrixUserIdentifier, 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 log
90    /// 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(
177            UserIdentifier::Matrix(MatrixUserIdentifier::new(id.as_ref().to_owned())),
178            password,
179        )
180    }
181
182    /// Log into the server with a user identifier and password.
183    ///
184    /// This is a more general form of [`login_username`][Self::login_username]
185    /// that also accepts third-party identifiers instead of just the user ID or
186    /// its localpart.
187    pub fn login_identifier(&self, id: UserIdentifier, password: &str) -> LoginBuilder {
188        LoginBuilder::new_password(self.clone(), id, password.to_owned())
189    }
190
191    /// Log into the server with a custom login type.
192    ///
193    /// # Arguments
194    ///
195    /// - `login_type` - Identifier of the custom login type, e.g.
196    ///   `org.matrix.login.jwt`
197    ///
198    /// - `data` - The additional data which should be attached to the login
199    ///   request.
200    ///
201    /// # Examples
202    ///
203    /// ```no_run
204    /// # use url::Url;
205    /// # let homeserver = Url::parse("http://example.com").unwrap();
206    /// # async {
207    /// use matrix_sdk::Client;
208    ///
209    /// let client = Client::new(homeserver).await?;
210    /// let user = "example";
211    ///
212    /// let response = client
213    ///     .matrix_auth()
214    ///     .login_custom(
215    ///         "org.matrix.login.jwt",
216    ///         [("token".to_owned(), "jwt_token_content".into())]
217    ///             .into_iter()
218    ///             .collect(),
219    ///     )?
220    ///     .initial_device_display_name("My bot")
221    ///     .await?;
222    ///
223    /// println!(
224    ///     "Logged in as {user}, got device_id {} and access_token {}",
225    ///     response.device_id, response.access_token,
226    /// );
227    /// # anyhow::Ok(()) };
228    /// ```
229    pub fn login_custom(
230        &self,
231        login_type: &str,
232        data: JsonObject,
233    ) -> serde_json::Result<LoginBuilder> {
234        LoginBuilder::new_custom(self.clone(), login_type, data)
235    }
236
237    /// Log into the server with a token.
238    ///
239    /// This token is usually received in the SSO flow after following the URL
240    /// provided by [`get_sso_login_url`], note that this is not the access
241    /// token of a session.
242    ///
243    /// This should only be used for the first login.
244    ///
245    /// The [`restore_session`] method should be used to restore a logged-in
246    /// client after the first login.
247    ///
248    /// A device ID should be provided through [`LoginBuilder::device_id`] to
249    /// restore the correct stores, if the device ID isn't provided a new device
250    /// will be created.
251    ///
252    /// # Arguments
253    ///
254    /// - `token` - A login token.
255    ///
256    /// # Examples
257    ///
258    /// ```no_run
259    /// use matrix_sdk::Client;
260    /// # use url::Url;
261    /// # let homeserver = Url::parse("https://example.com").unwrap();
262    /// # let redirect_url = "http://localhost:1234";
263    /// # let login_token = "token";
264    /// # async {
265    /// let client = Client::new(homeserver).await.unwrap();
266    /// let auth = client.matrix_auth();
267    /// let sso_url = auth.get_sso_login_url(redirect_url, None);
268    ///
269    /// // Let the user authenticate at the SSO URL.
270    /// // Receive the loginToken param at the redirect_url.
271    ///
272    /// let response = auth
273    ///     .login_token(login_token)
274    ///     .initial_device_display_name("My app")
275    ///     .await
276    ///     .unwrap();
277    ///
278    /// println!(
279    ///     "Logged in as {}, got device_id {} and access_token {}",
280    ///     response.user_id, response.device_id, response.access_token,
281    /// );
282    /// # };
283    /// ```
284    ///
285    /// [`get_sso_login_url`]: #method.get_sso_login_url
286    /// [`restore_session`]: #method.restore_session
287    pub fn login_token(&self, token: &str) -> LoginBuilder {
288        LoginBuilder::new_token(self.clone(), token.to_owned())
289    }
290
291    /// A higher level wrapper around the methods to complete an SSO login after
292    /// the user has logged in through a webview. This method should be used in
293    /// tandem with [`MatrixAuth::get_sso_login_url`].
294    ///
295    /// # Arguments
296    ///
297    /// - `url_or_query` - The full callback URL carrying the login token, or
298    ///   only its query string.
299    ///
300    /// # Examples
301    ///
302    /// ```no_run
303    /// use matrix_sdk::Client;
304    /// # use matrix_sdk::utils::UrlOrQuery;
305    /// # use url::Url;
306    /// # let homeserver = Url::parse("https://example.com").unwrap();
307    /// # let redirect_url = "http://localhost:1234";
308    /// # let url_or_query = UrlOrQuery::Query("loginToken=token".to_owned());
309    /// # async {
310    /// let client = Client::new(homeserver).await.unwrap();
311    /// let auth = client.matrix_auth();
312    /// let sso_url = auth.get_sso_login_url(redirect_url, None);
313    ///
314    /// // Let the user authenticate at the SSO URL.
315    /// // Receive the callback url or query string.
316    ///
317    /// let response = auth
318    ///     .login_with_sso_callback(url_or_query)
319    ///     .unwrap()
320    ///     .initial_device_display_name("My app")
321    ///     .await
322    ///     .unwrap();
323    ///
324    /// println!(
325    ///     "Logged in as {}, got device_id {} and access_token {}",
326    ///     response.user_id, response.device_id, response.access_token,
327    /// );
328    /// # };
329    /// ```
330    pub fn login_with_sso_callback(
331        &self,
332        url_or_query: UrlOrQuery,
333    ) -> Result<LoginBuilder, SsoError> {
334        #[derive(Deserialize)]
335        struct QueryParameters {
336            #[serde(rename = "loginToken")]
337            login_token: Option<String>,
338        }
339
340        let query_string = url_or_query.query().unwrap_or_default();
341        let query: QueryParameters =
342            serde_html_form::from_str(query_string).map_err(|_| SsoError::CallbackUrlInvalid)?;
343        let token = query.login_token.ok_or(SsoError::CallbackUrlInvalid)?;
344
345        Ok(self.login_token(token.as_str()))
346    }
347
348    /// Log into the server via Single Sign-On.
349    ///
350    /// This takes care of the whole SSO flow:
351    ///
352    /// - Spawn a local http server
353    /// - Provide a callback to open the SSO login URL in a web browser
354    /// - Wait for the local http server to get the loginToken
355    /// - Call [`login_token`]
356    ///
357    /// If cancellation is needed the method should be wrapped in a cancellable
358    /// task. **Note** that users with root access to the system have the
359    /// ability to snoop in on the data/token that is passed to the local HTTP
360    /// server that will be spawned.
361    ///
362    /// If you need more control over the SSO login process, you should use
363    /// [`get_sso_login_url`] and [`login_token`] directly.
364    ///
365    /// This should only be used for the first login.
366    ///
367    /// The [`restore_session`] method should be used to restore a logged-in
368    /// client after the first login.
369    ///
370    /// # Arguments
371    ///
372    /// - `use_sso_login_url` - A callback that will receive the SSO Login URL.
373    ///   It should usually be used to open the SSO URL in a browser and must
374    ///   return `Ok(())` if the URL was successfully opened. If it returns
375    ///   `Err`, the error will be forwarded.
376    ///
377    /// # Examples
378    ///
379    /// ```no_run
380    /// use matrix_sdk::Client;
381    /// # use url::Url;
382    /// # let homeserver = Url::parse("https://example.com").unwrap();
383    /// # async {
384    /// let client = Client::new(homeserver).await.unwrap();
385    ///
386    /// let response = client
387    ///     .matrix_auth()
388    ///     .login_sso(|sso_url| async move {
389    ///         // Open sso_url
390    ///         Ok(())
391    ///     })
392    ///     .initial_device_display_name("My app")
393    ///     .await
394    ///     .unwrap();
395    ///
396    /// println!(
397    ///     "Logged in as {}, got device_id {} and access_token {}",
398    ///     response.user_id, response.device_id, response.access_token
399    /// );
400    /// # };
401    /// ```
402    ///
403    /// [`get_sso_login_url`]: #method.get_sso_login_url
404    /// [`login_token`]: #method.login_token
405    /// [`restore_session`]: #method.restore_session
406    #[cfg(feature = "sso-login")]
407    pub fn login_sso<F, Fut>(&self, use_sso_login_url: F) -> SsoLoginBuilder<F>
408    where
409        F: FnOnce(String) -> Fut + Send,
410        Fut: Future<Output = Result<()>> + Send,
411    {
412        SsoLoginBuilder::new(self.clone(), use_sso_login_url)
413    }
414
415    /// Is the client logged in using the native Matrix authentication API.
416    pub fn logged_in(&self) -> bool {
417        self.client
418            .auth_ctx()
419            .auth_data
420            .get()
421            .is_some_and(|auth_data| matches!(auth_data, AuthData::Matrix))
422    }
423
424    /// Refresh the access token.
425    ///
426    /// When support for [refreshing access tokens] is activated on both the
427    /// homeserver and the client, access tokens have an expiration date and
428    /// need to be refreshed periodically. To activate support for refresh
429    /// tokens in the [`Client`], it needs to be done at login with the
430    /// [`LoginBuilder::request_refresh_token()`] method, or during account
431    /// registration.
432    ///
433    /// This method doesn't need to be called if
434    /// [`ClientBuilder::handle_refresh_tokens()`] is called during construction
435    /// of the `Client`. Otherwise, it should be called once when a refresh
436    /// token is available and an [`UnknownToken`] error is received. If this
437    /// call fails with another [`UnknownToken`] error, it means that the
438    /// session needs to be logged in again.
439    ///
440    /// It can also be called at any time when a refresh token is available, it
441    /// will invalidate the previous access token.
442    ///
443    /// The new tokens in the response will be used by the `Client` and should
444    /// be persisted to be able to [restore the session]. The response will
445    /// always contain an access token that replaces the previous one. It can
446    /// also contain a refresh token, in which case it will also replace the
447    /// previous one.
448    ///
449    /// This method is protected behind a lock, so calling this method several
450    /// times at once will only call the endpoint once and all subsequent calls
451    /// will wait for the result of the first call. The first call will return
452    /// `Ok(Some(response))` or the [`HttpError`] returned by the endpoint,
453    /// while the others will return `Ok(None)` if the token was refreshed by
454    /// the first call or a [`RefreshTokenError`] error, if it failed.
455    ///
456    /// # Examples
457    ///
458    /// ```no_run
459    /// use matrix_sdk::{Client, Error};
460    /// use url::Url;
461    /// # async {
462    /// # fn get_credentials() -> (&'static str, &'static str) { ("", "") };
463    /// # fn persist_session(_: Option<matrix_sdk::AuthSession>) {};
464    ///
465    /// let homeserver = Url::parse("http://example.com")?;
466    /// let client = Client::new(homeserver).await?;
467    ///
468    /// let (user, password) = get_credentials();
469    /// let response = client
470    ///     .matrix_auth()
471    ///     .login_username(user, password)
472    ///     .initial_device_display_name("My App")
473    ///     .request_refresh_token()
474    ///     .send()
475    ///     .await?;
476    ///
477    /// persist_session(client.session());
478    ///
479    /// // Handle when an `M_UNKNOWN_TOKEN` error is encountered.
480    /// async fn on_unknown_token_err(client: &Client) -> Result<(), Error> {
481    ///     let auth = client.matrix_auth();
482    ///
483    ///     if client
484    ///         .session_tokens()
485    ///         .and_then(|tokens| tokens.refresh_token)
486    ///         .is_some()
487    ///         && auth.refresh_access_token().await.is_ok()
488    ///     {
489    ///         persist_session(client.session());
490    ///         return Ok(());
491    ///     }
492    ///
493    ///     let (user, password) = get_credentials();
494    ///     auth.login_username(user, password)
495    ///         .request_refresh_token()
496    ///         .send()
497    ///         .await?;
498    ///
499    ///     persist_session(client.session());
500    ///
501    ///     Ok(())
502    /// }
503    /// # anyhow::Ok(()) };
504    /// ```
505    ///
506    /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
507    /// [`UnknownToken`]: ruma::api::error::ErrorKind::UnknownToken
508    /// [restore the session]: Client::restore_session
509    /// [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens
510    pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> {
511        macro_rules! fail {
512            ($lock:expr, $err:expr) => {
513                let error = $err;
514                *$lock = Err(error.clone());
515                return Err(error);
516            };
517        }
518
519        let refresh_token_lock = &self.client.auth_ctx().refresh_token_lock;
520        let Ok(mut guard) = refresh_token_lock.try_lock() else {
521            // Somebody else is also doing a token refresh; wait for it to
522            // finish first.
523            return refresh_token_lock.lock().await.clone();
524        };
525
526        let Some(mut session_tokens) = self.client.session_tokens() else {
527            fail!(guard, RefreshTokenError::RefreshTokenRequired);
528        };
529        let Some(refresh_token) = session_tokens.refresh_token.clone() else {
530            fail!(guard, RefreshTokenError::RefreshTokenRequired);
531        };
532
533        let request = refresh_token::v3::Request::new(refresh_token);
534        let res = self.client.send_inner(request, None, Default::default()).await;
535
536        match res {
537            Ok(res) => {
538                *guard = Ok(());
539
540                session_tokens.access_token = res.access_token;
541                if let Some(refresh_token) = res.refresh_token {
542                    session_tokens.refresh_token = Some(refresh_token);
543                }
544
545                self.client.auth_ctx().set_session_tokens(session_tokens);
546
547                if let Some(save_session_callback) =
548                    self.client.inner.auth_ctx.save_session_callback.get()
549                    && let Err(err) = save_session_callback(self.client.clone())
550                {
551                    error!("when saving session after refresh: {err}");
552                }
553
554                _ = self
555                    .client
556                    .inner
557                    .auth_ctx
558                    .session_change_sender
559                    .send(SessionChange::TokensRefreshed);
560
561                Ok(())
562            }
563            Err(error) => {
564                let error = RefreshTokenError::MatrixAuth(error.into());
565                fail!(guard, error);
566            }
567        }
568    }
569
570    /// Register a user to the server.
571    ///
572    /// If registration was successful and a session token was returned by the
573    /// server, the client session is set (the client is logged in).
574    ///
575    /// # Arguments
576    ///
577    /// - `registration` - The easiest way to create this request is using the
578    ///   [`register::v3::Request`] itself.
579    ///
580    /// # Examples
581    ///
582    /// ```no_run
583    /// use matrix_sdk::{
584    ///     Client,
585    ///     ruma::api::client::{
586    ///         account::register::v3::Request as RegistrationRequest, uiaa,
587    ///     },
588    /// };
589    /// # use url::Url;
590    /// # let homeserver = Url::parse("http://example.com").unwrap();
591    /// # async {
592    ///
593    /// let mut request = RegistrationRequest::new();
594    /// request.username = Some("user".to_owned());
595    /// request.password = Some("password".to_owned());
596    /// request.auth = Some(uiaa::AuthData::FallbackAcknowledgement(
597    ///     uiaa::FallbackAcknowledgement::new("foobar".to_owned()),
598    /// ));
599    ///
600    /// let client = Client::new(homeserver).await.unwrap();
601    /// client.matrix_auth().register(request).await;
602    /// # };
603    /// ```
604    #[instrument(skip_all)]
605    pub async fn register(&self, request: register::v3::Request) -> Result<register::v3::Response> {
606        let homeserver = self.client.homeserver();
607        info!("Registering to {homeserver}");
608
609        #[cfg(feature = "e2e-encryption")]
610        let login_info = match (&request.username, &request.password) {
611            (Some(u), Some(p)) => Some(login::v3::LoginInfo::Password(login::v3::Password::new(
612                UserIdentifier::Matrix(MatrixUserIdentifier::new(u.into())),
613                p.clone(),
614            ))),
615            _ => None,
616        };
617
618        let response = self.client.send(request).await?;
619        if let Some(session) = MatrixSession::from_register_response(&response) {
620            let _ = self
621                .set_session(
622                    session,
623                    RoomLoadSettings::default(),
624                    #[cfg(feature = "e2e-encryption")]
625                    login_info,
626                )
627                .await;
628        }
629        Ok(response)
630    }
631    /// Log out the current user.
632    pub async fn logout(&self) -> HttpResult<logout::v3::Response> {
633        let request = logout::v3::Request::new();
634        self.client.send(request).await
635    }
636
637    /// Get the whole native Matrix authentication session info of this client.
638    ///
639    /// Will be `None` if the client has not been logged in with the native
640    /// Matrix Authentication API.
641    ///
642    /// Can be used with [`MatrixAuth::restore_session`] to restore a previously
643    /// logged-in session.
644    pub fn session(&self) -> Option<MatrixSession> {
645        let meta = self.client.session_meta()?;
646        let tokens = self.client.session_tokens()?;
647        Some(MatrixSession { meta: meta.to_owned(), tokens })
648    }
649
650    /// Restore a previously logged in session.
651    ///
652    /// This can be used to restore the client to a logged in state, loading all
653    /// the stored state and encryption keys.
654    ///
655    /// Alternatively, if the whole session isn't stored the [`login`] method
656    /// can be used with a device ID.
657    ///
658    /// # Persisting the store
659    ///
660    /// Restoring only reattaches the client to its stored state; it does not
661    /// recreate that state. The same persistent store used during the original
662    /// login (for example via [`ClientBuilder::sqlite_store()`]) must be
663    /// configured on the [`ClientBuilder`] when the session is restored,
664    /// otherwise the encryption keys and room state will not be available. When
665    /// the `e2e-encryption` feature is enabled, restoring on top of an
666    /// in-memory store will leave the client unable to send or receive
667    /// encrypted messages. See the [`persist_session`] example for a full
668    /// walk-through.
669    ///
670    /// # Arguments
671    ///
672    /// - `session` - A session that the user already has from a previous login
673    ///   call.
674    ///
675    /// - `room_load_settings` — Specify how many rooms must be restored; use
676    ///   `::default()` if you don't know which value to pick.
677    ///
678    /// # Panics
679    ///
680    /// Panics if a session was already restored or logged in.
681    ///
682    /// # Examples
683    ///
684    /// ```no_run
685    /// use matrix_sdk::{
686    ///     Client, SessionMeta, SessionTokens,
687    ///     authentication::matrix::MatrixSession,
688    ///     ruma::{owned_device_id, owned_user_id},
689    /// };
690    /// # use url::Url;
691    /// # async {
692    ///
693    /// let homeserver = Url::parse("http://example.com")?;
694    /// let client = Client::new(homeserver).await?;
695    ///
696    /// let session = MatrixSession {
697    ///     meta: SessionMeta {
698    ///         user_id: owned_user_id!("@example:localhost"),
699    ///         device_id: owned_device_id!("MYDEVICEID"),
700    ///     },
701    ///     tokens: SessionTokens {
702    ///         access_token: "My-Token".to_owned(),
703    ///         refresh_token: None,
704    ///     },
705    /// };
706    ///
707    /// client.restore_session(session).await?;
708    /// # anyhow::Ok(()) };
709    /// ```
710    ///
711    /// The `MatrixSession` object can also be created from the response the
712    /// [`LoginBuilder::send()`] method returns:
713    ///
714    /// ```no_run
715    /// use matrix_sdk::{Client, store::RoomLoadSettings};
716    /// use url::Url;
717    /// # async {
718    ///
719    /// let homeserver = Url::parse("http://example.com")?;
720    /// let client = Client::new(homeserver).await?;
721    /// let auth = client.matrix_auth();
722    ///
723    /// let response = auth.login_username("example", "my-password").send().await?;
724    ///
725    /// // Persist the `MatrixSession` so it can later be used to restore the login.
726    ///
727    /// auth.restore_session((&response).into(), RoomLoadSettings::default())
728    ///     .await?;
729    /// # anyhow::Ok(()) };
730    /// ```
731    ///
732    /// [`login`]: #method.login
733    /// [`LoginBuilder::send()`]: crate::authentication::matrix::LoginBuilder::send
734    /// [`ClientBuilder`]: crate::ClientBuilder
735    /// [`ClientBuilder::sqlite_store()`]: crate::ClientBuilder::sqlite_store
736    /// [`persist_session`]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples/persist_session
737    #[instrument(skip_all)]
738    pub async fn restore_session(
739        &self,
740        session: MatrixSession,
741        room_load_settings: RoomLoadSettings,
742    ) -> Result<()> {
743        debug!("Restoring Matrix auth session");
744        self.set_session(
745            session,
746            room_load_settings,
747            #[cfg(feature = "e2e-encryption")]
748            None,
749        )
750        .await?;
751        debug!("Done restoring Matrix auth session");
752        Ok(())
753    }
754
755    /// Receive a login response and update the homeserver and the base client
756    /// if needed.
757    ///
758    /// # Arguments
759    ///
760    /// - `response` - A successful login response.
761    pub(crate) async fn receive_login_response(
762        &self,
763        response: &login::v3::Response,
764        #[cfg(feature = "e2e-encryption")] login_info: Option<login::v3::LoginInfo>,
765    ) -> Result<()> {
766        self.client.maybe_update_login_well_known(response.well_known.as_ref());
767
768        self.set_session(
769            response.into(),
770            RoomLoadSettings::default(),
771            #[cfg(feature = "e2e-encryption")]
772            login_info,
773        )
774        .await?;
775
776        Ok(())
777    }
778
779    /// Set the Matrix authentication session.
780    ///
781    /// # Arguments
782    ///
783    /// - `session` — The session being opened.
784    /// - `room_load_settings` — Specify how much rooms must be restored; use
785    ///   `::default()` if you don't know which value to pick.
786    ///
787    /// # Panic
788    ///
789    /// Panics if authentication data was already set.
790    async fn set_session(
791        &self,
792        session: MatrixSession,
793        room_load_settings: RoomLoadSettings,
794        #[cfg(feature = "e2e-encryption")] login_info: Option<login::v3::LoginInfo>,
795    ) -> Result<()> {
796        // This API doesn't have any data but by setting this variant we protect
797        // the user from using both authentication APIs at once.
798        self.client
799            .auth_ctx()
800            .auth_data
801            .set(AuthData::Matrix)
802            .expect("Client authentication data was already set");
803        self.client.auth_ctx().set_session_tokens(session.tokens);
804        self.client
805            .base_client()
806            .activate(
807                session.meta,
808                room_load_settings,
809                #[cfg(feature = "e2e-encryption")]
810                None,
811            )
812            .await?;
813
814        #[cfg(feature = "e2e-encryption")]
815        {
816            use ruma::api::client::uiaa::{AuthData, Password};
817
818            let auth_data = match login_info {
819                Some(login::v3::LoginInfo::Password(login::v3::Password {
820                    identifier: Some(identifier),
821                    password,
822                    ..
823                })) => Some(AuthData::Password(Password::new(identifier, password))),
824                // Other methods can't be immediately translated to an auth.
825                _ => None,
826            };
827
828            self.client.encryption().spawn_initialization_task(auth_data).await;
829        }
830
831        Ok(())
832    }
833}
834
835/// A user session using the native Matrix authentication API.
836///
837/// # Examples
838///
839/// ```
840/// use matrix_sdk::{
841///     SessionMeta, SessionTokens, authentication::matrix::MatrixSession,
842/// };
843/// use ruma::{owned_device_id, owned_user_id};
844///
845/// let session = MatrixSession {
846///     meta: SessionMeta {
847///         user_id: owned_user_id!("@example:localhost"),
848///         device_id: owned_device_id!("MYDEVICEID"),
849///     },
850///     tokens: SessionTokens {
851///         access_token: "My-Token".to_owned(),
852///         refresh_token: None,
853///     },
854/// };
855///
856/// assert_eq!(session.meta.device_id, "MYDEVICEID");
857/// ```
858#[derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
859pub struct MatrixSession {
860    /// The Matrix user session info.
861    #[serde(flatten)]
862    pub meta: SessionMeta,
863
864    /// The tokens used for authentication.
865    #[serde(flatten)]
866    pub tokens: SessionTokens,
867}
868
869#[cfg(not(tarpaulin_include))]
870impl fmt::Debug for MatrixSession {
871    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
872        f.debug_struct("MatrixSession").field("meta", &self.meta).finish_non_exhaustive()
873    }
874}
875
876impl From<&login::v3::Response> for MatrixSession {
877    fn from(response: &login::v3::Response) -> Self {
878        let login::v3::Response { user_id, access_token, device_id, refresh_token, .. } = response;
879        Self {
880            meta: SessionMeta { user_id: user_id.clone(), device_id: device_id.clone() },
881            tokens: SessionTokens {
882                access_token: access_token.clone(),
883                refresh_token: refresh_token.clone(),
884            },
885        }
886    }
887}
888
889impl MatrixSession {
890    #[allow(clippy::question_mark)] // clippy falsely complains about the let-unpacking
891    fn from_register_response(response: &register::v3::Response) -> Option<Self> {
892        let register::v3::Response { user_id, access_token, device_id, refresh_token, .. } =
893            response;
894        Some(Self {
895            meta: SessionMeta { user_id: user_id.clone(), device_id: device_id.clone()? },
896            tokens: SessionTokens {
897                access_token: access_token.clone()?,
898                refresh_token: refresh_token.clone(),
899            },
900        })
901    }
902}