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::StringEnum;
21use serde::{Deserialize, Serialize};
22use url::Url;
23use vodozemac::Curve25519PublicKey;
24
25#[cfg(doc)]
26use super::QRCodeLoginError::SecureChannel;
27
28/// Messages that will be exchanged over the [`SecureChannel`] to log in a new
29/// device using a QR code.
30#[derive(Debug, Serialize, Deserialize)]
31#[serde(tag = "type")]
32pub enum QrAuthMessage {
33    /// Message declaring the available protocols for sign in. Sent by the
34    /// existing device.
35    #[serde(rename = "m.login.protocols")]
36    LoginProtocols {
37        /// The login protocols the existing device supports.
38        protocols: Vec<LoginProtocolType>,
39        /// The homeserver we're going to log in to.
40        homeserver: Url,
41    },
42
43    /// Message declaring which protocols from the previous `m.login.protocols`
44    /// message the new device has picked. Sent by the new device.
45    #[serde(rename = "m.login.protocol")]
46    LoginProtocol {
47        /// The device authorization grant the OAuth 2.0 server has given to the
48        /// new device, contains the URL the existing device should use to
49        /// confirm the log in.
50        device_authorization_grant: AuthorizationGrant,
51        /// The protocol the new device has picked.
52        protocol: LoginProtocolType,
53        /// The device ID the new device will be using.
54        device_id: String,
55    },
56
57    /// Message declaring that the protocol in the previous `m.login.protocol`
58    /// message was accepted. Sent by the existing device.
59    #[serde(rename = "m.login.protocol_accepted")]
60    LoginProtocolAccepted,
61
62    /// Message that informs the existing device that it successfully obtained
63    /// an access token from the OAuth 2.0 server. Sent by the new device.
64    #[serde(rename = "m.login.success")]
65    LoginSuccess,
66
67    /// Message that informs the existing device that the OAuth 2.0 server has
68    /// declined to give us an access token, i.e. because the user declined the
69    /// log in. Sent by the new device.
70    #[serde(rename = "m.login.declined")]
71    LoginDeclined,
72
73    /// Message signaling that a failure happened during the login. Can be sent
74    /// by either device.
75    #[serde(rename = "m.login.failure")]
76    LoginFailure {
77        /// The claimed reason for the login failure.
78        reason: LoginFailureReason,
79        /// The homeserver that we attempted to log in to.
80        homeserver: Option<Url>,
81    },
82
83    /// Message containing end-to-end encryption related secrets, the new device
84    /// can use these secrets to mark itself as verified, connect to a room
85    /// key backup, and login other devices via a QR login. Sent by the
86    /// existing device.
87    #[serde(rename = "m.login.secrets")]
88    LoginSecrets(SecretsBundle),
89}
90
91impl QrAuthMessage {
92    /// Create a new [`QrAuthMessage::LoginProtocol`] message with the
93    /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol type.
94    pub fn authorization_grant_login_protocol(
95        device_authorization_grant: AuthorizationGrant,
96        device_id: Curve25519PublicKey,
97    ) -> QrAuthMessage {
98        QrAuthMessage::LoginProtocol {
99            device_id: device_id.to_base64(),
100            device_authorization_grant,
101            protocol: LoginProtocolType::DeviceAuthorizationGrant,
102        }
103    }
104}
105
106impl From<&StandardDeviceAuthorizationResponse> for AuthorizationGrant {
107    fn from(value: &StandardDeviceAuthorizationResponse) -> Self {
108        Self {
109            verification_uri: value.verification_uri().clone(),
110            verification_uri_complete: value.verification_uri_complete().cloned(),
111        }
112    }
113}
114
115/// Data for the device authorization grant login protocol.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct AuthorizationGrant {
118    /// The verification URL the user should open to log the new device in.
119    pub verification_uri: EndUserVerificationUrl,
120
121    /// The verification URL, with the user code pre-filled, which the user
122    /// should open to log the new device in. If this URL is available, the
123    /// user should be presented with it instead of the one in the
124    /// [`AuthorizationGrant::verification_uri`] field.
125    pub verification_uri_complete: Option<VerificationUriComplete>,
126}
127
128/// Reasons why the login might have failed.
129#[derive(Clone, StringEnum)]
130#[ruma_enum(rename_all = "snake_case")]
131pub enum LoginFailureReason {
132    /// The Device Authorization Grant expired.
133    AuthorizationExpired,
134    /// The device ID specified by the new device already exists in the
135    /// homeserver provided device list.
136    DeviceAlreadyExists,
137    /// The new device is not present in the device list as returned by the
138    /// homeserver.
139    DeviceNotFound,
140    /// Sent by either device to indicate that they received a message of a type
141    /// that they weren't expecting.
142    UnexpectedMessageReceived,
143    /// Sent by a device where no suitable protocol is available or the
144    /// requested protocol requested is not supported.
145    UnsupportedProtocol,
146    /// Sent by either new or existing device to indicate that the user has
147    /// cancelled the login.
148    UserCancelled,
149    #[doc(hidden)]
150    _Custom(PrivOwnedStr),
151}
152
153/// Enum containing known login protocol types.
154#[derive(Clone, StringEnum)]
155#[ruma_enum(rename_all = "snake_case")]
156pub enum LoginProtocolType {
157    /// The `device_authorization_grant` login protocol type.
158    DeviceAuthorizationGrant,
159    #[doc(hidden)]
160    _Custom(PrivOwnedStr),
161}
162
163#[cfg(test)]
164mod test {
165    use assert_matches2::assert_let;
166    use matrix_sdk_base::crypto::types::BackupSecrets;
167    use serde_json::json;
168    use similar_asserts::assert_eq;
169
170    use super::*;
171
172    #[test]
173    fn test_protocols_serialization() {
174        let json = json!({
175            "type": "m.login.protocols",
176            "protocols": ["device_authorization_grant"],
177            "homeserver": "https://matrix-client.matrix.org/"
178
179        });
180
181        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
182        assert_let!(QrAuthMessage::LoginProtocols { protocols, .. } = &message);
183        assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant));
184
185        let serialized = serde_json::to_value(&message).unwrap();
186        assert_eq!(json, serialized);
187    }
188
189    #[test]
190    fn test_protocol_serialization() {
191        let json = json!({
192            "type": "m.login.protocol",
193            "protocol": "device_authorization_grant",
194            "device_authorization_grant": {
195                "verification_uri_complete": "https://id.matrix.org/device/abcde",
196                "verification_uri": "https://id.matrix.org/device/abcde?code=ABCDE"
197            },
198            "device_id": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
199        });
200
201        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
202        assert_let!(QrAuthMessage::LoginProtocol { protocol, device_id, .. } = &message);
203        assert_eq!(protocol, &LoginProtocolType::DeviceAuthorizationGrant);
204        assert_eq!(device_id, "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4");
205        let serialized = serde_json::to_value(&message).unwrap();
206        assert_eq!(json, serialized);
207    }
208
209    #[test]
210    fn test_protocol_accepted_serialization() {
211        let json = json!({
212            "type": "m.login.protocol_accepted",
213        });
214
215        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
216        assert_let!(QrAuthMessage::LoginProtocolAccepted = &message);
217        let serialized = serde_json::to_value(&message).unwrap();
218        assert_eq!(json, serialized);
219    }
220
221    #[test]
222    fn test_login_success() {
223        let json = json!({
224            "type": "m.login.success",
225        });
226
227        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
228        assert_let!(QrAuthMessage::LoginSuccess = &message);
229        let serialized = serde_json::to_value(&message).unwrap();
230        assert_eq!(json, serialized);
231    }
232
233    #[test]
234    fn test_login_declined() {
235        let json = json!({
236            "type": "m.login.declined",
237        });
238
239        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
240        assert_let!(QrAuthMessage::LoginDeclined = &message);
241        let serialized = serde_json::to_value(&message).unwrap();
242        assert_eq!(json, serialized);
243    }
244
245    #[test]
246    fn test_login_failure() {
247        let json = json!({
248            "type": "m.login.failure",
249            "reason": "unsupported_protocol",
250            "homeserver": "https://matrix-client.matrix.org/"
251        });
252
253        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
254        assert_let!(QrAuthMessage::LoginFailure { reason, .. } = &message);
255        assert_eq!(reason, &LoginFailureReason::UnsupportedProtocol);
256        let serialized = serde_json::to_value(&message).unwrap();
257        assert_eq!(json, serialized);
258    }
259
260    #[test]
261    fn test_login_secrets() {
262        let json = json!({
263            "type": "m.login.secrets",
264            "cross_signing": {
265                "master_key": "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw",
266                "self_signing_key": "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk",
267                "user_signing_key": "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM",
268            },
269            "backup": {
270                "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
271                "backup_version": "2",
272                "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
273            },
274        });
275
276        let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap();
277        assert_let!(
278            QrAuthMessage::LoginSecrets(SecretsBundle { cross_signing, backup }) = &message
279        );
280        assert_eq!(cross_signing.master_key, "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw");
281        assert_eq!(cross_signing.self_signing_key, "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk");
282        assert_eq!(cross_signing.user_signing_key, "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM");
283
284        assert_let!(Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(backup)) = backup);
285        assert_eq!(backup.backup_version, "2");
286        assert_eq!(&backup.key.to_base64(), "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
287
288        let serialized = serde_json::to_value(&message).unwrap();
289        assert_eq!(json, serialized);
290    }
291}