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: ®ister::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}