Skip to main content

matrix_sdk/authentication/matrix/
login_builder.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2// Copyright 2022 Kévin Commaille
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16// TODO: Re-enable once https://github.com/rust-lang/rust-clippy/issues/17812 is resolved.
17// #![cfg_attr(not(target_family = "wasm"), deny(clippy::future_not_send))]
18
19#[cfg(feature = "sso-login")]
20use std::future::Future;
21use std::future::IntoFuture;
22
23use matrix_sdk_common::boxed_into_future;
24use ruma::{
25    api::client::{session::login, uiaa::UserIdentifier},
26    assign,
27    serde::JsonObject,
28};
29use tracing::{info, instrument};
30
31use super::MatrixAuth;
32#[cfg(feature = "sso-login")]
33use crate::utils::local_server::LocalServerBuilder;
34use crate::{Result, config::RequestConfig};
35
36/// The login method.
37///
38/// See also [`LoginInfo`][login::v3::LoginInfo] and [the spec].
39///
40/// [the spec]: https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3login
41enum LoginMethod {
42    /// Login type `m.login.password`
43    UserPassword {
44        id: UserIdentifier,
45        password: String,
46    },
47    /// Login type `m.token`
48    Token(String),
49    Custom(login::v3::LoginInfo),
50}
51
52impl LoginMethod {
53    fn id(&self) -> Option<&UserIdentifier> {
54        match self {
55            LoginMethod::UserPassword { id, .. } => Some(id),
56            LoginMethod::Token(_) | LoginMethod::Custom(_) => None,
57        }
58    }
59
60    fn tracing_desc(&self) -> &'static str {
61        match self {
62            LoginMethod::UserPassword { .. } => "identifier and password",
63            LoginMethod::Token(_) => "token",
64            LoginMethod::Custom(_) => "custom",
65        }
66    }
67
68    fn into_login_info(self) -> login::v3::LoginInfo {
69        match self {
70            LoginMethod::UserPassword { id, password } => {
71                login::v3::LoginInfo::Password(login::v3::Password::new(id, password))
72            }
73            LoginMethod::Token(token) => login::v3::LoginInfo::Token(login::v3::Token::new(token)),
74            LoginMethod::Custom(login_info) => login_info,
75        }
76    }
77}
78
79/// Builder type used to configure optional settings for logging in with a
80/// username or token.
81///
82/// Created with [`MatrixAuth::login_username`] or [`MatrixAuth::login_token`].
83/// Finalized with [`.send()`](Self::send).
84#[allow(missing_debug_implementations)]
85pub struct LoginBuilder {
86    auth: MatrixAuth,
87    login_method: LoginMethod,
88    device_id: Option<String>,
89    initial_device_display_name: Option<String>,
90    request_refresh_token: bool,
91}
92
93impl LoginBuilder {
94    fn new(auth: MatrixAuth, login_method: LoginMethod) -> Self {
95        Self {
96            auth,
97            login_method,
98            device_id: None,
99            initial_device_display_name: None,
100            request_refresh_token: false,
101        }
102    }
103
104    pub(super) fn new_password(auth: MatrixAuth, id: UserIdentifier, password: String) -> Self {
105        Self::new(auth, LoginMethod::UserPassword { id, password })
106    }
107
108    pub(super) fn new_token(auth: MatrixAuth, token: String) -> Self {
109        Self::new(auth, LoginMethod::Token(token))
110    }
111
112    pub(super) fn new_custom(
113        auth: MatrixAuth,
114        login_type: &str,
115        data: JsonObject,
116    ) -> serde_json::Result<Self> {
117        let login_info = login::v3::LoginInfo::new(login_type, data)?;
118        Ok(Self::new(auth, LoginMethod::Custom(login_info)))
119    }
120
121    /// Set the device ID.
122    ///
123    /// The device ID is a unique ID that will be associated with this session.
124    /// If not set, the homeserver will create one. Can be an existing device ID
125    /// from a previous login call. Note that this should be done only if the
126    /// client also holds the corresponding encryption keys.
127    pub fn device_id(mut self, value: &str) -> Self {
128        self.device_id = Some(value.to_owned());
129        self
130    }
131
132    /// Set the initial device display name.
133    ///
134    /// The device display name is the public name that will be associated with
135    /// the device ID. Only necessary the first time you log in with this device
136    /// ID. It can be changed later.
137    pub fn initial_device_display_name(mut self, value: &str) -> Self {
138        self.initial_device_display_name = Some(value.to_owned());
139        self
140    }
141
142    /// Advertise support for [refreshing access tokens].
143    ///
144    /// By default, the `Client` won't handle refreshing access tokens, so
145    /// [`Client::refresh_access_token()`] or
146    /// [`MatrixAuth::refresh_access_token()`] needs to be called manually.
147    ///
148    /// This behavior can be changed by calling [`handle_refresh_tokens()`] when
149    /// building the `Client`.
150    ///
151    /// _Note_ that refreshing access tokens might not be supported or might be
152    /// enforced by the homeserver regardless of this setting.
153    ///
154    /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
155    /// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token
156    /// [`handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens
157    pub fn request_refresh_token(mut self) -> Self {
158        self.request_refresh_token = true;
159        self
160    }
161
162    /// Send the login request.
163    ///
164    /// Instead of calling this function and `.await`ing its return value, you
165    /// can also `.await` the `LoginBuilder` directly.
166    ///
167    /// # Panics
168    ///
169    /// Panics if a session was already restored or logged in.
170    #[instrument(
171        target = "matrix_sdk::client",
172        name = "login",
173        skip_all,
174        fields(method = self.login_method.tracing_desc()),
175    )]
176    pub async fn send(self) -> Result<login::v3::Response> {
177        let client = &self.auth.client;
178        let homeserver = client.homeserver();
179        info!(homeserver = homeserver.as_str(), identifier = ?self.login_method.id(), "Logging in");
180
181        let login_info = self.login_method.into_login_info();
182
183        let request = assign!(login::v3::Request::new(login_info.clone()), {
184            device_id: self.device_id.map(Into::into),
185            initial_device_display_name: self.initial_device_display_name,
186            refresh_token: self.request_refresh_token,
187        });
188
189        let response =
190            client.send(request).with_request_config(RequestConfig::short_retry()).await?;
191        self.auth
192            .receive_login_response(
193                &response,
194                #[cfg(feature = "e2e-encryption")]
195                Some(login_info),
196            )
197            .await?;
198
199        Ok(response)
200    }
201}
202
203impl IntoFuture for LoginBuilder {
204    type Output = Result<login::v3::Response>;
205    boxed_into_future!();
206
207    fn into_future(self) -> Self::IntoFuture {
208        Box::pin(self.send())
209    }
210}
211
212/// Builder type used to configure optional settings for logging in via SSO.
213///
214/// Created with [`MatrixAuth::login_sso`]. Finalized with
215/// [`.send()`](Self::send).
216#[cfg(feature = "sso-login")]
217#[allow(missing_debug_implementations)]
218pub struct SsoLoginBuilder<F> {
219    auth: MatrixAuth,
220    use_sso_login_url: F,
221    device_id: Option<String>,
222    initial_device_display_name: Option<String>,
223    server_builder: Option<LocalServerBuilder>,
224    identity_provider_id: Option<String>,
225    request_refresh_token: bool,
226}
227
228#[cfg(feature = "sso-login")]
229impl<F, Fut> SsoLoginBuilder<F>
230where
231    F: FnOnce(String) -> Fut + Send,
232    Fut: Future<Output = Result<()>> + Send,
233{
234    pub(super) fn new(auth: MatrixAuth, use_sso_login_url: F) -> Self {
235        Self {
236            auth,
237            use_sso_login_url,
238            device_id: None,
239            initial_device_display_name: None,
240            server_builder: None,
241            identity_provider_id: None,
242            request_refresh_token: false,
243        }
244    }
245
246    /// Set the device ID.
247    ///
248    /// The device ID is a unique ID that will be associated with this session.
249    /// If not set, the homeserver will create one. Can be an existing device ID
250    /// from a previous login call. Note that this should be done only if the
251    /// client also holds the corresponding encryption keys.
252    pub fn device_id(mut self, value: &str) -> Self {
253        self.device_id = Some(value.to_owned());
254        self
255    }
256
257    /// Set the initial device display name.
258    ///
259    /// The device display name is the public name that will be associated with
260    /// the device ID. Only necessary the first time you login with this device
261    /// ID. It can be changed later.
262    pub fn initial_device_display_name(mut self, value: &str) -> Self {
263        self.initial_device_display_name = Some(value.to_owned());
264        self
265    }
266
267    /// Customize the settings used to construct the server where the end-user
268    /// will be redirected.
269    ///
270    /// If this is not set, the default settings of [`LocalServerBuilder`] will
271    /// be used.
272    pub fn server_builder(mut self, builder: LocalServerBuilder) -> Self {
273        self.server_builder = Some(builder);
274        self
275    }
276
277    /// Set the ID of the identity provider to log in with.
278    pub fn identity_provider_id(mut self, value: &str) -> Self {
279        self.identity_provider_id = Some(value.to_owned());
280        self
281    }
282
283    /// Advertise support for [refreshing access tokens].
284    ///
285    /// By default, the `Client` won't handle refreshing access tokens, so
286    /// [`Client::refresh_access_token()`] or
287    /// [`MatrixAuth::refresh_access_token()`] needs to be called manually.
288    ///
289    /// This behavior can be changed by calling [`handle_refresh_tokens()`] when
290    /// building the `Client`.
291    ///
292    /// _Note_ that refreshing access tokens might not be supported or might be
293    /// enforced by the homeserver regardless of this setting.
294    ///
295    /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
296    /// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token
297    /// [`handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens
298    pub fn request_refresh_token(mut self) -> Self {
299        self.request_refresh_token = true;
300        self
301    }
302
303    /// Send the login request.
304    ///
305    /// Instead of calling this function and `.await`ing its return value, you
306    /// can also `.await` the `SsoLoginBuilder` directly.
307    ///
308    /// # Panics
309    ///
310    /// Panics if a session was already restored or logged in.
311    #[instrument(target = "matrix_sdk::client", name = "login", skip_all, fields(method = "sso"))]
312    pub async fn send(self) -> Result<login::v3::Response> {
313        use std::io::Error as IoError;
314
315        use serde::Deserialize;
316
317        let client = &self.auth.client;
318        let homeserver = client.homeserver();
319        info!(%homeserver, "Logging in");
320
321        #[derive(Deserialize)]
322        struct QueryParameters {
323            #[serde(rename = "loginToken")]
324            login_token: String,
325        }
326
327        let server_builder = self.server_builder.unwrap_or_default();
328        let (redirect_url, server_handle) = server_builder.spawn().await?;
329
330        let sso_url = self
331            .auth
332            .get_sso_login_url(redirect_url.as_str(), self.identity_provider_id.as_deref())
333            .await?;
334
335        (self.use_sso_login_url)(sso_url).await?;
336
337        let query_string =
338            server_handle.await.ok_or_else(|| IoError::other("Could not get the loginToken"))?;
339        let token = serde_html_form::from_str::<QueryParameters>(&query_string)
340            .map_err(IoError::other)?
341            .login_token;
342
343        let login_builder = LoginBuilder {
344            device_id: self.device_id,
345            initial_device_display_name: self.initial_device_display_name,
346            request_refresh_token: self.request_refresh_token,
347            ..LoginBuilder::new_token(self.auth, token)
348        };
349        login_builder.send().await
350    }
351}
352
353#[cfg(feature = "sso-login")]
354impl<F, Fut> IntoFuture for SsoLoginBuilder<F>
355where
356    F: FnOnce(String) -> Fut + Send + 'static,
357    Fut: Future<Output = Result<()>> + Send + 'static,
358{
359    type Output = Result<login::v3::Response>;
360    boxed_into_future!();
361
362    fn into_future(self) -> Self::IntoFuture {
363        Box::pin(self.send())
364    }
365}