matrix_sdk/test_utils/mocks/
oauth.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//! Helpers to mock an OAuth 2.0 server for the purpose of integration tests.
16
17use ruma::{
18    api::client::discovery::get_authorization_server_metadata::msc2965::AuthorizationServerMetadata,
19    serde::Raw,
20};
21use serde_json::json;
22use url::Url;
23use wiremock::{
24    matchers::{method, path_regex},
25    Mock, MockBuilder, ResponseTemplate,
26};
27
28use super::{MatrixMock, MatrixMockServer, MockEndpoint};
29
30/// A [`wiremock`] [`MockServer`] along with useful methods to help mocking
31/// OAuth 2.0 API endpoints easily.
32///
33/// It implements mock endpoints, limiting the shared code as much as possible,
34/// so the mocks are still flexible to use as scoped/unscoped mounts, named, and
35/// so on.
36///
37/// It works like this:
38///
39/// * start by saying which endpoint you'd like to mock, e.g.
40///   [`Self::mock_server_metadata()`]. This returns a specialized
41///   [`MockEndpoint`] data structure, with its own impl. For this example, it's
42///   `MockEndpoint<ServerMetadataEndpoint>`.
43/// * configure the response on the endpoint-specific mock data structure. For
44///   instance, if you want the sending to result in a transient failure, call
45///   [`MockEndpoint::error500`]; if you want it to succeed and return the
46///   metadata, call [`MockEndpoint::ok()`]. It's still possible to call
47///   [`MockEndpoint::respond_with()`], as we do with wiremock MockBuilder, for
48///   maximum flexibility when the helpers aren't sufficient.
49/// * once the endpoint's response is configured, for any mock builder, you get
50///   a [`MatrixMock`]; this is a plain [`wiremock::Mock`] with the server
51///   curried, so one doesn't have to pass it around when calling
52///   [`MatrixMock::mount()`] or [`MatrixMock::mount_as_scoped()`]. As such, it
53///   mostly defers its implementations to [`wiremock::Mock`] under the hood.
54///
55/// [`MockServer`]: wiremock::MockServer
56pub struct OauthMockServer<'a> {
57    server: &'a MatrixMockServer,
58}
59
60impl<'a> OauthMockServer<'a> {
61    pub(super) fn new(server: &'a MatrixMockServer) -> Self {
62        Self { server }
63    }
64
65    /// Mock the given endpoint.
66    fn mock_endpoint<T>(&self, mock: MockBuilder, endpoint: T) -> MockEndpoint<'a, T> {
67        self.server.mock_endpoint(mock, endpoint)
68    }
69}
70
71// Specific mount endpoints.
72impl OauthMockServer<'_> {
73    /// Creates a prebuilt mock for the Matrix endpoint used to query the
74    /// authorization server's metadata.
75    ///
76    /// Contrary to all the other endpoints of [`OauthMockServer`], this is an
77    /// endpoint from the Matrix API, but it is only used in the context of the
78    /// OAuth 2.0 API, which is why it is mocked here rather than on
79    /// [`MatrixMockServer`].
80    ///
81    /// [`MatrixMockServer`]: super::MatrixMockServer
82    pub fn mock_server_metadata(&self) -> MockEndpoint<'_, ServerMetadataEndpoint> {
83        let mock = Mock::given(method("GET"))
84            .and(path_regex(r"^/_matrix/client/unstable/org.matrix.msc2965/auth_metadata"));
85        self.mock_endpoint(mock, ServerMetadataEndpoint)
86    }
87
88    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to register a
89    /// new client.
90    pub fn mock_registration(&self) -> MockEndpoint<'_, RegistrationEndpoint> {
91        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/registration"));
92        self.mock_endpoint(mock, RegistrationEndpoint)
93    }
94
95    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to authorize a
96    /// device.
97    pub fn mock_device_authorization(&self) -> MockEndpoint<'_, DeviceAuthorizationEndpoint> {
98        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/device"));
99        self.mock_endpoint(mock, DeviceAuthorizationEndpoint)
100    }
101
102    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to request an
103    /// access token.
104    pub fn mock_token(&self) -> MockEndpoint<'_, TokenEndpoint> {
105        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/token"));
106        self.mock_endpoint(mock, TokenEndpoint)
107    }
108
109    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to revoke a
110    /// token.
111    pub fn mock_revocation(&self) -> MockEndpoint<'_, RevocationEndpoint> {
112        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/revoke"));
113        self.mock_endpoint(mock, RevocationEndpoint)
114    }
115}
116
117/// A prebuilt mock for a `GET /auth_metadata` request.
118pub struct ServerMetadataEndpoint;
119
120impl<'a> MockEndpoint<'a, ServerMetadataEndpoint> {
121    /// Returns a successful metadata response with all the supported endpoints.
122    pub fn ok(self) -> MatrixMock<'a> {
123        let metadata = MockServerMetadataBuilder::new(&self.server.uri()).build();
124        self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
125    }
126
127    /// Returns a successful metadata response with all the supported endpoints
128    /// using HTTPS URLs.
129    ///
130    /// This should be used with
131    /// `MockClientBuilder::insecure_rewrite_https_to_http()` to bypass checks
132    /// from the oauth2 crate.
133    pub fn ok_https(self) -> MatrixMock<'a> {
134        let issuer = self.server.uri().replace("http://", "https://");
135
136        let metadata = MockServerMetadataBuilder::new(&issuer).build();
137        self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
138    }
139
140    /// Returns a successful metadata response without the device authorization
141    /// endpoint.
142    pub fn ok_without_device_authorization(self) -> MatrixMock<'a> {
143        let metadata = MockServerMetadataBuilder::new(&self.server.uri())
144            .without_device_authorization()
145            .build();
146        self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
147    }
148
149    /// Returns a successful metadata response without the registration
150    /// endpoint.
151    pub fn ok_without_registration(self) -> MatrixMock<'a> {
152        let metadata =
153            MockServerMetadataBuilder::new(&self.server.uri()).without_registration().build();
154        self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
155    }
156}
157
158/// Helper struct to construct an `AuthorizationServerMetadata` for integration
159/// tests.
160#[derive(Debug, Clone)]
161pub struct MockServerMetadataBuilder {
162    issuer: Url,
163    with_device_authorization: bool,
164    with_registration: bool,
165}
166
167impl MockServerMetadataBuilder {
168    /// Construct a `MockServerMetadataBuilder` that will generate all the
169    /// supported fields.
170    pub fn new(issuer: &str) -> Self {
171        let issuer = Url::parse(issuer).expect("We should be able to parse the issuer");
172
173        Self { issuer, with_device_authorization: true, with_registration: true }
174    }
175
176    /// Don't generate the field for the device authorization endpoint.
177    fn without_device_authorization(mut self) -> Self {
178        self.with_device_authorization = false;
179        self
180    }
181
182    /// Don't generate the field for the registration endpoint.
183    fn without_registration(mut self) -> Self {
184        self.with_registration = false;
185        self
186    }
187
188    /// The authorization endpoint of this server.
189    fn authorization_endpoint(&self) -> Url {
190        self.issuer.join("oauth2/authorize").unwrap()
191    }
192
193    /// The token endpoint of this server.
194    fn token_endpoint(&self) -> Url {
195        self.issuer.join("oauth2/token").unwrap()
196    }
197
198    /// The JWKS URI of this server.
199    fn jwks_uri(&self) -> Url {
200        self.issuer.join("oauth2/keys.json").unwrap()
201    }
202
203    /// The registration endpoint of this server.
204    fn registration_endpoint(&self) -> Url {
205        self.issuer.join("oauth2/registration").unwrap()
206    }
207
208    /// The account management URI of this server.
209    fn account_management_uri(&self) -> Url {
210        self.issuer.join("account").unwrap()
211    }
212
213    /// The device authorization endpoint of this server.
214    fn device_authorization_endpoint(&self) -> Url {
215        self.issuer.join("oauth2/device").unwrap()
216    }
217
218    /// The revocation endpoint of this server.
219    fn revocation_endpoint(&self) -> Url {
220        self.issuer.join("oauth2/revoke").unwrap()
221    }
222
223    /// Build the server metadata.
224    pub fn build(&self) -> Raw<AuthorizationServerMetadata> {
225        let mut json_metadata = json!({
226            "issuer": self.issuer,
227            "authorization_endpoint": self.authorization_endpoint(),
228            "token_endpoint": self.token_endpoint(),
229            "response_types_supported": ["code"],
230            "response_modes_supported": ["query", "fragment"],
231            "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
232            "revocation_endpoint": self.revocation_endpoint(),
233            "code_challenge_methods_supported": ["S256"],
234            "account_management_uri": self.account_management_uri(),
235            "account_management_actions_supported": ["org.matrix.profile", "org.matrix.sessions_list", "org.matrix.session_view", "org.matrix.session_end", "org.matrix.deactivateaccount", "org.matrix.cross_signing_reset"],
236            "prompt_values_supported": ["create"],
237        });
238        let json_metadata_object = json_metadata.as_object_mut().unwrap();
239
240        if self.with_device_authorization {
241            json_metadata_object.insert(
242                "device_authorization_endpoint".to_owned(),
243                self.device_authorization_endpoint().as_str().into(),
244            );
245        }
246
247        if self.with_registration {
248            json_metadata_object.insert(
249                "registration_endpoint".to_owned(),
250                self.registration_endpoint().as_str().into(),
251            );
252        }
253
254        serde_json::from_value(json_metadata).unwrap()
255    }
256}
257
258/// A prebuilt mock for a `POST /oauth/registration` request.
259pub struct RegistrationEndpoint;
260
261impl<'a> MockEndpoint<'a, RegistrationEndpoint> {
262    /// Returns a successful registration response.
263    pub fn ok(self) -> MatrixMock<'a> {
264        self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
265            "client_id": "test_client_id",
266            "client_id_issued_at": 1716375696,
267        })))
268    }
269}
270
271/// A prebuilt mock for a `POST /oauth/device` request.
272pub struct DeviceAuthorizationEndpoint;
273
274impl<'a> MockEndpoint<'a, DeviceAuthorizationEndpoint> {
275    /// Returns a successful device authorization response.
276    pub fn ok(self) -> MatrixMock<'a> {
277        let issuer_url = Url::parse(&self.server.uri())
278            .expect("We should be able to parse the wiremock server URI");
279        let verification_uri = issuer_url.join("link").unwrap();
280        let mut verification_uri_complete = issuer_url.join("link").unwrap();
281        verification_uri_complete.set_query(Some("code=N32YVC"));
282
283        self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
284            "device_code": "N8NAYD9fOhMulpm37mSthx0xSw2p7vdR",
285            "expires_in": 1200,
286            "interval": 5,
287            "user_code": "N32YVC",
288            "verification_uri": verification_uri,
289            "verification_uri_complete": verification_uri_complete,
290        })))
291    }
292}
293
294/// A prebuilt mock for a `POST /oauth/token` request.
295pub struct TokenEndpoint;
296
297impl<'a> MockEndpoint<'a, TokenEndpoint> {
298    /// Returns a successful token response.
299    pub fn ok(self) -> MatrixMock<'a> {
300        self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
301            "access_token": "1234",
302            "expires_in": 300,
303            "refresh_token": "ZYXWV",
304            "token_type": "Bearer"
305        })))
306    }
307
308    /// Returns an error response when the request was invalid.
309    pub fn access_denied(self) -> MatrixMock<'a> {
310        self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
311            "error": "access_denied",
312        })))
313    }
314
315    /// Returns an error response when the token in the request has expired.
316    pub fn expired_token(self) -> MatrixMock<'a> {
317        self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
318            "error": "expired_token",
319        })))
320    }
321
322    /// Returns an error response when the token in the request is invalid.
323    pub fn invalid_grant(self) -> MatrixMock<'a> {
324        self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
325            "error": "invalid_grant",
326        })))
327    }
328}
329
330/// A prebuilt mock for a `POST /oauth/revoke` request.
331pub struct RevocationEndpoint;
332
333impl<'a> MockEndpoint<'a, RevocationEndpoint> {
334    /// Returns a successful revocation response.
335    pub fn ok(self) -> MatrixMock<'a> {
336        self.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
337    }
338}