Skip to main content

matrix_sdk/authentication/oauth/qrcode/
mod.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types for the QR code login support defined in [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108).
16//!
17//! Please note, QR code logins are only supported when using OAuth 2.0 as the
18//! authentication mechanism, native Matrix authentication does not support it.
19//!
20//! This currently only implements the case where the new device is scanning the
21//! QR code. To log in using a QR code, please take a look at the
22//! [`OAuth::login_with_qr_code()`] method.
23
24use std::sync::Arc;
25
26use as_variant::as_variant;
27pub use matrix_sdk_base::crypto::types::qr_login::{
28    LoginQrCodeDecodeError, Msc4108IntentData, QrCodeData, QrCodeIntent, QrCodeIntentData,
29};
30use matrix_sdk_base::crypto::{SecretImportError, store::SecretsBundleExportError};
31pub use oauth2::{
32    ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError,
33    RequestTokenError, StandardErrorResponse,
34    basic::{BasicErrorResponse, BasicRequestTokenError},
35};
36use ruma::api::error::ErrorKind;
37use thiserror::Error;
38use tokio::sync::Mutex;
39use url::Url;
40pub use vodozemac::ecies::{Error as EciesError, MessageDecodeError as EciesMessageDecodeError};
41
42mod grant;
43mod login;
44mod messages;
45mod rendezvous_channel;
46mod secure_channel;
47
48pub use self::{
49    grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode},
50    login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode},
51    messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage},
52};
53use super::CrossProcessRefreshLockError;
54#[cfg(doc)]
55use super::OAuth;
56use crate::HttpError;
57
58/// The error type for failures while trying to log in a new device using a QR
59/// code.
60#[derive(Debug, Error)]
61#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
62pub enum QRCodeLoginError {
63    /// An error happened while we were communicating with the OAuth 2.0
64    /// authorization server.
65    #[error(transparent)]
66    OAuth(#[from] DeviceAuthorizationOAuthError),
67
68    /// The other device has signaled to us that the login has failed.
69    #[error("The login failed, reason: {reason}")]
70    LoginFailure {
71        /// The reason, as signaled by the other device, for the login failure.
72        reason: LoginFailureReason,
73        /// The homeserver that we attempted to log in to.
74        homeserver: Option<Url>,
75    },
76
77    /// An unexpected message was received from the other device.
78    #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
79    UnexpectedMessage {
80        /// The message we expected.
81        expected: &'static str,
82        /// The message we received instead.
83        received: Box<QrAuthMessage>,
84    },
85
86    /// An error happened while exchanging messages with the other device.
87    #[error(transparent)]
88    SecureChannel(SecureChannelError),
89
90    /// The rendezvous session was not found and might have expired.
91    #[error("The rendezvous session was not found and might have expired")]
92    NotFound,
93
94    /// The cross-process refresh lock failed to be initialized.
95    #[error(transparent)]
96    CrossProcessRefreshLock(#[from] CrossProcessRefreshLockError),
97
98    /// An error happened while we were trying to discover our user and device
99    /// ID, after we have acquired an access token from the OAuth 2.0
100    /// authorization server.
101    #[error(transparent)]
102    UserIdDiscovery(HttpError),
103
104    /// We failed to set the session tokens after we figured out our device and
105    /// user IDs.
106    #[error(transparent)]
107    SessionTokens(crate::Error),
108
109    /// The device keys failed to be uploaded after we successfully logged in.
110    #[error(transparent)]
111    DeviceKeyUpload(crate::Error),
112
113    /// The secrets bundle we received from the existing device failed to be
114    /// imported.
115    #[error(transparent)]
116    SecretImport(#[from] SecretImportError),
117
118    /// The other party told us to use a different homeserver but we failed to
119    /// reset the server URL.
120    #[error(transparent)]
121    ServerReset(crate::Error),
122}
123
124impl From<SecureChannelError> for QRCodeLoginError {
125    fn from(e: SecureChannelError) -> Self {
126        match e {
127            SecureChannelError::RendezvousChannel(ref http_error) => {
128                if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
129                    return Self::NotFound;
130                }
131                Self::SecureChannel(e)
132            }
133            e => Self::SecureChannel(e),
134        }
135    }
136}
137
138/// The error type for failures while trying to grant log in to a new device
139/// using a QR code.
140#[derive(Debug, Error)]
141pub enum QRCodeGrantLoginError {
142    /// Secrets backup not set up.
143    #[error("Secrets backup not set up")]
144    MissingSecretsBackup(Option<SecretsBundleExportError>),
145
146    /// The check code was incorrect.
147    #[error("The check code was incorrect")]
148    InvalidCheckCode,
149
150    /// The rendezvous session was not found and might have expired.
151    #[error("The rendezvous session was not found and might have expired")]
152    NotFound,
153
154    /// Auth handshake error.
155    #[error("Auth handshake error: {0}")]
156    Unknown(String),
157
158    /// Unsupported protocol.
159    #[error("Unsupported protocol: {0}")]
160    UnsupportedProtocol(LoginProtocolType),
161
162    /// The requested device ID is already in use.
163    #[error("The requested device ID is already in use")]
164    DeviceIDAlreadyInUse,
165
166    /// The requested device was not returned by the homeserver.
167    #[error("The requested device was not returned by the homeserver")]
168    DeviceNotFound,
169
170    /// An error happened while exchanging messages with the other device.
171    #[error(transparent)]
172    SecureChannel(SecureChannelError),
173
174    /// An unexpected message was received from the other device.
175    #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
176    UnexpectedMessage {
177        /// The message we expected.
178        expected: &'static str,
179        /// The message we received instead.
180        received: Box<QrAuthMessage>,
181    },
182
183    /// The other device has signaled to us that the login has failed.
184    #[error("The login failed, reason: {reason}")]
185    LoginFailure {
186        /// The reason, as signaled by the other device, for the login failure.
187        reason: LoginFailureReason,
188    },
189}
190
191impl From<SecureChannelError> for QRCodeGrantLoginError {
192    fn from(e: SecureChannelError) -> Self {
193        match e {
194            SecureChannelError::RendezvousChannel(ref http_error) => {
195                if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
196                    return Self::NotFound;
197                }
198                Self::SecureChannel(e)
199            }
200            SecureChannelError::InvalidCheckCode => Self::InvalidCheckCode,
201            e => Self::SecureChannel(e),
202        }
203    }
204}
205
206impl From<SecretsBundleExportError> for QRCodeGrantLoginError {
207    fn from(e: SecretsBundleExportError) -> Self {
208        Self::MissingSecretsBackup(Some(e))
209    }
210}
211
212/// Error type describing failures in the interaction between the device
213/// attempting to log in and the OAuth 2.0 authorization server.
214#[derive(Debug, Error)]
215pub enum DeviceAuthorizationOAuthError {
216    /// A generic OAuth 2.0 error happened while we were attempting to register
217    /// the device with the OAuth 2.0 authorization server.
218    #[error(transparent)]
219    OAuth(#[from] crate::authentication::oauth::OAuthError),
220
221    /// The OAuth 2.0 server doesn't support the device authorization grant.
222    #[error("OAuth 2.0 server doesn't support the device authorization grant")]
223    NoDeviceAuthorizationEndpoint,
224
225    /// An error happened while we attempted to request a device authorization
226    /// from the OAuth 2.0 authorization server.
227    #[error(transparent)]
228    DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
229
230    /// An error happened while waiting for the access token to be issued and
231    /// sent to us by the OAuth 2.0 authorization server.
232    #[error(transparent)]
233    RequestToken(
234        #[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
235    ),
236}
237
238impl DeviceAuthorizationOAuthError {
239    /// If the [`DeviceAuthorizationOAuthError`] is of the
240    /// [`DeviceCodeErrorResponseType`] error variant, return it.
241    pub fn as_request_token_error(&self) -> Option<&DeviceCodeErrorResponseType> {
242        let error = as_variant!(self, DeviceAuthorizationOAuthError::RequestToken)?;
243        let request_token_error = as_variant!(error, RequestTokenError::ServerResponse)?;
244
245        Some(request_token_error.error())
246    }
247}
248
249/// Error type which describes failures when messages which are received over
250/// the secure channel fail to be decoded.
251#[derive(Debug, Error)]
252pub enum MessageDecodeError {
253    /// A received message has failed to be decoded.
254    #[error(transparent)]
255    Ecies(#[from] EciesMessageDecodeError),
256    /// A message we received over the secure channel was not a valid UTF-8
257    /// encoded string.
258    #[error(transparent)]
259    Utf8(#[from] std::str::Utf8Error),
260    /// A message couldn't be deserialized from JSON.
261    #[error(transparent)]
262    Json(#[from] serde_json::Error),
263}
264
265/// Error type for decryption failures of the secure channel.
266#[derive(Debug, Error)]
267pub enum DecryptionError {
268    /// A ECIES message failed to be decrypted.
269    #[error(transparent)]
270    Ecies(#[from] EciesError),
271}
272
273/// Error type for failures in when receiving or sending messages over the
274/// secure channel.
275#[derive(Debug, Error)]
276pub enum SecureChannelError {
277    /// A message has failed to be decrypted.
278    #[error(transparent)]
279    Decryption(#[from] DecryptionError),
280
281    /// A received message has failed to be decoded.
282    #[error(transparent)]
283    MessageDecode(#[from] MessageDecodeError),
284
285    /// The secure channel failed to be established because it received an
286    /// unexpected message.
287    #[error(
288        "The secure channel setup has received an unexpected message, expected: {expected}, got {received}"
289    )]
290    SecureChannelMessage {
291        /// The secure channel message we expected.
292        expected: &'static str,
293        /// The secure channel message we received instead.
294        received: String,
295    },
296
297    /// The secure channel could not have been established, the check code was
298    /// invalid.
299    #[error("The secure channel could not have been established, the check code was invalid")]
300    InvalidCheckCode,
301
302    /// An error happened in the underlying rendezvous channel.
303    #[error("Error in the rendezvous channel: {0:?}")]
304    RendezvousChannel(#[from] HttpError),
305
306    /// Both devices have advertised the same intent in the login attempt, i.e.
307    /// both sides claim to be a new device.
308    #[error(
309        "The secure channel could not have been established, \
310         the two devices have the same login intent"
311    )]
312    InvalidIntent,
313
314    /// The secure channel could not have been established, the check code
315    /// cannot be received.
316    #[error(
317        "The secure channel could not have been established, \
318         the check code cannot be received"
319    )]
320    CannotReceiveCheckCode,
321
322    #[error("The QR code specifies an unsupported protocol version")]
323    /// The QR code specifies an unsupported protocol version.
324    UnsupportedQrCodeType,
325}
326
327/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] or
328/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the
329/// one scanning the QR code.
330///
331/// We have established the secure channel, but we need to let the other side
332/// know about the check code so they can verify that the secure channel is
333/// indeed secure.
334#[derive(Clone, Debug)]
335pub struct QrProgress {
336    /// The check code we need to, out of band, send to the other device.
337    pub check_code: u8,
338}
339
340/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] and
341/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the
342/// one generating the QR code.
343///
344/// We have established the secure channel, but we need to let the other device
345/// know about the [`QrCodeData`] so they can connect to the channel and let us
346/// know about the check code so we can verify that the channel is indeed
347/// secure.
348#[derive(Clone, Debug)]
349pub enum GeneratedQrProgress {
350    /// The QR code has been created and this device is waiting for the other
351    /// device to scan it.
352    QrReady(QrCodeData),
353    /// The QR code has been scanned by the other device and this device is
354    /// waiting for the user to put in the check code displayed on the other
355    /// device.
356    QrScanned(CheckCodeSender),
357}
358
359/// A oneshot sender used to send the check code back to the device that
360/// generated the QR code.
361pub type CheckCodeSender = CloneableSender<u8>;
362
363impl CheckCodeSender {
364    /// Send the check code.
365    ///
366    /// Calling this method more than once will result in an error.
367    ///
368    /// # Arguments
369    ///
370    /// * `check_code` - The check code in digits representation.
371    pub async fn send(&self, check_code: u8) -> Result<(), SenderError> {
372        self.send_impl(check_code).await
373    }
374}
375
376/// The internal message of the [`ContinuationMessageSender`] to either continue
377/// the login granting process or to cancel it.
378#[derive(Clone, Copy, Debug)]
379pub(crate) enum ContinuationMessage {
380    Confirm,
381    Cancel,
382}
383
384/// Struct used to let the QR code granting logic know that it can continue with
385/// the process since applications might suspend things while the verification
386/// URI is open.
387#[derive(Clone, Debug)]
388pub struct ContinuationMessageSender(CloneableSender<ContinuationMessage>);
389
390impl ContinuationMessageSender {
391    /// Confirm the continuation of the login granting process.
392    pub async fn confirm(&self) -> Result<(), SenderError> {
393        self.0.send_impl(ContinuationMessage::Confirm).await
394    }
395
396    /// Cancel the login granting process.
397    pub async fn cancel(&self) -> Result<(), SenderError> {
398        self.0.send_impl(ContinuationMessage::Cancel).await
399    }
400}
401
402/// A oneshot sender we are able to clone so we can put it into a
403/// [`SharedObservable`](eyeball::SharedObservable).
404#[derive(Clone, Debug)]
405pub struct CloneableSender<T> {
406    inner: Arc<Mutex<Option<tokio::sync::oneshot::Sender<T>>>>,
407}
408
409impl<T> CloneableSender<T> {
410    pub(crate) fn new(tx: tokio::sync::oneshot::Sender<T>) -> Self {
411        Self { inner: Arc::new(Mutex::new(Some(tx))) }
412    }
413
414    async fn send_impl(&self, message: T) -> Result<(), SenderError> {
415        match self.inner.lock().await.take() {
416            Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend),
417            None => Err(SenderError::AlreadySent),
418        }
419    }
420}
421
422/// Possible errors when calling [`CloneableSender::send`].
423#[derive(Debug, thiserror::Error)]
424pub enum SenderError {
425    /// The message has already been sent.
426    #[error("message already sent.")]
427    AlreadySent,
428    /// The message cannot be sent.
429    #[error("message cannot be sent.")]
430    CannotSend,
431}
432
433#[cfg(all(test, not(target_family = "wasm")))]
434mod tests {
435    use matrix_sdk_test::async_test;
436    use serde_json::json;
437    use wiremock::{
438        Mock, ResponseTemplate,
439        matchers::{method, path},
440    };
441
442    use crate::test_utils::mocks::MatrixMockServer;
443
444    #[async_test]
445    async fn test_msc_4388_rendezvous_server_supported() {
446        const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous";
447
448        let server = MatrixMockServer::new().await;
449        let client = server.client_builder().logged_in_with_oauth().build().await;
450
451        {
452            let _discover_guard = server
453                .server()
454                .register_as_scoped(
455                    Mock::given(method("GET"))
456                        .and(path(URL))
457                        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
458                            "create_available": true,
459                        })))
460                        .expect(1),
461                )
462                .await;
463
464            let supported = client
465                .oauth()
466                .msc_4388_rendezvous_server_supported()
467                .await
468                .expect("We should be able to check if the rendezvous server is supported");
469
470            assert!(supported, "The rendezvous server should be supported");
471        }
472
473        {
474            let _discover_guard = server
475                .server()
476                .register_as_scoped(
477                    Mock::given(method("GET"))
478                        .and(path(URL))
479                        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
480                            "create_available": false,
481                        })))
482                        .expect(1),
483                )
484                .await;
485
486            let supported = client
487                .oauth()
488                .msc_4388_rendezvous_server_supported()
489                .await
490                .expect("We should be able to check if the rendezvous server is supported");
491
492            assert!(
493                !supported,
494                "The rendezvous server should not be supported, because create_available is false"
495            );
496        }
497
498        {
499            let _discover_guard = server
500                .server()
501                .register_as_scoped(
502                    Mock::given(method("GET"))
503                        .and(path(URL))
504                        .respond_with(ResponseTemplate::new(404))
505                        .expect(1),
506                )
507                .await;
508
509            let supported = client
510                .oauth()
511                .msc_4388_rendezvous_server_supported()
512                .await
513                .expect("We should be able to check if the rendezvous server is supported");
514
515            assert!(
516                !supported,
517                "The rendezvous server should not be supported if we receive a 404 response"
518            );
519        }
520
521        {
522            let _discover_guard = server
523                .server()
524                .register_as_scoped(
525                    Mock::given(method("GET"))
526                        .and(path(URL))
527                        .respond_with(ResponseTemplate::new(403))
528                        .expect(1),
529                )
530                .await;
531
532            let supported = client
533                .oauth()
534                .msc_4388_rendezvous_server_supported()
535                .await
536                .expect("We should be able to check if the rendezvous server is supported");
537
538            assert!(
539                !supported,
540                "The rendezvous server should not be supported if we receive a 403 response"
541            );
542        }
543
544        {
545            let _discover_guard = server
546                .server()
547                .register_as_scoped(
548                    Mock::given(method("GET"))
549                        .and(path(URL))
550                        .respond_with(ResponseTemplate::new(500))
551                        .expect(1),
552                )
553                .await;
554
555            client
556                .oauth()
557                .msc_4388_rendezvous_server_supported()
558                .await
559                .expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not");
560        }
561    }
562}