matrix_sdk/test_utils/mocks/
oauth.rs1use std::time::Duration;
18
19use ruma::{
20 api::client::discovery::get_authorization_server_metadata::v1::AuthorizationServerMetadata,
21 serde::Raw,
22};
23use serde_json::json;
24use url::Url;
25use wiremock::{
26 Mock, MockBuilder, ResponseTemplate,
27 matchers::{method, path_regex},
28};
29
30use super::{MatrixMock, MatrixMockServer, MockEndpoint};
31
32pub struct OAuthMockServer<'a> {
59 server: &'a MatrixMockServer,
60}
61
62impl<'a> OAuthMockServer<'a> {
63 pub(super) fn new(server: &'a MatrixMockServer) -> Self {
64 Self { server }
65 }
66
67 fn mock_endpoint<T>(&self, mock: MockBuilder, endpoint: T) -> MockEndpoint<'a, T> {
69 self.server.mock_endpoint(mock, endpoint)
70 }
71
72 pub fn server_metadata(&self) -> AuthorizationServerMetadata {
74 MockServerMetadataBuilder::new(&self.server.uri())
75 .build()
76 .deserialize()
77 .expect("mock OAuth 2.0 server metadata should deserialize successfully")
78 }
79}
80
81impl OAuthMockServer<'_> {
83 pub fn mock_server_metadata(&self) -> MockEndpoint<'_, ServerMetadataEndpoint> {
93 let mock = Mock::given(method("GET"))
94 .and(path_regex(r"^/_matrix/client/unstable/org.matrix.msc2965/auth_metadata"));
95 self.mock_endpoint(mock, ServerMetadataEndpoint::default())
96 }
97
98 pub fn mock_registration(&self) -> MockEndpoint<'_, RegistrationEndpoint> {
101 let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/registration"));
102 self.mock_endpoint(mock, RegistrationEndpoint)
103 }
104
105 pub fn mock_device_authorization(&self) -> MockEndpoint<'_, DeviceAuthorizationEndpoint> {
108 let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/device"));
109 self.mock_endpoint(mock, DeviceAuthorizationEndpoint)
110 }
111
112 pub fn mock_token(&self) -> MockEndpoint<'_, TokenEndpoint> {
115 let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/token"));
116 self.mock_endpoint(mock, TokenEndpoint)
117 }
118
119 pub fn mock_revocation(&self) -> MockEndpoint<'_, RevocationEndpoint> {
122 let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/revoke"));
123 self.mock_endpoint(mock, RevocationEndpoint)
124 }
125}
126
127#[derive(Default)]
129pub struct ServerMetadataEndpoint {
130 delay: Option<Duration>,
132}
133
134impl<'a> MockEndpoint<'a, ServerMetadataEndpoint> {
135 pub fn with_delay(mut self, delay: Duration) -> Self {
137 self.endpoint.delay = Some(delay);
138 self
139 }
140
141 fn ok_with_metadata(self, metadata: Raw<AuthorizationServerMetadata>) -> MatrixMock<'a> {
144 let mut template = ResponseTemplate::new(200).set_body_json(metadata);
145
146 if let Some(delay) = self.endpoint.delay {
147 template = template.set_delay(delay);
148 }
149
150 self.respond_with(template)
151 }
152
153 pub fn ok(self) -> MatrixMock<'a> {
155 let metadata = MockServerMetadataBuilder::new(&self.server.uri()).build();
156 self.ok_with_metadata(metadata)
157 }
158
159 pub fn ok_https(self) -> MatrixMock<'a> {
166 let issuer = self.server.uri().replace("http://", "https://");
167
168 let metadata = MockServerMetadataBuilder::new(&issuer).build();
169 self.ok_with_metadata(metadata)
170 }
171
172 pub fn ok_without_device_authorization(self) -> MatrixMock<'a> {
175 let metadata = MockServerMetadataBuilder::new(&self.server.uri())
176 .without_device_authorization()
177 .build();
178 self.ok_with_metadata(metadata)
179 }
180
181 pub fn ok_without_registration(self) -> MatrixMock<'a> {
184 let metadata =
185 MockServerMetadataBuilder::new(&self.server.uri()).without_registration().build();
186 self.ok_with_metadata(metadata)
187 }
188}
189
190#[derive(Debug, Clone)]
193pub struct MockServerMetadataBuilder {
194 issuer: Url,
195 with_device_authorization: bool,
196 with_registration: bool,
197}
198
199impl MockServerMetadataBuilder {
200 pub fn new(issuer: &str) -> Self {
203 let issuer = Url::parse(issuer).expect("We should be able to parse the issuer");
204
205 Self { issuer, with_device_authorization: true, with_registration: true }
206 }
207
208 fn without_device_authorization(mut self) -> Self {
210 self.with_device_authorization = false;
211 self
212 }
213
214 fn without_registration(mut self) -> Self {
216 self.with_registration = false;
217 self
218 }
219
220 fn authorization_endpoint(&self) -> Url {
222 self.issuer.join("oauth2/authorize").unwrap()
223 }
224
225 fn token_endpoint(&self) -> Url {
227 self.issuer.join("oauth2/token").unwrap()
228 }
229
230 fn jwks_uri(&self) -> Url {
232 self.issuer.join("oauth2/keys.json").unwrap()
233 }
234
235 fn registration_endpoint(&self) -> Url {
237 self.issuer.join("oauth2/registration").unwrap()
238 }
239
240 fn account_management_uri(&self) -> Url {
242 self.issuer.join("account").unwrap()
243 }
244
245 fn device_authorization_endpoint(&self) -> Url {
247 self.issuer.join("oauth2/device").unwrap()
248 }
249
250 fn revocation_endpoint(&self) -> Url {
252 self.issuer.join("oauth2/revoke").unwrap()
253 }
254
255 pub fn build(&self) -> Raw<AuthorizationServerMetadata> {
257 let mut json_metadata = json!({
258 "issuer": self.issuer,
259 "authorization_endpoint": self.authorization_endpoint(),
260 "token_endpoint": self.token_endpoint(),
261 "response_types_supported": ["code"],
262 "response_modes_supported": ["query", "fragment"],
263 "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
264 "revocation_endpoint": self.revocation_endpoint(),
265 "code_challenge_methods_supported": ["S256"],
266 "account_management_uri": self.account_management_uri(),
267 "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"],
268 "prompt_values_supported": ["create"],
269 });
270 let json_metadata_object = json_metadata.as_object_mut().unwrap();
271
272 if self.with_device_authorization {
273 json_metadata_object.insert(
274 "device_authorization_endpoint".to_owned(),
275 self.device_authorization_endpoint().as_str().into(),
276 );
277 }
278
279 if self.with_registration {
280 json_metadata_object.insert(
281 "registration_endpoint".to_owned(),
282 self.registration_endpoint().as_str().into(),
283 );
284 }
285
286 serde_json::from_value(json_metadata).unwrap()
287 }
288}
289
290pub struct RegistrationEndpoint;
292
293impl<'a> MockEndpoint<'a, RegistrationEndpoint> {
294 pub fn ok(self) -> MatrixMock<'a> {
296 self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
297 "client_id": "test_client_id",
298 "client_id_issued_at": 1716375696,
299 })))
300 }
301}
302
303pub struct DeviceAuthorizationEndpoint;
305
306impl<'a> MockEndpoint<'a, DeviceAuthorizationEndpoint> {
307 pub fn ok(self) -> MatrixMock<'a> {
309 let issuer_url = Url::parse(&self.server.uri())
310 .expect("We should be able to parse the wiremock server URI");
311 let verification_uri = issuer_url.join("link").unwrap();
312 let mut verification_uri_complete = issuer_url.join("link").unwrap();
313 verification_uri_complete.set_query(Some("code=N32YVC"));
314
315 self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
316 "device_code": "N8NAYD9fOhMulpm37mSthx0xSw2p7vdR",
317 "expires_in": 1200,
318 "interval": 5,
319 "user_code": "N32YVC",
320 "verification_uri": verification_uri,
321 "verification_uri_complete": verification_uri_complete,
322 })))
323 }
324}
325
326pub struct TokenEndpoint;
328
329impl<'a> MockEndpoint<'a, TokenEndpoint> {
330 pub fn ok(self) -> MatrixMock<'a> {
332 self.ok_with_tokens("1234", "ZYXWV")
333 }
334
335 pub fn ok_with_tokens(self, access_token: &str, refresh_token: &str) -> MatrixMock<'a> {
337 self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
338 "access_token": access_token,
339 "expires_in": 300,
340 "refresh_token": refresh_token,
341 "token_type": "Bearer"
342 })))
343 }
344
345 pub fn access_denied(self) -> MatrixMock<'a> {
347 self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
348 "error": "access_denied",
349 })))
350 }
351
352 pub fn expired_token(self) -> MatrixMock<'a> {
354 self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
355 "error": "expired_token",
356 })))
357 }
358
359 pub fn invalid_grant(self) -> MatrixMock<'a> {
361 self.respond_with(ResponseTemplate::new(400).set_body_json(json!({
362 "error": "invalid_grant",
363 })))
364 }
365}
366
367pub struct RevocationEndpoint;
369
370impl<'a> MockEndpoint<'a, RevocationEndpoint> {
371 pub fn ok(self) -> MatrixMock<'a> {
373 self.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
374 }
375}