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