Skip to main content

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::v1::AuthorizationServerMetadata,
19    serde::Raw,
20};
21use serde_json::json;
22use url::Url;
23use wiremock::{
24    Mock, MockBuilder,
25    matchers::{method, path_regex},
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    /// Get the mock OAuth 2.0 server metadata.
71    pub fn server_metadata(&self) -> AuthorizationServerMetadata {
72        MockServerMetadataBuilder::new(&self.server.uri())
73            .build()
74            .deserialize()
75            .expect("mock OAuth 2.0 server metadata should deserialize successfully")
76    }
77}
78
79// Specific mount endpoints.
80impl OAuthMockServer<'_> {
81    /// Creates a prebuilt mock for the Matrix endpoint used to query the
82    /// authorization server's metadata.
83    ///
84    /// Contrary to all the other endpoints of [`OAuthMockServer`], this is an
85    /// endpoint from the Matrix API, but it is only used in the context of the
86    /// OAuth 2.0 API, which is why it is mocked here rather than on
87    /// [`MatrixMockServer`].
88    ///
89    /// [`MatrixMockServer`]: super::MatrixMockServer
90    pub fn mock_server_metadata(&self) -> MockEndpoint<'_, ServerMetadataEndpoint> {
91        let mock = Mock::given(method("GET"))
92            .and(path_regex(r"^/_matrix/client/unstable/org.matrix.msc2965/auth_metadata"));
93        self.mock_endpoint(mock, ServerMetadataEndpoint)
94    }
95
96    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to register a
97    /// new client.
98    pub fn mock_registration(&self) -> MockEndpoint<'_, RegistrationEndpoint> {
99        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/registration"));
100        self.mock_endpoint(mock, RegistrationEndpoint)
101    }
102
103    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to authorize a
104    /// device.
105    pub fn mock_device_authorization(&self) -> MockEndpoint<'_, DeviceAuthorizationEndpoint> {
106        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/device"));
107        self.mock_endpoint(mock, DeviceAuthorizationEndpoint)
108    }
109
110    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to request an
111    /// access token.
112    pub fn mock_token(&self) -> MockEndpoint<'_, TokenEndpoint> {
113        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/token"));
114        self.mock_endpoint(mock, TokenEndpoint)
115    }
116
117    /// Creates a prebuilt mock for the OAuth 2.0 endpoint used to revoke a
118    /// token.
119    pub fn mock_revocation(&self) -> MockEndpoint<'_, RevocationEndpoint> {
120        let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/revoke"));
121        self.mock_endpoint(mock, RevocationEndpoint)
122    }
123}
124
125/// A prebuilt mock for a `GET /auth_metadata` request.
126#[derive(Default)]
127pub struct ServerMetadataEndpoint;
128
129impl<'a> MockEndpoint<'a, ServerMetadataEndpoint> {
130    /// Returns a successful response with the given metadata, honouring the
131    /// delay set with [`Self::with_delay`].
132    fn ok_with_metadata(self, metadata: Raw<AuthorizationServerMetadata>) -> MatrixMock<'a> {
133        self.ok_json(metadata)
134    }
135
136    /// Returns a successful metadata response with all the supported endpoints.
137    pub fn ok(self) -> MatrixMock<'a> {
138        let metadata = MockServerMetadataBuilder::new(&self.server.uri()).build();
139        self.ok_with_metadata(metadata)
140    }
141
142    /// Returns a successful metadata response with all the supported endpoints
143    /// using HTTPS URLs.
144    ///
145    /// This should be used with
146    /// `MockClientBuilder::insecure_rewrite_https_to_http()` to bypass checks
147    /// from the oauth2 crate.
148    pub fn ok_https(self) -> MatrixMock<'a> {
149        let issuer = self.server.uri().replace("http://", "https://");
150
151        let metadata = MockServerMetadataBuilder::new(&issuer).build();
152        self.ok_with_metadata(metadata)
153    }
154
155    /// Returns a successful metadata response without the device authorization
156    /// endpoint and grant type.
157    pub fn ok_without_device_authorization(self) -> MatrixMock<'a> {
158        let metadata = MockServerMetadataBuilder::new(&self.server.uri())
159            .without_device_authorization()
160            .build();
161        self.ok_with_metadata(metadata)
162    }
163
164    /// Returns a successful metadata response without the registration
165    /// endpoint.
166    pub fn ok_without_registration(self) -> MatrixMock<'a> {
167        let metadata =
168            MockServerMetadataBuilder::new(&self.server.uri()).without_registration().build();
169        self.ok_with_metadata(metadata)
170    }
171}
172
173/// Helper struct to construct an `AuthorizationServerMetadata` for integration
174/// tests.
175#[derive(Debug, Clone)]
176pub struct MockServerMetadataBuilder {
177    issuer: Url,
178    with_device_authorization: bool,
179    with_registration: bool,
180}
181
182impl MockServerMetadataBuilder {
183    /// Construct a `MockServerMetadataBuilder` that will generate all the
184    /// supported fields.
185    pub fn new(issuer: &str) -> Self {
186        let issuer = Url::parse(issuer).expect("We should be able to parse the issuer");
187
188        Self { issuer, with_device_authorization: true, with_registration: true }
189    }
190
191    /// Don't generate the field for the device authorization endpoint, and
192    /// don't advertise the device authorization grant type.
193    fn without_device_authorization(mut self) -> Self {
194        self.with_device_authorization = false;
195        self
196    }
197
198    /// Don't generate the field for the registration endpoint.
199    fn without_registration(mut self) -> Self {
200        self.with_registration = false;
201        self
202    }
203
204    /// The authorization endpoint of this server.
205    fn authorization_endpoint(&self) -> Url {
206        self.issuer.join("oauth2/authorize").unwrap()
207    }
208
209    /// The token endpoint of this server.
210    fn token_endpoint(&self) -> Url {
211        self.issuer.join("oauth2/token").unwrap()
212    }
213
214    /// The JWKS URI of this server.
215    fn jwks_uri(&self) -> Url {
216        self.issuer.join("oauth2/keys.json").unwrap()
217    }
218
219    /// The registration endpoint of this server.
220    fn registration_endpoint(&self) -> Url {
221        self.issuer.join("oauth2/registration").unwrap()
222    }
223
224    /// The account management URI of this server.
225    fn account_management_uri(&self) -> Url {
226        self.issuer.join("account").unwrap()
227    }
228
229    /// The device authorization endpoint of this server.
230    fn device_authorization_endpoint(&self) -> Url {
231        self.issuer.join("oauth2/device").unwrap()
232    }
233
234    /// The revocation endpoint of this server.
235    fn revocation_endpoint(&self) -> Url {
236        self.issuer.join("oauth2/revoke").unwrap()
237    }
238
239    /// Build the server metadata.
240    pub fn build(&self) -> Raw<AuthorizationServerMetadata> {
241        let mut json_metadata = json!({
242            "issuer": self.issuer,
243            "authorization_endpoint": self.authorization_endpoint(),
244            "token_endpoint": self.token_endpoint(),
245            "response_types_supported": ["code"],
246            "response_modes_supported": ["query", "fragment"],
247            "grant_types_supported": ["authorization_code", "refresh_token"],
248            "revocation_endpoint": self.revocation_endpoint(),
249            "code_challenge_methods_supported": ["S256"],
250            "account_management_uri": self.account_management_uri(),
251            "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"],
252            "prompt_values_supported": ["create"],
253        });
254        let json_metadata_object = json_metadata.as_object_mut().unwrap();
255
256        if self.with_device_authorization {
257            json_metadata_object.insert(
258                "grant_types_supported".to_owned(),
259                json!([
260                    "authorization_code",
261                    "refresh_token",
262                    "urn:ietf:params:oauth:grant-type:device_code"
263                ]),
264            );
265            json_metadata_object.insert(
266                "device_authorization_endpoint".to_owned(),
267                self.device_authorization_endpoint().as_str().into(),
268            );
269        }
270
271        if self.with_registration {
272            json_metadata_object.insert(
273                "registration_endpoint".to_owned(),
274                self.registration_endpoint().as_str().into(),
275            );
276        }
277
278        serde_json::from_value(json_metadata).unwrap()
279    }
280}
281
282/// A prebuilt mock for a `POST /oauth/registration` request.
283pub struct RegistrationEndpoint;
284
285impl<'a> MockEndpoint<'a, RegistrationEndpoint> {
286    /// Returns a successful registration response.
287    pub fn ok(self) -> MatrixMock<'a> {
288        self.ok_json(json!({
289            "client_id": "test_client_id",
290            "client_id_issued_at": 1716375696,
291        }))
292    }
293}
294
295/// A prebuilt mock for a `POST /oauth/device` request.
296pub struct DeviceAuthorizationEndpoint;
297
298impl<'a> MockEndpoint<'a, DeviceAuthorizationEndpoint> {
299    /// Returns a successful device authorization response.
300    pub fn ok(self) -> MatrixMock<'a> {
301        let issuer_url = Url::parse(&self.server.uri())
302            .expect("We should be able to parse the wiremock server URI");
303        let verification_uri = issuer_url.join("link").unwrap();
304        let mut verification_uri_complete = issuer_url.join("link").unwrap();
305        verification_uri_complete.set_query(Some("code=N32YVC"));
306
307        self.ok_json(json!({
308            "device_code": "N8NAYD9fOhMulpm37mSthx0xSw2p7vdR",
309            "expires_in": 1200,
310            "interval": 5,
311            "user_code": "N32YVC",
312            "verification_uri": verification_uri,
313            "verification_uri_complete": verification_uri_complete,
314        }))
315    }
316}
317
318/// A prebuilt mock for a `POST /oauth/token` request.
319pub struct TokenEndpoint;
320
321impl<'a> MockEndpoint<'a, TokenEndpoint> {
322    /// Returns a successful token response with the default tokens.
323    pub fn ok(self) -> MatrixMock<'a> {
324        self.ok_with_tokens("1234", "ZYXWV")
325    }
326
327    /// Returns a successful token response with custom tokens.
328    pub fn ok_with_tokens(self, access_token: &str, refresh_token: &str) -> MatrixMock<'a> {
329        self.ok_json(json!({
330            "access_token": access_token,
331            "expires_in": 300,
332            "refresh_token":  refresh_token,
333            "token_type": "Bearer"
334        }))
335    }
336
337    /// Returns an error response when the request was invalid.
338    pub fn access_denied(self) -> MatrixMock<'a> {
339        self.respond_with_code_and_json(
340            400,
341            json!({
342                "error": "access_denied",
343            }),
344        )
345    }
346
347    /// Returns an error response when the token in the request has expired.
348    pub fn expired_token(self) -> MatrixMock<'a> {
349        self.respond_with_code_and_json(
350            400,
351            json!({
352                "error": "expired_token",
353            }),
354        )
355    }
356
357    /// Returns an error response when the token in the request is invalid.
358    pub fn invalid_grant(self) -> MatrixMock<'a> {
359        self.respond_with_code_and_json(
360            400,
361            json!({
362                "error": "invalid_grant",
363            }),
364        )
365    }
366}
367
368/// A prebuilt mock for a `POST /oauth/revoke` request.
369pub struct RevocationEndpoint;
370
371impl<'a> MockEndpoint<'a, RevocationEndpoint> {
372    /// Returns a successful revocation response.
373    pub fn ok(self) -> MatrixMock<'a> {
374        self.ok_empty_json()
375    }
376}