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.
1415//! Helpers to mock an OAuth 2.0 server for the purpose of integration tests.
1617use 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};
2728use super::{MatrixMock, MatrixMockServer, MockEndpoint};
2930/// 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}
5960impl<'a> OAuthMockServer<'a> {
61pub(super) fn new(server: &'a MatrixMockServer) -> Self {
62Self { server }
63 }
6465/// Mock the given endpoint.
66fn mock_endpoint<T>(&self, mock: MockBuilder, endpoint: T) -> MockEndpoint<'a, T> {
67self.server.mock_endpoint(mock, endpoint)
68 }
6970/// Get the mock OAuth 2.0 server metadata.
71pub fn server_metadata(&self) -> AuthorizationServerMetadata {
72 MockServerMetadataBuilder::new(&self.server.server().uri())
73 .build()
74 .deserialize()
75 .expect("mock OAuth 2.0 server metadata should deserialize successfully")
76 }
77}
7879// 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
90pub fn mock_server_metadata(&self) -> MockEndpoint<'_, ServerMetadataEndpoint> {
91let mock = Mock::given(method("GET"))
92 .and(path_regex(r"^/_matrix/client/unstable/org.matrix.msc2965/auth_metadata"));
93self.mock_endpoint(mock, ServerMetadataEndpoint)
94 }
9596/// Creates a prebuilt mock for the OAuth 2.0 endpoint used to register a
97 /// new client.
98pub fn mock_registration(&self) -> MockEndpoint<'_, RegistrationEndpoint> {
99let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/registration"));
100self.mock_endpoint(mock, RegistrationEndpoint)
101 }
102103/// Creates a prebuilt mock for the OAuth 2.0 endpoint used to authorize a
104 /// device.
105pub fn mock_device_authorization(&self) -> MockEndpoint<'_, DeviceAuthorizationEndpoint> {
106let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/device"));
107self.mock_endpoint(mock, DeviceAuthorizationEndpoint)
108 }
109110/// Creates a prebuilt mock for the OAuth 2.0 endpoint used to request an
111 /// access token.
112pub fn mock_token(&self) -> MockEndpoint<'_, TokenEndpoint> {
113let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/token"));
114self.mock_endpoint(mock, TokenEndpoint)
115 }
116117/// Creates a prebuilt mock for the OAuth 2.0 endpoint used to revoke a
118 /// token.
119pub fn mock_revocation(&self) -> MockEndpoint<'_, RevocationEndpoint> {
120let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/revoke"));
121self.mock_endpoint(mock, RevocationEndpoint)
122 }
123}
124125/// A prebuilt mock for a `GET /auth_metadata` request.
126pub struct ServerMetadataEndpoint;
127128impl<'a> MockEndpoint<'a, ServerMetadataEndpoint> {
129/// Returns a successful metadata response with all the supported endpoints.
130pub fn ok(self) -> MatrixMock<'a> {
131let metadata = MockServerMetadataBuilder::new(&self.server.uri()).build();
132self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
133 }
134135/// Returns a successful metadata response with all the supported endpoints
136 /// using HTTPS URLs.
137 ///
138 /// This should be used with
139 /// `MockClientBuilder::insecure_rewrite_https_to_http()` to bypass checks
140 /// from the oauth2 crate.
141pub fn ok_https(self) -> MatrixMock<'a> {
142let issuer = self.server.uri().replace("http://", "https://");
143144let metadata = MockServerMetadataBuilder::new(&issuer).build();
145self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
146 }
147148/// Returns a successful metadata response without the device authorization
149 /// endpoint.
150pub fn ok_without_device_authorization(self) -> MatrixMock<'a> {
151let metadata = MockServerMetadataBuilder::new(&self.server.uri())
152 .without_device_authorization()
153 .build();
154self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
155 }
156157/// Returns a successful metadata response without the registration
158 /// endpoint.
159pub fn ok_without_registration(self) -> MatrixMock<'a> {
160let metadata =
161 MockServerMetadataBuilder::new(&self.server.uri()).without_registration().build();
162self.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
163 }
164}
165166/// Helper struct to construct an `AuthorizationServerMetadata` for integration
167/// tests.
168#[derive(Debug, Clone)]
169pub struct MockServerMetadataBuilder {
170 issuer: Url,
171 with_device_authorization: bool,
172 with_registration: bool,
173}
174175impl MockServerMetadataBuilder {
176/// Construct a `MockServerMetadataBuilder` that will generate all the
177 /// supported fields.
178pub fn new(issuer: &str) -> Self {
179let issuer = Url::parse(issuer).expect("We should be able to parse the issuer");
180181Self { issuer, with_device_authorization: true, with_registration: true }
182 }
183184/// Don't generate the field for the device authorization endpoint.
185fn without_device_authorization(mut self) -> Self {
186self.with_device_authorization = false;
187self
188}
189190/// Don't generate the field for the registration endpoint.
191fn without_registration(mut self) -> Self {
192self.with_registration = false;
193self
194}
195196/// The authorization endpoint of this server.
197fn authorization_endpoint(&self) -> Url {
198self.issuer.join("oauth2/authorize").unwrap()
199 }
200201/// The token endpoint of this server.
202fn token_endpoint(&self) -> Url {
203self.issuer.join("oauth2/token").unwrap()
204 }
205206/// The JWKS URI of this server.
207fn jwks_uri(&self) -> Url {
208self.issuer.join("oauth2/keys.json").unwrap()
209 }
210211/// The registration endpoint of this server.
212fn registration_endpoint(&self) -> Url {
213self.issuer.join("oauth2/registration").unwrap()
214 }
215216/// The account management URI of this server.
217fn account_management_uri(&self) -> Url {
218self.issuer.join("account").unwrap()
219 }
220221/// The device authorization endpoint of this server.
222fn device_authorization_endpoint(&self) -> Url {
223self.issuer.join("oauth2/device").unwrap()
224 }
225226/// The revocation endpoint of this server.
227fn revocation_endpoint(&self) -> Url {
228self.issuer.join("oauth2/revoke").unwrap()
229 }
230231/// Build the server metadata.
232pub fn build(&self) -> Raw<AuthorizationServerMetadata> {
233let mut json_metadata = json!({
234"issuer": self.issuer,
235"authorization_endpoint": self.authorization_endpoint(),
236"token_endpoint": self.token_endpoint(),
237"response_types_supported": ["code"],
238"response_modes_supported": ["query", "fragment"],
239"grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
240"revocation_endpoint": self.revocation_endpoint(),
241"code_challenge_methods_supported": ["S256"],
242"account_management_uri": self.account_management_uri(),
243"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"],
244"prompt_values_supported": ["create"],
245 });
246let json_metadata_object = json_metadata.as_object_mut().unwrap();
247248if self.with_device_authorization {
249 json_metadata_object.insert(
250"device_authorization_endpoint".to_owned(),
251self.device_authorization_endpoint().as_str().into(),
252 );
253 }
254255if self.with_registration {
256 json_metadata_object.insert(
257"registration_endpoint".to_owned(),
258self.registration_endpoint().as_str().into(),
259 );
260 }
261262 serde_json::from_value(json_metadata).unwrap()
263 }
264}
265266/// A prebuilt mock for a `POST /oauth/registration` request.
267pub struct RegistrationEndpoint;
268269impl<'a> MockEndpoint<'a, RegistrationEndpoint> {
270/// Returns a successful registration response.
271pub fn ok(self) -> MatrixMock<'a> {
272self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
273"client_id": "test_client_id",
274"client_id_issued_at": 1716375696,
275 })))
276 }
277}
278279/// A prebuilt mock for a `POST /oauth/device` request.
280pub struct DeviceAuthorizationEndpoint;
281282impl<'a> MockEndpoint<'a, DeviceAuthorizationEndpoint> {
283/// Returns a successful device authorization response.
284pub fn ok(self) -> MatrixMock<'a> {
285let issuer_url = Url::parse(&self.server.uri())
286 .expect("We should be able to parse the wiremock server URI");
287let verification_uri = issuer_url.join("link").unwrap();
288let mut verification_uri_complete = issuer_url.join("link").unwrap();
289 verification_uri_complete.set_query(Some("code=N32YVC"));
290291self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
292"device_code": "N8NAYD9fOhMulpm37mSthx0xSw2p7vdR",
293"expires_in": 1200,
294"interval": 5,
295"user_code": "N32YVC",
296"verification_uri": verification_uri,
297"verification_uri_complete": verification_uri_complete,
298 })))
299 }
300}
301302/// A prebuilt mock for a `POST /oauth/token` request.
303pub struct TokenEndpoint;
304305impl<'a> MockEndpoint<'a, TokenEndpoint> {
306/// Returns a successful token response with the default tokens.
307pub fn ok(self) -> MatrixMock<'a> {
308self.ok_with_tokens("1234", "ZYXWV")
309 }
310311/// Returns a successful token response with custom tokens.
312pub fn ok_with_tokens(self, access_token: &str, refresh_token: &str) -> MatrixMock<'a> {
313self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
314"access_token": access_token,
315"expires_in": 300,
316"refresh_token": refresh_token,
317"token_type": "Bearer"
318})))
319 }
320321/// Returns an error response when the request was invalid.
322pub fn access_denied(self) -> MatrixMock<'a> {
323self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
324"error": "access_denied",
325 })))
326 }
327328/// Returns an error response when the token in the request has expired.
329pub fn expired_token(self) -> MatrixMock<'a> {
330self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
331"error": "expired_token",
332 })))
333 }
334335/// Returns an error response when the token in the request is invalid.
336pub fn invalid_grant(self) -> MatrixMock<'a> {
337self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
338"error": "invalid_grant",
339 })))
340 }
341}
342343/// A prebuilt mock for a `POST /oauth/revoke` request.
344pub struct RevocationEndpoint;
345346impl<'a> MockEndpoint<'a, RevocationEndpoint> {
347/// Returns a successful revocation response.
348pub fn ok(self) -> MatrixMock<'a> {
349self.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
350 }
351}