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::{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};
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#[derive(Debug, Error)]
61#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
62pub enum QRCodeLoginError {
63 #[error(transparent)]
66 OAuth(#[from] DeviceAuthorizationOAuthError),
67
68 #[error("The login failed, reason: {reason}")]
70 LoginFailure {
71 reason: LoginFailureReason,
73 homeserver: Option<Url>,
75 },
76
77 #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
79 UnexpectedMessage {
80 expected: &'static str,
82 received: Box<QrAuthMessage>,
84 },
85
86 #[error(transparent)]
88 SecureChannel(SecureChannelError),
89
90 #[error("The rendezvous session was not found and might have expired")]
92 NotFound,
93
94 #[error(transparent)]
96 CrossProcessRefreshLock(#[from] CrossProcessRefreshLockError),
97
98 #[error(transparent)]
102 UserIdDiscovery(HttpError),
103
104 #[error(transparent)]
107 SessionTokens(crate::Error),
108
109 #[error(transparent)]
111 DeviceKeyUpload(crate::Error),
112
113 #[error(transparent)]
116 SecretImport(#[from] SecretImportError),
117
118 #[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#[derive(Debug, Error)]
141pub enum QRCodeGrantLoginError {
142 #[error("Secrets backup not set up")]
144 MissingSecretsBackup(Option<SecretsBundleExportError>),
145
146 #[error("The check code was incorrect")]
148 InvalidCheckCode,
149
150 #[error("The rendezvous session was not found and might have expired")]
152 NotFound,
153
154 #[error("Auth handshake error: {0}")]
156 Unknown(String),
157
158 #[error("Unsupported protocol: {0}")]
160 UnsupportedProtocol(LoginProtocolType),
161
162 #[error("The requested device ID is already in use")]
164 DeviceIDAlreadyInUse,
165
166 #[error("The requested device was not returned by the homeserver")]
168 DeviceNotFound,
169
170 #[error(transparent)]
172 SecureChannel(SecureChannelError),
173
174 #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
176 UnexpectedMessage {
177 expected: &'static str,
179 received: Box<QrAuthMessage>,
181 },
182
183 #[error("The login failed, reason: {reason}")]
185 LoginFailure {
186 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#[derive(Debug, Error)]
215pub enum DeviceAuthorizationOAuthError {
216 #[error(transparent)]
219 OAuth(#[from] crate::authentication::oauth::OAuthError),
220
221 #[error("OAuth 2.0 server doesn't support the device authorization grant")]
223 NoDeviceAuthorizationEndpoint,
224
225 #[error(transparent)]
228 DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
229
230 #[error(transparent)]
233 RequestToken(
234 #[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
235 ),
236}
237
238impl DeviceAuthorizationOAuthError {
239 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#[derive(Debug, Error)]
252pub enum SecureChannelError {
253 #[error(transparent)]
256 Utf8(#[from] std::str::Utf8Error),
257
258 #[error(transparent)]
260 Ecies(#[from] EciesError),
261
262 #[error(transparent)]
264 MessageDecode(#[from] MessageDecodeError),
265
266 #[error(transparent)]
268 Json(#[from] serde_json::Error),
269
270 #[error(
273 "The secure channel setup has received an unexpected message, expected: {expected}, got {received}"
274 )]
275 SecureChannelMessage {
276 expected: &'static str,
278 received: String,
280 },
281
282 #[error("The secure channel could not have been established, the check code was invalid")]
285 InvalidCheckCode,
286
287 #[error("Error in the rendezvous channel: {0:?}")]
289 RendezvousChannel(#[from] HttpError),
290
291 #[error(
294 "The secure channel could not have been established, \
295 the two devices have the same login intent"
296 )]
297 InvalidIntent,
298
299 #[error(
302 "The secure channel could not have been established, \
303 the check code cannot be received"
304 )]
305 CannotReceiveCheckCode,
306
307 #[error("The QR code specifies an unsupported protocol version")]
308 UnsupportedQrCodeType,
310}
311
312#[derive(Clone, Debug)]
320pub struct QrProgress {
321 pub check_code: u8,
323}
324
325#[derive(Clone, Debug)]
334pub enum GeneratedQrProgress {
335 QrReady(QrCodeData),
338 QrScanned(CheckCodeSender),
342}
343
344pub type CheckCodeSender = CloneableSender<u8>;
347
348impl CheckCodeSender {
349 pub async fn send(&self, check_code: u8) -> Result<(), SenderError> {
357 self.send_impl(check_code).await
358 }
359}
360
361#[derive(Clone, Copy, Debug)]
364pub(crate) enum ContinuationMessage {
365 Confirm,
366 Cancel,
367}
368
369#[derive(Clone, Debug)]
373pub struct ContinuationMessageSender(CloneableSender<ContinuationMessage>);
374
375impl ContinuationMessageSender {
376 pub async fn confirm(&self) -> Result<(), SenderError> {
378 self.0.send_impl(ContinuationMessage::Confirm).await
379 }
380
381 pub async fn cancel(&self) -> Result<(), SenderError> {
383 self.0.send_impl(ContinuationMessage::Cancel).await
384 }
385}
386
387#[derive(Clone, Debug)]
390pub struct CloneableSender<T> {
391 inner: Arc<Mutex<Option<tokio::sync::oneshot::Sender<T>>>>,
392}
393
394impl<T> CloneableSender<T> {
395 pub(crate) fn new(tx: tokio::sync::oneshot::Sender<T>) -> Self {
396 Self { inner: Arc::new(Mutex::new(Some(tx))) }
397 }
398
399 async fn send_impl(&self, message: T) -> Result<(), SenderError> {
400 match self.inner.lock().await.take() {
401 Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend),
402 None => Err(SenderError::AlreadySent),
403 }
404 }
405}
406
407#[derive(Debug, thiserror::Error)]
409pub enum SenderError {
410 #[error("message already sent.")]
412 AlreadySent,
413 #[error("message cannot be sent.")]
415 CannotSend,
416}
417
418#[cfg(all(test, not(target_family = "wasm")))]
419mod tests {
420 use matrix_sdk_test::async_test;
421 use serde_json::json;
422 use wiremock::{
423 Mock, ResponseTemplate,
424 matchers::{method, path},
425 };
426
427 use crate::test_utils::mocks::MatrixMockServer;
428
429 #[async_test]
430 async fn test_msc_4388_rendezvous_server_supported() {
431 const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous";
432
433 let server = MatrixMockServer::new().await;
434 let client = server.client_builder().logged_in_with_oauth().build().await;
435
436 {
437 let _discover_guard = server
438 .server()
439 .register_as_scoped(
440 Mock::given(method("GET"))
441 .and(path(URL))
442 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
443 "create_available": true,
444 })))
445 .expect(1),
446 )
447 .await;
448
449 let supported = client
450 .oauth()
451 .msc_4388_rendezvous_server_supported()
452 .await
453 .expect("We should be able to check if the rendezvous server is supported");
454
455 assert!(supported, "The rendezvous server should be supported");
456 }
457
458 {
459 let _discover_guard = server
460 .server()
461 .register_as_scoped(
462 Mock::given(method("GET"))
463 .and(path(URL))
464 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
465 "create_available": false,
466 })))
467 .expect(1),
468 )
469 .await;
470
471 let supported = client
472 .oauth()
473 .msc_4388_rendezvous_server_supported()
474 .await
475 .expect("We should be able to check if the rendezvous server is supported");
476
477 assert!(
478 !supported,
479 "The rendezvous server should not be supported, because create_available is false"
480 );
481 }
482
483 {
484 let _discover_guard = server
485 .server()
486 .register_as_scoped(
487 Mock::given(method("GET"))
488 .and(path(URL))
489 .respond_with(ResponseTemplate::new(404))
490 .expect(1),
491 )
492 .await;
493
494 let supported = client
495 .oauth()
496 .msc_4388_rendezvous_server_supported()
497 .await
498 .expect("We should be able to check if the rendezvous server is supported");
499
500 assert!(
501 !supported,
502 "The rendezvous server should not be supported if we receive a 404 response"
503 );
504 }
505
506 {
507 let _discover_guard = server
508 .server()
509 .register_as_scoped(
510 Mock::given(method("GET"))
511 .and(path(URL))
512 .respond_with(ResponseTemplate::new(403))
513 .expect(1),
514 )
515 .await;
516
517 let supported = client
518 .oauth()
519 .msc_4388_rendezvous_server_supported()
520 .await
521 .expect("We should be able to check if the rendezvous server is supported");
522
523 assert!(
524 !supported,
525 "The rendezvous server should not be supported if we receive a 403 response"
526 );
527 }
528
529 {
530 let _discover_guard = server
531 .server()
532 .register_as_scoped(
533 Mock::given(method("GET"))
534 .and(path(URL))
535 .respond_with(ResponseTemplate::new(500))
536 .expect(1),
537 )
538 .await;
539
540 client
541 .oauth()
542 .msc_4388_rendezvous_server_supported()
543 .await
544 .expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not");
545 }
546 }
547}