Skip to main content

matrix_sdk/authentication/oauth/qrcode/
messages.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
15use matrix_sdk_base::crypto::types::SecretsBundle;
16use matrix_sdk_common::deserialized_responses::PrivOwnedStr;
17use oauth2::{
18    EndUserVerificationUrl, StandardDeviceAuthorizationResponse, VerificationUriComplete,
19};
20use ruma::serde::{JsonObject, StringEnum};
21use serde::{Deserialize, Serialize};
22use url::Url;
23use vodozemac::Curve25519PublicKey;
24
25#[cfg(doc)]
26use super::QRCodeLoginError::SecureChannel;
27use super::secure_channel::ChannelVariant;
28
29/// Messages that will be exchanged over the [`SecureChannel`] to log in a new
30/// device using a QR code.
31#[derive(Debug, Serialize, Deserialize)]
32#[serde(tag = "type")]
33pub enum QrAuthMessage {
34    /// Message declaring the available protocols for sign in. Sent by the
35    /// existing device.
36    #[serde(rename = "m.login.protocols")]
37    LoginProtocols(LoginProtocolsMessage),
38
39    /// Message declaring which protocols from the previous `m.login.protocols`
40    /// message the new device has picked. Sent by the new device.
41    #[serde(rename = "m.login.protocol")]
42    LoginProtocol {
43        /// The protocol the new device has picked, along with the data specific
44        /// to that protocol.
45        #[serde(flatten)]
46        protocol: LoginProtocolData,
47        /// The device ID the new device will be using.
48        device_id: String,
49    },
50
51    /// Message declaring that the protocol in the previous `m.login.protocol`
52    /// message was accepted. Sent by the existing device.
53    #[serde(rename = "m.login.protocol_accepted")]
54    LoginProtocolAccepted,
55
56    /// Message that informs the existing device that it successfully obtained
57    /// an access token from the OAuth 2.0 server. Sent by the new device.
58    #[serde(rename = "m.login.success")]
59    LoginSuccess,
60
61    /// Message that informs the existing device that the OAuth 2.0 server has
62    /// declined to give us an access token, i.e. because the user declined the
63    /// log in. Sent by the new device.
64    #[serde(rename = "m.login.declined")]
65    LoginDeclined,
66
67    /// Message signaling that a failure happened during the login. Can be sent
68    /// by either device.
69    #[serde(rename = "m.login.failure")]
70    LoginFailure {
71        /// The claimed reason for the login failure.
72        reason: LoginFailureReason,
73        /// The homeserver the new device should use, optionally sent by the
74        /// existing device so the user doesn't have to type it in.
75        ///
76        /// The MSC defines this as a server name, but older implementations
77        /// send a homeserver URL, so both are accepted as is. Either form can
78        /// be passed to [`ClientBuilder::server_name_or_homeserver_url()`].
79        ///
80        /// [`ClientBuilder::server_name_or_homeserver_url()`]: crate::ClientBuilder::server_name_or_homeserver_url
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        homeserver: Option<String>,
83    },
84
85    /// Message containing end-to-end encryption related secrets, the new device
86    /// can use these secrets to mark itself as verified, connect to a room key
87    /// backup, and login other devices via a QR login. Sent by the existing
88    /// device.
89    #[serde(rename = "m.login.secrets")]
90    LoginSecrets(SecretsBundle),
91}
92
93/// Message declaring the available protocols for sign in. Sent by the
94/// existing device.
95///
96/// Supports both the MSC4108 variant of the m.login.protocols message as well
97/// as the MSC4388 variant.
98#[derive(Debug, Serialize, Deserialize)]
99#[serde(untagged)]
100pub enum LoginProtocolsMessage {
101    Msc4388 {
102        /// The login protocols the existing device supports.
103        protocols: Vec<LoginProtocolType>,
104        /// The homeserver we're going to log in to.
105        base_url: Url,
106    },
107    Msc4108 {
108        /// The login protocols the existing device supports.
109        protocols: Vec<LoginProtocolType>,
110        /// The homeserver we're going to log in to.
111        ///
112        /// Note: this doesn't match the MSC which says that it is a server name
113        /// not a full URL. This is an implementation mistake in the first
114        /// version of the QR code login support.
115        homeserver: Url,
116    },
117}
118
119impl QrAuthMessage {
120    /// Create a new [`QrAuthMessage::LoginProtocol`] message with the
121    /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol type, using the
122    /// layout of the given channel variant.
123    pub(super) fn authorization_grant_login_protocol(
124        device_authorization_grant: AuthorizationGrant,
125        device_id: Curve25519PublicKey,
126        channel_variant: ChannelVariant,
127    ) -> QrAuthMessage {
128        QrAuthMessage::LoginProtocol {
129            device_id: device_id.to_base64(),
130            protocol: LoginProtocolData::device_authorization_grant(
131                device_authorization_grant,
132                channel_variant,
133            ),
134        }
135    }
136}
137
138/// The login protocol the new device has picked in a
139/// [`QrAuthMessage::LoginProtocol`] message, along with the data specific to
140/// that protocol.
141///
142/// On the wire, the protocol name is in the `protocol` field, and the protocol
143/// specific fields sit next to it, like other Matrix structures keyed on a
144/// discriminator field.
145///
146/// For the `device_authorization_grant` protocol, clients using the MSC4108
147/// variant of the secure channel instead nest the data in a field named after
148/// the protocol, i.e. `"protocol": "device_authorization_grant"` comes with a
149/// `"device_authorization_grant": { ... }` field. Which layout is used depends
150/// on the variant of the secure channel.
151///
152/// Like other known message types, a known protocol only keeps the fields it
153/// knows about, while an [`UnknownLoginProtocol`] keeps all the fields, as we
154/// don't know which ones are relevant.
155#[derive(Debug, Clone, Deserialize)]
156#[serde(try_from = "LoginProtocolDataHelper")]
157#[non_exhaustive]
158pub enum LoginProtocolData {
159    /// The `device_authorization_grant` login protocol, as sent over the
160    /// MSC4108 variant of the secure channel, with the data nested in a
161    /// `device_authorization_grant` field.
162    ///
163    /// Contains the device authorization grant the OAuth 2.0 server has given
164    /// to the new device, with the URL the existing device should use to
165    /// confirm the log in.
166    Msc4108DeviceAuthorizationGrant(AuthorizationGrant),
167    /// The `device_authorization_grant` login protocol, as sent over the
168    /// MSC4388 variant of the secure channel, with the data next to the
169    /// `protocol` field.
170    ///
171    /// Contains the device authorization grant the OAuth 2.0 server has given
172    /// to the new device, with the URL the existing device should use to
173    /// confirm the log in.
174    Msc4388DeviceAuthorizationGrant(AuthorizationGrant),
175    /// An unknown and unsupported login protocol.
176    Unknown(UnknownLoginProtocol),
177}
178
179impl LoginProtocolData {
180    /// Create a new [`LoginProtocolData`] from the given login protocol and its
181    /// data.
182    ///
183    /// The `data` contains the fields of the [`QrAuthMessage::LoginProtocol`]
184    /// message other than `type`, `protocol` and `device_id`. If the protocol
185    /// is a known one, the data is deserialized into the corresponding
186    /// variant and only the fields that protocol knows about are kept.
187    /// Otherwise a [`LoginProtocolData::Unknown`] is created, which keeps all
188    /// the fields verbatim.
189    ///
190    /// For the [`LoginProtocolType::DeviceAuthorizationGrant`] protocol, the
191    /// layout is detected from the data: if it contains a
192    /// `device_authorization_grant` field, a
193    /// [`LoginProtocolData::Msc4108DeviceAuthorizationGrant`] is created,
194    /// otherwise a [`LoginProtocolData::Msc4388DeviceAuthorizationGrant`].
195    ///
196    /// Returns an error if the protocol is a known one and the data is not
197    /// valid for it, or if the protocol is unknown and the data contains one
198    /// of the `type`, `protocol` or `device_id` fields, as they would clash
199    /// with the fields of the message when serialized.
200    pub fn new(protocol: LoginProtocolType, data: JsonObject) -> Result<Self, serde_json::Error> {
201        LoginProtocolDataHelper { protocol, other: data }.try_into()
202    }
203
204    /// Create a new [`LoginProtocolData`] for the
205    /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol, using the
206    /// layout of the given channel variant.
207    pub(super) fn device_authorization_grant(
208        device_authorization_grant: AuthorizationGrant,
209        channel_variant: ChannelVariant,
210    ) -> Self {
211        match channel_variant {
212            ChannelVariant::Msc4108 => {
213                Self::Msc4108DeviceAuthorizationGrant(device_authorization_grant)
214            }
215            #[cfg(feature = "unstable-msc4388")]
216            ChannelVariant::Msc4388 => {
217                Self::Msc4388DeviceAuthorizationGrant(device_authorization_grant)
218            }
219        }
220    }
221
222    /// Get the login protocol.
223    pub fn protocol(&self) -> LoginProtocolType {
224        match self {
225            Self::Msc4108DeviceAuthorizationGrant(_) | Self::Msc4388DeviceAuthorizationGrant(_) => {
226                LoginProtocolType::DeviceAuthorizationGrant
227            }
228            Self::Unknown(c) => c.protocol.clone(),
229        }
230    }
231}
232
233/// An unknown and unsupported login protocol of a
234/// [`QrAuthMessage::LoginProtocol`] message.
235#[derive(Debug, Clone)]
236pub struct UnknownLoginProtocol {
237    /// The name of the unknown login protocol.
238    protocol: LoginProtocolType,
239
240    /// The other data of the unknown login protocol.
241    other: JsonObject,
242}
243
244impl UnknownLoginProtocol {
245    /// Get the name of the unknown login protocol.
246    pub fn protocol(&self) -> &LoginProtocolType {
247        &self.protocol
248    }
249
250    /// Get the data of the unknown login protocol, i.e. the fields of the
251    /// [`QrAuthMessage::LoginProtocol`] message other than `type`, `protocol`
252    /// and `device_id`.
253    pub fn data(&self) -> &JsonObject {
254        &self.other
255    }
256}
257
258#[derive(Deserialize)]
259struct LoginProtocolDataHelper {
260    protocol: LoginProtocolType,
261    #[serde(flatten)]
262    other: JsonObject,
263}
264
265#[derive(Serialize)]
266struct LoginProtocolDataSerHelper<'a, T: Serialize> {
267    protocol: LoginProtocolType,
268    #[serde(flatten)]
269    data: &'a T,
270}
271
272#[derive(Serialize)]
273struct Msc4108DeviceAuthorizationGrantSerHelper<'a> {
274    device_authorization_grant: &'a AuthorizationGrant,
275}
276
277impl Serialize for LoginProtocolData {
278    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
279    where
280        S: serde::Serializer,
281    {
282        let protocol = self.protocol();
283
284        match self {
285            Self::Msc4108DeviceAuthorizationGrant(device_authorization_grant) => {
286                LoginProtocolDataSerHelper {
287                    protocol,
288                    data: &Msc4108DeviceAuthorizationGrantSerHelper { device_authorization_grant },
289                }
290                .serialize(serializer)
291            }
292            Self::Msc4388DeviceAuthorizationGrant(device_authorization_grant) => {
293                LoginProtocolDataSerHelper { protocol, data: device_authorization_grant }
294                    .serialize(serializer)
295            }
296            Self::Unknown(c) => {
297                LoginProtocolDataSerHelper { protocol, data: &c.other }.serialize(serializer)
298            }
299        }
300    }
301}
302
303impl TryFrom<LoginProtocolDataHelper> for LoginProtocolData {
304    type Error = serde_json::Error;
305
306    fn try_from(mut value: LoginProtocolDataHelper) -> Result<Self, Self::Error> {
307        Ok(match value.protocol {
308            LoginProtocolType::DeviceAuthorizationGrant => {
309                let invalid_data = |error: serde_json::Error| {
310                    serde::de::Error::custom(format_args!(
311                        "invalid device_authorization_grant: {error}"
312                    ))
313                };
314
315                // The MSC4108 variant nests the data in a field named after the
316                // protocol, while the MSC4388 variant puts it next to the
317                // `protocol` field.
318                if let Some(device_authorization_grant) =
319                    value.other.remove("device_authorization_grant")
320                {
321                    Self::Msc4108DeviceAuthorizationGrant(
322                        serde_json::from_value(device_authorization_grant).map_err(invalid_data)?,
323                    )
324                } else {
325                    Self::Msc4388DeviceAuthorizationGrant(
326                        serde_json::from_value(value.other.into()).map_err(invalid_data)?,
327                    )
328                }
329            }
330            _ => {
331                // The fields of an unknown protocol are serialized next to the
332                // fields of the message, so they must not clash with them.
333                const RESERVED_FIELDS: &[&str] = &["type", "protocol", "device_id"];
334
335                if let Some(field) =
336                    RESERVED_FIELDS.iter().find(|field| value.other.contains_key(**field))
337                {
338                    return Err(serde::de::Error::custom(format_args!(
339                        "the `{field}` field is reserved and can't be part of the login \
340                         protocol data"
341                    )));
342                }
343
344                Self::Unknown(UnknownLoginProtocol { protocol: value.protocol, other: value.other })
345            }
346        })
347    }
348}
349
350impl From<&StandardDeviceAuthorizationResponse> for AuthorizationGrant {
351    fn from(value: &StandardDeviceAuthorizationResponse) -> Self {
352        Self {
353            verification_uri: value.verification_uri().clone(),
354            verification_uri_complete: value.verification_uri_complete().cloned(),
355        }
356    }
357}
358
359/// Data for the device authorization grant login protocol.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct AuthorizationGrant {
362    /// The verification URL the user should open to log the new device in.
363    pub verification_uri: EndUserVerificationUrl,
364
365    /// The verification URL, with the user code pre-filled, which the user
366    /// should open to log the new device in. If this URL is available, the user
367    /// should be presented with it instead of the one in the
368    /// [`AuthorizationGrant::verification_uri`] field.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub verification_uri_complete: Option<VerificationUriComplete>,
371}
372
373/// Reasons why the login might have failed.
374#[derive(Clone, StringEnum)]
375#[ruma_enum(rename_all = "snake_case")]
376pub enum LoginFailureReason {
377    /// The Device Authorization Grant expired.
378    AuthorizationExpired,
379    /// The device ID specified by the new device already exists in the
380    /// homeserver provided device list.
381    DeviceAlreadyExists,
382    /// The new device is not present in the device list as returned by the
383    /// homeserver.
384    DeviceNotFound,
385    /// Sent by either device to indicate that they received a message of a type
386    /// that they weren't expecting.
387    UnexpectedMessageReceived,
388    /// Sent by a device where no suitable protocol is available or the
389    /// requested protocol requested is not supported.
390    UnsupportedProtocol,
391    /// Sent by either new or existing device to indicate that the user has
392    /// cancelled the login.
393    UserCancelled,
394    #[doc(hidden)]
395    _Custom(PrivOwnedStr),
396}
397
398/// Enum containing known login protocol types.
399#[derive(Clone, StringEnum)]
400#[ruma_enum(rename_all = "snake_case")]
401pub enum LoginProtocolType {
402    /// The `device_authorization_grant` login protocol type.
403    DeviceAuthorizationGrant,
404    #[doc(hidden)]
405    _Custom(PrivOwnedStr),
406}
407
408#[cfg(test)]
409mod test {
410    use matrix_sdk_base::crypto::types::BackupSecrets;
411    use serde_json::json;
412    use similar_asserts::assert_eq;
413    use strass::assert_let;
414
415    use super::*;
416
417    #[test]
418    fn test_protocols_serialization_msc_4108() {
419        const HOMESERVER: &str = "https://matrix-client.matrix.org/";
420
421        let json = json!({
422            "type": "m.login.protocols",
423            "protocols": ["device_authorization_grant"],
424            "homeserver": HOMESERVER,
425
426        });
427
428        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
429        assert_let!(
430            QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4108 {
431                protocols,
432                homeserver
433            }) = &message
434        );
435        assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant));
436        assert_eq!(homeserver.as_str(), HOMESERVER);
437
438        let serialized = serde_json::to_value(&message).unwrap();
439        assert_eq!(json, serialized);
440    }
441
442    #[test]
443    fn test_protocols_serialization_msc_4388() {
444        const HOMESERVER: &str = "https://matrix-client.matrix.org/";
445
446        let json = json!({
447            "type": "m.login.protocols",
448            "protocols": ["device_authorization_grant"],
449            "base_url": HOMESERVER,
450
451        });
452
453        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
454        assert_let!(
455            QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4388 { protocols, base_url }) =
456                &message
457        );
458        assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant));
459        assert_eq!(base_url.as_str(), HOMESERVER);
460
461        let serialized = serde_json::to_value(&message).unwrap();
462        assert_eq!(json, serialized);
463    }
464
465    #[test]
466    fn test_protocol_serialization_msc_4108() {
467        let json = json!({
468            "type": "m.login.protocol",
469            "protocol": "device_authorization_grant",
470            "device_authorization_grant": {
471                "verification_uri_complete": "https://id.matrix.org/device/abcde",
472                "verification_uri": "https://id.matrix.org/device/abcde?code=ABCDE"
473            },
474            "device_id": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
475        });
476
477        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
478        assert_let!(
479            QrAuthMessage::LoginProtocol {
480                protocol: LoginProtocolData::Msc4108DeviceAuthorizationGrant(
481                    device_authorization_grant
482                ),
483                device_id,
484            } = &message
485        );
486        assert_eq!(
487            device_authorization_grant.verification_uri.as_str(),
488            "https://id.matrix.org/device/abcde?code=ABCDE"
489        );
490        assert_eq!(device_id, "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4");
491        let serialized = serde_json::to_value(&message).unwrap();
492        assert_eq!(json, serialized);
493    }
494
495    #[test]
496    fn test_protocol_serialization_msc_4388() {
497        let json = json!({
498            "type": "m.login.protocol",
499            "protocol": "device_authorization_grant",
500            "verification_uri_complete": "https://id.matrix.org/device/abcde",
501            "verification_uri": "https://id.matrix.org/device/abcde?code=ABCDE",
502            "device_id": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
503        });
504
505        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
506        assert_let!(
507            QrAuthMessage::LoginProtocol {
508                protocol: LoginProtocolData::Msc4388DeviceAuthorizationGrant(
509                    device_authorization_grant
510                ),
511                device_id,
512            } = &message
513        );
514        assert_eq!(
515            device_authorization_grant.verification_uri.as_str(),
516            "https://id.matrix.org/device/abcde?code=ABCDE"
517        );
518        assert_eq!(device_id, "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4");
519        let serialized = serde_json::to_value(&message).unwrap();
520        assert_eq!(json, serialized);
521    }
522
523    #[test]
524    fn test_protocol_serialization_unknown_protocol() {
525        // A future protocol carries its own fields, and no
526        // `verification_uri` or `device_authorization_grant` field.
527        let json = json!({
528            "type": "m.login.protocol",
529            "protocol": "org.example.future_protocol",
530            "org.example.field": {
531                "some": "data"
532            },
533            "device_id": "ABCDEFGH"
534        });
535
536        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
537        assert_let!(
538            QrAuthMessage::LoginProtocol {
539                protocol: LoginProtocolData::Unknown(protocol),
540                device_id
541            } = &message
542        );
543        assert_eq!(protocol.protocol().as_str(), "org.example.future_protocol");
544        assert_eq!(
545            protocol.data(),
546            &JsonObject::from_iter([("org.example.field".to_owned(), json!({ "some": "data" }))])
547        );
548        assert_eq!(device_id, "ABCDEFGH");
549
550        // The data of the unknown protocol is kept, and no device
551        // authorization grant fields are added.
552        let serialized = serde_json::to_value(&message).unwrap();
553        assert_eq!(json, serialized);
554    }
555
556    #[test]
557    fn test_protocol_deserialization_missing_device_authorization_grant() {
558        let json = json!({
559            "type": "m.login.protocol",
560            "protocol": "device_authorization_grant",
561            "device_id": "ABCDEFGH"
562        });
563
564        let error = serde_json::from_value::<QrAuthMessage>(json).expect_err(
565            "The device authorization grant protocol requires the device authorization grant",
566        );
567        let error = error.to_string();
568        assert!(error.contains("device_authorization_grant"), "{error}");
569        assert!(error.contains("verification_uri"), "{error}");
570    }
571
572    #[test]
573    fn test_protocol_deserialization_invalid_device_authorization_grant() {
574        let json = json!({
575            "type": "m.login.protocol",
576            "protocol": "device_authorization_grant",
577            "device_authorization_grant": {},
578            "device_id": "ABCDEFGH"
579        });
580
581        let error = serde_json::from_value::<QrAuthMessage>(json)
582            .expect_err("The device authorization grant requires the verification_uri field");
583        let error = error.to_string();
584        assert!(error.contains("device_authorization_grant"), "{error}");
585        assert!(error.contains("verification_uri"), "{error}");
586    }
587
588    #[test]
589    fn test_protocol_serialization_drops_unknown_fields_of_known_protocol() {
590        let json = json!({
591            "type": "m.login.protocol",
592            "protocol": "device_authorization_grant",
593            "device_authorization_grant": {
594                "verification_uri": "https://id.matrix.org/device/abcde"
595            },
596            "org.example.hint": "data",
597            "device_id": "ABCDEFGH"
598        });
599
600        let message: QrAuthMessage = serde_json::from_value(json).unwrap();
601        let serialized = serde_json::to_value(&message).unwrap();
602        assert_eq!(
603            serialized,
604            json!({
605                "type": "m.login.protocol",
606                "protocol": "device_authorization_grant",
607                "device_authorization_grant": {
608                    "verification_uri": "https://id.matrix.org/device/abcde",
609                },
610                "device_id": "ABCDEFGH"
611            })
612        );
613
614        let json = json!({
615            "type": "m.login.protocol",
616            "protocol": "device_authorization_grant",
617            "verification_uri": "https://id.matrix.org/device/abcde",
618            "org.example.hint": "data",
619            "device_id": "ABCDEFGH"
620        });
621
622        let message: QrAuthMessage = serde_json::from_value(json).unwrap();
623        let serialized = serde_json::to_value(&message).unwrap();
624        assert_eq!(
625            serialized,
626            json!({
627                "type": "m.login.protocol",
628                "protocol": "device_authorization_grant",
629                "verification_uri": "https://id.matrix.org/device/abcde",
630                "device_id": "ABCDEFGH"
631            })
632        );
633    }
634
635    #[test]
636    fn test_login_protocol_data_new() {
637        let grant = json!({
638            "verification_uri": "https://id.matrix.org/device/abcde",
639        });
640        let assert_grant = |device_authorization_grant: AuthorizationGrant| {
641            assert_eq!(
642                device_authorization_grant.verification_uri.as_str(),
643                "https://id.matrix.org/device/abcde"
644            );
645        };
646
647        // A known protocol with valid data nested in a field named after it
648        // gets the MSC4108 variant.
649        let data = LoginProtocolData::new(
650            LoginProtocolType::DeviceAuthorizationGrant,
651            JsonObject::from_iter([("device_authorization_grant".to_owned(), grant.clone())]),
652        )
653        .unwrap();
654        assert_let!(
655            LoginProtocolData::Msc4108DeviceAuthorizationGrant(device_authorization_grant) = data
656        );
657        assert_grant(device_authorization_grant);
658
659        // A known protocol with valid data next to the protocol gets the
660        // MSC4388 variant.
661        let data = LoginProtocolData::new(
662            LoginProtocolType::DeviceAuthorizationGrant,
663            serde_json::from_value(grant.clone()).unwrap(),
664        )
665        .unwrap();
666        assert_let!(
667            LoginProtocolData::Msc4388DeviceAuthorizationGrant(device_authorization_grant) = data
668        );
669        assert_grant(device_authorization_grant);
670
671        // A known protocol with missing data is an error.
672        LoginProtocolData::new(LoginProtocolType::DeviceAuthorizationGrant, JsonObject::new())
673            .expect_err("The device authorization grant protocol requires its data");
674
675        // An unknown protocol keeps its data, even if it looks like the data of
676        // a known protocol.
677        let other =
678            JsonObject::from_iter([("device_authorization_grant".to_owned(), grant.clone())]);
679        let data =
680            LoginProtocolData::new("org.example.future_protocol".into(), other.clone()).unwrap();
681        assert_let!(LoginProtocolData::Unknown(protocol) = data);
682        assert_eq!(protocol.protocol().as_str(), "org.example.future_protocol");
683        assert_eq!(protocol.data(), &other);
684
685        // The data of an unknown protocol can't contain the fields of the
686        // message, as they would be duplicated when serialized.
687        for field in ["type", "protocol", "device_id"] {
688            let other = JsonObject::from_iter([(field.to_owned(), json!("foo"))]);
689            let error = LoginProtocolData::new("org.example.future_protocol".into(), other)
690                .expect_err("The reserved fields should be rejected");
691            assert!(error.to_string().contains(field), "{error}");
692        }
693
694        // A known protocol only keeps the fields it knows about, so the
695        // reserved fields can't clash and are dropped like any other field.
696        let data = LoginProtocolData::new(
697            LoginProtocolType::DeviceAuthorizationGrant,
698            JsonObject::from_iter([
699                ("device_authorization_grant".to_owned(), grant.clone()),
700                ("type".to_owned(), json!("foo")),
701                ("org.example.hint".to_owned(), json!(1)),
702            ]),
703        )
704        .unwrap();
705        assert_let!(LoginProtocolData::Msc4108DeviceAuthorizationGrant(_) = data);
706
707        let data = LoginProtocolData::new(
708            LoginProtocolType::DeviceAuthorizationGrant,
709            JsonObject::from_iter([
710                ("verification_uri".to_owned(), grant["verification_uri"].clone()),
711                ("type".to_owned(), json!("foo")),
712                ("org.example.hint".to_owned(), json!(1)),
713            ]),
714        )
715        .unwrap();
716        assert_let!(LoginProtocolData::Msc4388DeviceAuthorizationGrant(_) = data);
717    }
718
719    #[test]
720    fn test_protocol_accepted_serialization() {
721        let json = json!({
722            "type": "m.login.protocol_accepted",
723        });
724
725        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
726        assert_let!(QrAuthMessage::LoginProtocolAccepted = &message);
727        let serialized = serde_json::to_value(&message).unwrap();
728        assert_eq!(json, serialized);
729    }
730
731    #[test]
732    fn test_login_success() {
733        let json = json!({
734            "type": "m.login.success",
735        });
736
737        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
738        assert_let!(QrAuthMessage::LoginSuccess = &message);
739        let serialized = serde_json::to_value(&message).unwrap();
740        assert_eq!(json, serialized);
741    }
742
743    #[test]
744    fn test_login_declined() {
745        let json = json!({
746            "type": "m.login.declined",
747        });
748
749        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
750        assert_let!(QrAuthMessage::LoginDeclined = &message);
751        let serialized = serde_json::to_value(&message).unwrap();
752        assert_eq!(json, serialized);
753    }
754
755    #[test]
756    fn test_login_failure() {
757        let json = json!({
758            "type": "m.login.failure",
759            "reason": "unsupported_protocol",
760            "homeserver": "https://matrix-client.matrix.org/"
761        });
762
763        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
764        assert_let!(QrAuthMessage::LoginFailure { reason, homeserver } = &message);
765        assert_eq!(reason, &LoginFailureReason::UnsupportedProtocol);
766        // Older implementations send a URL, which we keep as is.
767        assert_eq!(homeserver.as_deref(), Some("https://matrix-client.matrix.org/"));
768        let serialized = serde_json::to_value(&message).unwrap();
769        assert_eq!(json, serialized);
770    }
771
772    #[test]
773    fn test_login_failure_with_server_name() {
774        // The MSC defines the homeserver as a server name, not a URL.
775        let json = json!({
776            "type": "m.login.failure",
777            "reason": "unsupported_protocol",
778            "homeserver": "matrix.org"
779        });
780
781        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
782        assert_let!(QrAuthMessage::LoginFailure { reason, homeserver } = &message);
783        assert_eq!(reason, &LoginFailureReason::UnsupportedProtocol);
784        assert_eq!(homeserver.as_deref(), Some("matrix.org"));
785        let serialized = serde_json::to_value(&message).unwrap();
786        assert_eq!(json, serialized);
787    }
788
789    #[test]
790    fn test_login_failure_without_homeserver() {
791        let json = json!({
792            "type": "m.login.failure",
793            "reason": "user_cancelled",
794        });
795
796        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
797        assert_let!(QrAuthMessage::LoginFailure { reason, homeserver } = &message);
798        assert_eq!(reason, &LoginFailureReason::UserCancelled);
799        assert!(homeserver.is_none());
800
801        // A missing homeserver must not be serialized as `null`.
802        let serialized = serde_json::to_value(&message).unwrap();
803        assert_eq!(json, serialized);
804
805        // But we accept an explicit `null` from other implementations.
806        let message: QrAuthMessage = serde_json::from_value(json!({
807            "type": "m.login.failure",
808            "reason": "user_cancelled",
809            "homeserver": null,
810        }))
811        .unwrap();
812        assert_let!(QrAuthMessage::LoginFailure { homeserver: None, .. } = &message);
813    }
814
815    #[test]
816    fn test_login_secrets() {
817        let json = json!({
818            "type": "m.login.secrets",
819            "cross_signing": {
820                "master_key": "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw",
821                "self_signing_key": "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk",
822                "user_signing_key": "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM",
823            },
824            "backup": {
825                "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
826                "backup_version": "2",
827                "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
828            },
829        });
830
831        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
832        assert_let!(
833            QrAuthMessage::LoginSecrets(SecretsBundle { cross_signing, backup }) = &message
834        );
835        assert_eq!(cross_signing.master_key, "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw");
836        assert_eq!(cross_signing.self_signing_key, "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk");
837        assert_eq!(cross_signing.user_signing_key, "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM");
838
839        assert_let!(Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(backup)) = backup);
840        assert_eq!(backup.backup_version, "2");
841        assert_eq!(&backup.key.to_base64(), "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
842
843        let serialized = serde_json::to_value(&message).unwrap();
844        assert_eq!(json, serialized);
845    }
846}