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