matrix_sdk/authentication/oauth/qrcode/
mod.rs1use 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::{
31 SecretImportError,
32 store::SecretsBundleExportError,
33 types::qr_login::{InvalidLengthError, QrCodeCreationError},
34};
35pub use oauth2::{
36 ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError,
37 RequestTokenError, StandardErrorResponse,
38 basic::{BasicErrorResponse, BasicRequestTokenError},
39};
40use ruma::api::error::ErrorKind;
41use thiserror::Error;
42use tokio::sync::Mutex;
43pub use vodozemac::{
44 ecies::{Error as EciesError, MessageDecodeError as EciesMessageDecodeError},
45 hpke::{Error as HpkeError, MessageDecodeError as HpkeMessageDecodeError},
46};
47
48mod grant;
49mod login;
50mod messages;
51mod rendezvous_channel;
52mod secure_channel;
53
54pub use self::{
55 grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode},
56 login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode},
57 messages::{
58 AuthorizationGrant, LoginFailureReason, LoginProtocolData, LoginProtocolType,
59 QrAuthMessage, UnknownLoginProtocol,
60 },
61};
62use super::CrossProcessRefreshLockError;
63#[cfg(doc)]
64use super::OAuth;
65use crate::HttpError;
66
67#[derive(Debug, Error)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
71pub enum QRCodeLoginError {
72 #[error(transparent)]
75 OAuth(#[from] DeviceAuthorizationOAuthError),
76
77 #[error("The login failed, reason: {reason}")]
79 LoginFailure {
80 reason: LoginFailureReason,
82 homeserver: Option<String>,
85 },
86
87 #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
89 UnexpectedMessage {
90 expected: &'static str,
92 received: Box<QrAuthMessage>,
94 },
95
96 #[error(transparent)]
98 SecureChannel(SecureChannelError),
99
100 #[error("The rendezvous session was not found and might have expired")]
102 NotFound,
103
104 #[error(transparent)]
106 CrossProcessRefreshLock(#[from] CrossProcessRefreshLockError),
107
108 #[error(transparent)]
112 UserIdDiscovery(HttpError),
113
114 #[error(transparent)]
117 SessionTokens(crate::Error),
118
119 #[error(transparent)]
121 DeviceKeyUpload(crate::Error),
122
123 #[error(transparent)]
126 SecretImport(#[from] SecretImportError),
127
128 #[error(transparent)]
131 ServerReset(crate::Error),
132}
133
134impl From<SecureChannelError> for QRCodeLoginError {
135 fn from(e: SecureChannelError) -> Self {
136 match e {
137 SecureChannelError::RendezvousChannel(ref http_error) => {
138 if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
139 return Self::NotFound;
140 }
141 Self::SecureChannel(e)
142 }
143 e => Self::SecureChannel(e),
144 }
145 }
146}
147
148#[derive(Debug, Error)]
151pub enum QRCodeGrantLoginError {
152 #[error("Secrets backup not set up")]
154 MissingSecretsBackup(Option<SecretsBundleExportError>),
155
156 #[error("The check code was incorrect")]
158 InvalidCheckCode,
159
160 #[error("The rendezvous session was not found and might have expired")]
162 NotFound,
163
164 #[error("Auth handshake error: {0}")]
166 Unknown(String),
167
168 #[error("Unsupported protocol: {0}")]
170 UnsupportedProtocol(LoginProtocolType),
171
172 #[error("The homeserver doesn't support any of the login protocols we can offer")]
175 NoSupportedProtocols,
176
177 #[error("The requested device ID is already in use")]
179 DeviceIDAlreadyInUse,
180
181 #[error("The requested device was not returned by the homeserver")]
183 DeviceNotFound,
184
185 #[error(transparent)]
187 SecureChannel(SecureChannelError),
188
189 #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
191 UnexpectedMessage {
192 expected: &'static str,
194 received: Box<QrAuthMessage>,
196 },
197
198 #[error("The login failed, reason: {reason}")]
200 LoginFailure {
201 reason: LoginFailureReason,
203 },
204}
205
206impl From<SecureChannelError> for QRCodeGrantLoginError {
207 fn from(e: SecureChannelError) -> Self {
208 match e {
209 SecureChannelError::RendezvousChannel(ref http_error) => {
210 if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
211 return Self::NotFound;
212 }
213 Self::SecureChannel(e)
214 }
215 SecureChannelError::InvalidCheckCode => Self::InvalidCheckCode,
216 e => Self::SecureChannel(e),
217 }
218 }
219}
220
221impl From<SecretsBundleExportError> for QRCodeGrantLoginError {
222 fn from(e: SecretsBundleExportError) -> Self {
223 Self::MissingSecretsBackup(Some(e))
224 }
225}
226
227#[derive(Debug, Error)]
230pub enum DeviceAuthorizationOAuthError {
231 #[error(transparent)]
234 OAuth(#[from] crate::authentication::oauth::OAuthError),
235
236 #[error("OAuth 2.0 server doesn't support the device authorization grant")]
238 NoDeviceAuthorizationEndpoint,
239
240 #[error(transparent)]
243 DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
244
245 #[error(transparent)]
248 RequestToken(
249 #[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
250 ),
251}
252
253impl DeviceAuthorizationOAuthError {
254 pub fn as_request_token_error(&self) -> Option<&DeviceCodeErrorResponseType> {
257 let error = as_variant!(self, DeviceAuthorizationOAuthError::RequestToken)?;
258 let request_token_error = as_variant!(error, RequestTokenError::ServerResponse)?;
259
260 Some(request_token_error.error())
261 }
262}
263
264#[derive(Debug, Error)]
267pub enum MessageDecodeError {
268 #[error(transparent)]
270 Ecies(#[from] EciesMessageDecodeError),
271
272 #[error(transparent)]
274 Hpke(#[from] HpkeMessageDecodeError),
275
276 #[error(transparent)]
279 Utf8(#[from] std::str::Utf8Error),
280
281 #[error(transparent)]
283 Json(#[from] serde_json::Error),
284
285 #[error("The sequence token is too long")]
289 TooLongSequenceToken(InvalidLengthError),
290
291 #[error("The base URL of the homeserver is too long")]
294 TooLongBaseUrl(InvalidLengthError),
295
296 #[error("The rendezvous ID is too long")]
299 TooLongRendezvousId(InvalidLengthError),
300}
301
302#[derive(Debug, Error)]
304pub enum DecryptionError {
305 #[error(transparent)]
307 Ecies(#[from] EciesError),
308 #[error(transparent)]
310 Hpke(#[from] HpkeError),
311}
312
313#[derive(Debug, Error)]
316pub enum SecureChannelError {
317 #[error(transparent)]
319 Decryption(#[from] DecryptionError),
320
321 #[error(transparent)]
323 MessageDecode(#[from] MessageDecodeError),
324
325 #[error(
328 "The secure channel setup has received an unexpected message, expected: {expected}, got {received}"
329 )]
330 SecureChannelMessage {
331 expected: &'static str,
333 received: String,
335 },
336
337 #[error("The secure channel could not have been established, the check code was invalid")]
340 InvalidCheckCode,
341
342 #[error("Error in the rendezvous channel: {0:?}")]
344 RendezvousChannel(#[from] HttpError),
345
346 #[error(
349 "The secure channel could not have been established, \
350 the two devices have the same login intent"
351 )]
352 InvalidIntent,
353
354 #[error(
357 "The secure channel could not have been established, \
358 the check code cannot be received"
359 )]
360 CannotReceiveCheckCode,
361
362 #[error("The QR code specifies an unsupported protocol version")]
364 UnsupportedQrCodeType,
365
366 #[error(transparent)]
368 QrCodeCreationError(#[from] QrCodeCreationError),
369}
370
371#[derive(Clone, Debug)]
379pub struct QrProgress {
380 pub check_code: u8,
382}
383
384#[derive(Clone, Debug)]
393pub enum GeneratedQrProgress {
394 QrReady(QrCodeData),
397 QrScanned(CheckCodeSender),
401}
402
403pub type CheckCodeSender = CloneableSender<u8>;
406
407impl CheckCodeSender {
408 pub async fn send(&self, check_code: u8) -> Result<(), SenderError> {
416 self.send_impl(check_code).await
417 }
418}
419
420#[derive(Clone, Copy, Debug)]
423pub(crate) enum ContinuationMessage {
424 Confirm,
425 Cancel,
426}
427
428#[derive(Clone, Debug)]
432pub struct ContinuationMessageSender(CloneableSender<ContinuationMessage>);
433
434impl ContinuationMessageSender {
435 pub async fn confirm(&self) -> Result<(), SenderError> {
437 self.0.send_impl(ContinuationMessage::Confirm).await
438 }
439
440 pub async fn cancel(&self) -> Result<(), SenderError> {
442 self.0.send_impl(ContinuationMessage::Cancel).await
443 }
444}
445
446#[derive(Clone, Debug)]
449pub struct CloneableSender<T> {
450 inner: Arc<Mutex<Option<tokio::sync::oneshot::Sender<T>>>>,
451}
452
453impl<T> CloneableSender<T> {
454 pub(crate) fn new(tx: tokio::sync::oneshot::Sender<T>) -> Self {
455 Self { inner: Arc::new(Mutex::new(Some(tx))) }
456 }
457
458 async fn send_impl(&self, message: T) -> Result<(), SenderError> {
459 match self.inner.lock().await.take() {
460 Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend),
461 None => Err(SenderError::AlreadySent),
462 }
463 }
464}
465
466#[derive(Debug, thiserror::Error)]
468pub enum SenderError {
469 #[error("message already sent.")]
471 AlreadySent,
472 #[error("message cannot be sent.")]
474 CannotSend,
475}
476
477#[cfg(all(test, not(target_family = "wasm"), feature = "unstable-msc4388"))]
478mod tests {
479 use matrix_sdk_test::async_test;
480 use serde_json::json;
481 use wiremock::{
482 Mock, ResponseTemplate,
483 matchers::{method, path},
484 };
485
486 use crate::test_utils::mocks::MatrixMockServer;
487
488 #[async_test]
489 async fn test_msc_4388_rendezvous_server_supported() {
490 const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous";
491
492 let server = MatrixMockServer::new().await;
493 let client = server.client_builder().logged_in_with_oauth().build().await;
494
495 {
496 let _discover_guard = server
497 .server()
498 .register_as_scoped(
499 Mock::given(method("GET"))
500 .and(path(URL))
501 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
502 "create_available": true,
503 })))
504 .expect(1),
505 )
506 .await;
507
508 let supported = client
509 .oauth()
510 .msc_4388_rendezvous_server_supported()
511 .await
512 .expect("We should be able to check if the rendezvous server is supported");
513
514 assert!(supported, "The rendezvous server should be supported");
515 }
516
517 {
518 let _discover_guard = server
519 .server()
520 .register_as_scoped(
521 Mock::given(method("GET"))
522 .and(path(URL))
523 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
524 "create_available": false,
525 })))
526 .expect(1),
527 )
528 .await;
529
530 let supported = client
531 .oauth()
532 .msc_4388_rendezvous_server_supported()
533 .await
534 .expect("We should be able to check if the rendezvous server is supported");
535
536 assert!(
537 !supported,
538 "The rendezvous server should not be supported, because create_available is false"
539 );
540 }
541
542 {
543 let _discover_guard = server
544 .server()
545 .register_as_scoped(
546 Mock::given(method("GET"))
547 .and(path(URL))
548 .respond_with(ResponseTemplate::new(404))
549 .expect(1),
550 )
551 .await;
552
553 let supported = client
554 .oauth()
555 .msc_4388_rendezvous_server_supported()
556 .await
557 .expect("We should be able to check if the rendezvous server is supported");
558
559 assert!(
560 !supported,
561 "The rendezvous server should not be supported if we receive a 404 response"
562 );
563 }
564
565 {
566 let _discover_guard = server
567 .server()
568 .register_as_scoped(
569 Mock::given(method("GET"))
570 .and(path(URL))
571 .respond_with(ResponseTemplate::new(403))
572 .expect(1),
573 )
574 .await;
575
576 let supported = client
577 .oauth()
578 .msc_4388_rendezvous_server_supported()
579 .await
580 .expect("We should be able to check if the rendezvous server is supported");
581
582 assert!(
583 !supported,
584 "The rendezvous server should not be supported if we receive a 403 response"
585 );
586 }
587
588 {
589 let _discover_guard = server
590 .server()
591 .register_as_scoped(
592 Mock::given(method("GET"))
593 .and(path(URL))
594 .respond_with(ResponseTemplate::new(500))
595 .expect(1),
596 )
597 .await;
598
599 client
600 .oauth()
601 .msc_4388_rendezvous_server_supported()
602 .await
603 .expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not");
604 }
605 }
606}