1use std::sync::LazyLock;
8
9use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse};
10use hyper::{HeaderMap, StatusCode};
11use mas_axum_utils::{
12 client_authorization::{ClientAuthorization, CredentialsVerificationError},
13 sentry::SentryEventID,
14};
15use mas_data_model::{Device, TokenFormatError, TokenType};
16use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint};
17use mas_keystore::Encrypter;
18use mas_storage::{
19 BoxClock, BoxRepository, Clock,
20 compat::{CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository},
21 oauth2::{OAuth2AccessTokenRepository, OAuth2RefreshTokenRepository, OAuth2SessionRepository},
22 user::UserRepository,
23};
24use oauth2_types::{
25 errors::{ClientError, ClientErrorCode},
26 requests::{IntrospectionRequest, IntrospectionResponse},
27 scope::ScopeToken,
28};
29use opentelemetry::{Key, KeyValue, metrics::Counter};
30use thiserror::Error;
31
32use crate::{ActivityTracker, METER, impl_from_error_for_route};
33
34static INTROSPECTION_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
35 METER
36 .u64_counter("mas.oauth2.introspection_request")
37 .with_description("Number of OAuth 2.0 introspection requests")
38 .with_unit("{request}")
39 .build()
40});
41
42const KIND: Key = Key::from_static_str("kind");
43const ACTIVE: Key = Key::from_static_str("active");
44
45#[derive(Debug, Error)]
46pub enum RouteError {
47 #[error(transparent)]
49 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
50
51 #[error("could not find client")]
53 ClientNotFound,
54
55 #[error("client is not allowed to introspect")]
57 NotAllowed,
58
59 #[error("unexpected token type")]
61 UnexpectedTokenType,
62
63 #[error("invalid token format")]
65 InvalidTokenFormat(#[from] TokenFormatError),
66
67 #[error("unknown {0}")]
69 UnknownToken(TokenType),
70
71 #[error("{0} is not valid")]
73 InvalidToken(TokenType),
74
75 #[error("invalid oauth session")]
77 InvalidOAuthSession,
78
79 #[error("unknown oauth session")]
81 CantLoadOAuthSession,
82
83 #[error("invalid compat session")]
85 InvalidCompatSession,
86
87 #[error("unknown compat session")]
89 CantLoadCompatSession,
90
91 #[error("device ID contains characters that are not allowed in a scope")]
93 CantEncodeDeviceID(#[from] mas_data_model::ToScopeTokenError),
94
95 #[error("invalid user")]
96 InvalidUser,
97
98 #[error("unknown user")]
99 CantLoadUser,
100
101 #[error("bad request")]
102 BadRequest,
103
104 #[error(transparent)]
105 ClientCredentialsVerification(#[from] CredentialsVerificationError),
106}
107
108impl IntoResponse for RouteError {
109 fn into_response(self) -> axum::response::Response {
110 let event_id = sentry::capture_error(&self);
111 let response = match self {
112 e @ (Self::Internal(_)
113 | Self::CantLoadCompatSession
114 | Self::CantLoadOAuthSession
115 | Self::CantLoadUser) => (
116 StatusCode::INTERNAL_SERVER_ERROR,
117 Json(
118 ClientError::from(ClientErrorCode::ServerError).with_description(e.to_string()),
119 ),
120 )
121 .into_response(),
122 Self::ClientNotFound => (
123 StatusCode::UNAUTHORIZED,
124 Json(ClientError::from(ClientErrorCode::InvalidClient)),
125 )
126 .into_response(),
127 Self::ClientCredentialsVerification(e) => (
128 StatusCode::UNAUTHORIZED,
129 Json(
130 ClientError::from(ClientErrorCode::InvalidClient)
131 .with_description(e.to_string()),
132 ),
133 )
134 .into_response(),
135
136 Self::UnknownToken(_)
137 | Self::UnexpectedTokenType
138 | Self::InvalidToken(_)
139 | Self::InvalidUser
140 | Self::InvalidCompatSession
141 | Self::InvalidOAuthSession
142 | Self::InvalidTokenFormat(_)
143 | Self::CantEncodeDeviceID(_) => {
144 INTROSPECTION_COUNTER.add(1, &[KeyValue::new(ACTIVE.clone(), false)]);
145
146 Json(INACTIVE).into_response()
147 }
148
149 Self::NotAllowed => (
150 StatusCode::UNAUTHORIZED,
151 Json(ClientError::from(ClientErrorCode::AccessDenied)),
152 )
153 .into_response(),
154 Self::BadRequest => (
155 StatusCode::BAD_REQUEST,
156 Json(ClientError::from(ClientErrorCode::InvalidRequest)),
157 )
158 .into_response(),
159 };
160
161 (SentryEventID::from(event_id), response).into_response()
162 }
163}
164
165impl_from_error_for_route!(mas_storage::RepositoryError);
166
167const INACTIVE: IntrospectionResponse = IntrospectionResponse {
168 active: false,
169 scope: None,
170 client_id: None,
171 username: None,
172 token_type: None,
173 exp: None,
174 expires_in: None,
175 iat: None,
176 nbf: None,
177 sub: None,
178 aud: None,
179 iss: None,
180 jti: None,
181 device_id: None,
182};
183
184const API_SCOPE: ScopeToken = ScopeToken::from_static("urn:matrix:org.matrix.msc2967.client:api:*");
185const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:admin:*");
186
187#[tracing::instrument(
188 name = "handlers.oauth2.introspection.post",
189 fields(client.id = client_authorization.client_id()),
190 skip_all,
191 err,
192)]
193#[allow(clippy::too_many_lines)]
194pub(crate) async fn post(
195 clock: BoxClock,
196 State(http_client): State<reqwest::Client>,
197 mut repo: BoxRepository,
198 activity_tracker: ActivityTracker,
199 State(encrypter): State<Encrypter>,
200 headers: HeaderMap,
201 client_authorization: ClientAuthorization<IntrospectionRequest>,
202) -> Result<impl IntoResponse, RouteError> {
203 let client = client_authorization
204 .credentials
205 .fetch(&mut repo)
206 .await?
207 .ok_or(RouteError::ClientNotFound)?;
208
209 let method = match &client.token_endpoint_auth_method {
210 None | Some(OAuthClientAuthenticationMethod::None) => {
211 return Err(RouteError::NotAllowed);
212 }
213 Some(c) => c,
214 };
215
216 client_authorization
217 .credentials
218 .verify(&http_client, &encrypter, method, &client)
219 .await?;
220
221 let Some(form) = client_authorization.form else {
222 return Err(RouteError::BadRequest);
223 };
224
225 let token = &form.token;
226 let token_type = TokenType::check(token)?;
227 if let Some(hint) = form.token_type_hint {
228 if token_type != hint {
229 return Err(RouteError::UnexpectedTokenType);
230 }
231 }
232
233 let supports_explicit_device_id =
241 headers.get("X-MAS-Supports-Device-Id") == Some(&HeaderValue::from_static("1"));
242
243 let ip = None;
245
246 let reply = match token_type {
247 TokenType::AccessToken => {
248 let mut access_token = repo
249 .oauth2_access_token()
250 .find_by_token(token)
251 .await?
252 .ok_or(RouteError::UnknownToken(TokenType::AccessToken))?;
253
254 if !access_token.is_valid(clock.now()) {
255 return Err(RouteError::InvalidToken(TokenType::AccessToken));
256 }
257
258 let session = repo
259 .oauth2_session()
260 .lookup(access_token.session_id)
261 .await?
262 .ok_or(RouteError::InvalidOAuthSession)?;
263
264 if !session.is_valid() {
265 return Err(RouteError::InvalidOAuthSession);
266 }
267
268 if !access_token.is_used() {
270 access_token = repo
271 .oauth2_access_token()
272 .mark_used(&clock, access_token)
273 .await?;
274 }
275
276 let (sub, username) = if let Some(user_id) = session.user_id {
279 let user = repo
280 .user()
281 .lookup(user_id)
282 .await?
283 .ok_or(RouteError::CantLoadUser)?;
284
285 if !user.is_valid() {
286 return Err(RouteError::InvalidUser);
287 }
288
289 (Some(user.sub), Some(user.username))
290 } else {
291 (None, None)
292 };
293
294 activity_tracker
295 .record_oauth2_session(&clock, &session, ip)
296 .await;
297
298 INTROSPECTION_COUNTER.add(
299 1,
300 &[
301 KeyValue::new(KIND, "oauth2_access_token"),
302 KeyValue::new(ACTIVE, true),
303 ],
304 );
305
306 IntrospectionResponse {
307 active: true,
308 scope: Some(session.scope),
309 client_id: Some(session.client_id.to_string()),
310 username,
311 token_type: Some(OAuthTokenTypeHint::AccessToken),
312 exp: access_token.expires_at,
313 expires_in: access_token
314 .expires_at
315 .map(|expires_at| expires_at.signed_duration_since(clock.now())),
316 iat: Some(access_token.created_at),
317 nbf: Some(access_token.created_at),
318 sub,
319 aud: None,
320 iss: None,
321 jti: Some(access_token.jti()),
322 device_id: None,
323 }
324 }
325
326 TokenType::RefreshToken => {
327 let refresh_token = repo
328 .oauth2_refresh_token()
329 .find_by_token(token)
330 .await?
331 .ok_or(RouteError::UnknownToken(TokenType::RefreshToken))?;
332
333 if !refresh_token.is_valid() {
334 return Err(RouteError::InvalidToken(TokenType::RefreshToken));
335 }
336
337 let session = repo
338 .oauth2_session()
339 .lookup(refresh_token.session_id)
340 .await?
341 .ok_or(RouteError::CantLoadOAuthSession)?;
342
343 if !session.is_valid() {
344 return Err(RouteError::InvalidOAuthSession);
345 }
346
347 let (sub, username) = if let Some(user_id) = session.user_id {
350 let user = repo
351 .user()
352 .lookup(user_id)
353 .await?
354 .ok_or(RouteError::CantLoadUser)?;
355
356 if !user.is_valid() {
357 return Err(RouteError::InvalidUser);
358 }
359
360 (Some(user.sub), Some(user.username))
361 } else {
362 (None, None)
363 };
364
365 activity_tracker
366 .record_oauth2_session(&clock, &session, ip)
367 .await;
368
369 INTROSPECTION_COUNTER.add(
370 1,
371 &[
372 KeyValue::new(KIND, "oauth2_refresh_token"),
373 KeyValue::new(ACTIVE, true),
374 ],
375 );
376
377 IntrospectionResponse {
378 active: true,
379 scope: Some(session.scope),
380 client_id: Some(session.client_id.to_string()),
381 username,
382 token_type: Some(OAuthTokenTypeHint::RefreshToken),
383 exp: None,
384 expires_in: None,
385 iat: Some(refresh_token.created_at),
386 nbf: Some(refresh_token.created_at),
387 sub,
388 aud: None,
389 iss: None,
390 jti: Some(refresh_token.jti()),
391 device_id: None,
392 }
393 }
394
395 TokenType::CompatAccessToken => {
396 let access_token = repo
397 .compat_access_token()
398 .find_by_token(token)
399 .await?
400 .ok_or(RouteError::UnknownToken(TokenType::CompatAccessToken))?;
401
402 if !access_token.is_valid(clock.now()) {
403 return Err(RouteError::InvalidToken(TokenType::CompatAccessToken));
404 }
405
406 let session = repo
407 .compat_session()
408 .lookup(access_token.session_id)
409 .await?
410 .ok_or(RouteError::CantLoadCompatSession)?;
411
412 if !session.is_valid() {
413 return Err(RouteError::InvalidCompatSession);
414 }
415
416 let user = repo
417 .user()
418 .lookup(session.user_id)
419 .await?
420 .ok_or(RouteError::CantLoadUser)?;
421
422 if !user.is_valid() {
423 return Err(RouteError::InvalidUser)?;
424 }
425
426 let synapse_admin_scope_opt = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
428
429 let device_scope_opt = if supports_explicit_device_id {
432 None
433 } else {
434 session
435 .device
436 .as_ref()
437 .map(Device::to_scope_token)
438 .transpose()?
439 };
440
441 let scope = [API_SCOPE]
442 .into_iter()
443 .chain(device_scope_opt)
444 .chain(synapse_admin_scope_opt)
445 .collect();
446
447 activity_tracker
448 .record_compat_session(&clock, &session, ip)
449 .await;
450
451 INTROSPECTION_COUNTER.add(
452 1,
453 &[
454 KeyValue::new(KIND, "compat_access_token"),
455 KeyValue::new(ACTIVE, true),
456 ],
457 );
458
459 IntrospectionResponse {
460 active: true,
461 scope: Some(scope),
462 client_id: Some("legacy".into()),
463 username: Some(user.username),
464 token_type: Some(OAuthTokenTypeHint::AccessToken),
465 exp: access_token.expires_at,
466 expires_in: access_token
467 .expires_at
468 .map(|expires_at| expires_at.signed_duration_since(clock.now())),
469 iat: Some(access_token.created_at),
470 nbf: Some(access_token.created_at),
471 sub: Some(user.sub),
472 aud: None,
473 iss: None,
474 jti: None,
475 device_id: session.device.map(Device::into),
476 }
477 }
478
479 TokenType::CompatRefreshToken => {
480 let refresh_token = repo
481 .compat_refresh_token()
482 .find_by_token(token)
483 .await?
484 .ok_or(RouteError::UnknownToken(TokenType::CompatRefreshToken))?;
485
486 if !refresh_token.is_valid() {
487 return Err(RouteError::InvalidToken(TokenType::CompatRefreshToken));
488 }
489
490 let session = repo
491 .compat_session()
492 .lookup(refresh_token.session_id)
493 .await?
494 .ok_or(RouteError::CantLoadCompatSession)?;
495
496 if !session.is_valid() {
497 return Err(RouteError::InvalidCompatSession);
498 }
499
500 let user = repo
501 .user()
502 .lookup(session.user_id)
503 .await?
504 .ok_or(RouteError::CantLoadUser)?;
505
506 if !user.is_valid() {
507 return Err(RouteError::InvalidUser)?;
508 }
509
510 let synapse_admin_scope_opt = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
512
513 let device_scope_opt = if supports_explicit_device_id {
516 None
517 } else {
518 session
519 .device
520 .as_ref()
521 .map(Device::to_scope_token)
522 .transpose()?
523 };
524
525 let scope = [API_SCOPE]
526 .into_iter()
527 .chain(device_scope_opt)
528 .chain(synapse_admin_scope_opt)
529 .collect();
530
531 activity_tracker
532 .record_compat_session(&clock, &session, ip)
533 .await;
534
535 INTROSPECTION_COUNTER.add(
536 1,
537 &[
538 KeyValue::new(KIND, "compat_refresh_token"),
539 KeyValue::new(ACTIVE, true),
540 ],
541 );
542
543 IntrospectionResponse {
544 active: true,
545 scope: Some(scope),
546 client_id: Some("legacy".into()),
547 username: Some(user.username),
548 token_type: Some(OAuthTokenTypeHint::RefreshToken),
549 exp: None,
550 expires_in: None,
551 iat: Some(refresh_token.created_at),
552 nbf: Some(refresh_token.created_at),
553 sub: Some(user.sub),
554 aud: None,
555 iss: None,
556 jti: None,
557 device_id: session.device.map(Device::into),
558 }
559 }
560 };
561
562 repo.save().await?;
563
564 Ok(Json(reply))
565}
566
567#[cfg(test)]
568mod tests {
569 use chrono::Duration;
570 use hyper::{Request, StatusCode};
571 use mas_data_model::{AccessToken, RefreshToken};
572 use mas_iana::oauth::OAuthTokenTypeHint;
573 use mas_matrix::{HomeserverConnection, ProvisionRequest};
574 use mas_router::{OAuth2Introspection, OAuth2RegistrationEndpoint, SimpleRoute};
575 use mas_storage::Clock;
576 use oauth2_types::{
577 registration::ClientRegistrationResponse,
578 requests::IntrospectionResponse,
579 scope::{OPENID, Scope},
580 };
581 use serde_json::json;
582 use sqlx::PgPool;
583 use zeroize::Zeroizing;
584
585 use crate::{
586 oauth2::generate_token_pair,
587 test_utils::{RequestBuilderExt, ResponseExt, TestState, setup},
588 };
589
590 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
591 async fn test_introspect_oauth_tokens(pool: PgPool) {
592 setup();
593 let state = TestState::from_pool(pool).await.unwrap();
594
595 let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
597 "client_uri": "https://introspecting.com/",
598 "grant_types": [],
599 "token_endpoint_auth_method": "client_secret_basic",
600 }));
601
602 let response = state.request(request).await;
603 response.assert_status(StatusCode::CREATED);
604 let client: ClientRegistrationResponse = response.json();
605 let introspecting_client_id = client.client_id;
606 let introspecting_client_secret = client.client_secret.unwrap();
607
608 let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
610 "client_uri": "https://client.com/",
611 "redirect_uris": ["https://client.com/"],
612 "response_types": ["code"],
613 "grant_types": ["authorization_code", "refresh_token"],
614 "token_endpoint_auth_method": "none",
615 }));
616
617 let response = state.request(request).await;
618 response.assert_status(StatusCode::CREATED);
619 let ClientRegistrationResponse { client_id, .. } = response.json();
620
621 let mut repo = state.repository().await.unwrap();
622 let user = repo
624 .user()
625 .add(&mut state.rng(), &state.clock, "alice".to_owned())
626 .await
627 .unwrap();
628
629 let mxid = state.homeserver_connection.mxid(&user.username);
630 state
631 .homeserver_connection
632 .provision_user(&ProvisionRequest::new(mxid, &user.sub))
633 .await
634 .unwrap();
635
636 let client = repo
637 .oauth2_client()
638 .find_by_client_id(&client_id)
639 .await
640 .unwrap()
641 .unwrap();
642
643 let browser_session = repo
644 .browser_session()
645 .add(&mut state.rng(), &state.clock, &user, None)
646 .await
647 .unwrap();
648
649 let session = repo
650 .oauth2_session()
651 .add_from_browser_session(
652 &mut state.rng(),
653 &state.clock,
654 &client,
655 &browser_session,
656 Scope::from_iter([OPENID]),
657 )
658 .await
659 .unwrap();
660
661 let (AccessToken { access_token, .. }, RefreshToken { refresh_token, .. }) =
662 generate_token_pair(
663 &mut state.rng(),
664 &state.clock,
665 &mut repo,
666 &session,
667 Duration::microseconds(5 * 60 * 1000 * 1000),
668 )
669 .await
670 .unwrap();
671
672 repo.save().await.unwrap();
673
674 let request = Request::post(OAuth2Introspection::PATH)
676 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
677 .form(json!({ "token": access_token }));
678 let response = state.request(request).await;
679 response.assert_status(StatusCode::OK);
680 let response: IntrospectionResponse = response.json();
681 assert!(response.active);
682 assert_eq!(response.username, Some("alice".to_owned()));
683 assert_eq!(response.client_id, Some(client_id.clone()));
684 assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken));
685 assert_eq!(response.scope, Some(Scope::from_iter([OPENID])));
686
687 let request = Request::post(OAuth2Introspection::PATH)
689 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
690 .form(json!({"token": access_token, "token_type_hint": "access_token"}));
691 let response = state.request(request).await;
692 response.assert_status(StatusCode::OK);
693 let response: IntrospectionResponse = response.json();
694 assert!(response.active);
695
696 let request = Request::post(OAuth2Introspection::PATH)
698 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
699 .form(json!({"token": access_token, "token_type_hint": "refresh_token"}));
700 let response = state.request(request).await;
701 response.assert_status(StatusCode::OK);
702 let response: IntrospectionResponse = response.json();
703 assert!(!response.active); let request = Request::post(OAuth2Introspection::PATH)
707 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
708 .form(json!({ "token": refresh_token }));
709 let response = state.request(request).await;
710 response.assert_status(StatusCode::OK);
711 let response: IntrospectionResponse = response.json();
712 assert!(response.active);
713 assert_eq!(response.username, Some("alice".to_owned()));
714 assert_eq!(response.client_id, Some(client_id.clone()));
715 assert_eq!(response.token_type, Some(OAuthTokenTypeHint::RefreshToken));
716 assert_eq!(response.scope, Some(Scope::from_iter([OPENID])));
717
718 let request = Request::post(OAuth2Introspection::PATH)
720 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
721 .form(json!({"token": refresh_token, "token_type_hint": "refresh_token"}));
722 let response = state.request(request).await;
723 response.assert_status(StatusCode::OK);
724 let response: IntrospectionResponse = response.json();
725 assert!(response.active);
726
727 let request = Request::post(OAuth2Introspection::PATH)
729 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
730 .form(json!({"token": refresh_token, "token_type_hint": "access_token"}));
731 let response = state.request(request).await;
732 response.assert_status(StatusCode::OK);
733 let response: IntrospectionResponse = response.json();
734 assert!(!response.active); state.activity_tracker.flush().await;
738 let mut repo = state.repository().await.unwrap();
739 let session = repo
740 .oauth2_session()
741 .lookup(session.id)
742 .await
743 .unwrap()
744 .unwrap();
745 assert_eq!(session.last_active_at, Some(state.clock.now()));
746
747 let access_token_lookup = repo
749 .oauth2_access_token()
750 .find_by_token(&access_token)
751 .await
752 .unwrap()
753 .unwrap();
754 assert!(access_token_lookup.is_used());
755 assert_eq!(access_token_lookup.first_used_at, Some(state.clock.now()));
756 repo.cancel().await.unwrap();
757
758 let old_now = state.clock.now();
760 state.clock.advance(Duration::try_hours(1).unwrap());
761
762 let request = Request::post(OAuth2Introspection::PATH)
763 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
764 .form(json!({ "token": access_token }));
765 let response = state.request(request).await;
766 response.assert_status(StatusCode::OK);
767 let response: IntrospectionResponse = response.json();
768 assert!(!response.active); state.activity_tracker.flush().await;
772 let mut repo = state.repository().await.unwrap();
773 let session = repo
774 .oauth2_session()
775 .lookup(session.id)
776 .await
777 .unwrap()
778 .unwrap();
779 assert_eq!(session.last_active_at, Some(old_now));
780 repo.cancel().await.unwrap();
781
782 let request = Request::post(OAuth2Introspection::PATH)
784 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
785 .form(json!({ "token": refresh_token }));
786 let response = state.request(request).await;
787 response.assert_status(StatusCode::OK);
788 let response: IntrospectionResponse = response.json();
789 assert!(response.active);
790
791 state.activity_tracker.flush().await;
793 let mut repo = state.repository().await.unwrap();
794 let session = repo
795 .oauth2_session()
796 .lookup(session.id)
797 .await
798 .unwrap()
799 .unwrap();
800 assert_eq!(session.last_active_at, Some(state.clock.now()));
801 repo.cancel().await.unwrap();
802 }
803
804 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
805 async fn test_introspect_compat_tokens(pool: PgPool) {
806 setup();
807 let state = TestState::from_pool(pool).await.unwrap();
808
809 let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
811 "client_uri": "https://introspecting.com/",
812 "grant_types": [],
813 "token_endpoint_auth_method": "client_secret_basic",
814 }));
815
816 let response = state.request(request).await;
817 response.assert_status(StatusCode::CREATED);
818 let client: ClientRegistrationResponse = response.json();
819 let introspecting_client_id = client.client_id;
820 let introspecting_client_secret = client.client_secret.unwrap();
821
822 let mut repo = state.repository().await.unwrap();
824 let user = repo
825 .user()
826 .add(&mut state.rng(), &state.clock, "alice".to_owned())
827 .await
828 .unwrap();
829
830 let mxid = state.homeserver_connection.mxid(&user.username);
831 state
832 .homeserver_connection
833 .provision_user(&ProvisionRequest::new(mxid, &user.sub))
834 .await
835 .unwrap();
836
837 let (version, hashed_password) = state
838 .password_manager
839 .hash(&mut state.rng(), Zeroizing::new(b"password".to_vec()))
840 .await
841 .unwrap();
842
843 repo.user_password()
844 .add(
845 &mut state.rng(),
846 &state.clock,
847 &user,
848 version,
849 hashed_password,
850 None,
851 )
852 .await
853 .unwrap();
854
855 repo.save().await.unwrap();
856
857 let request = Request::post("/_matrix/client/v3/login").json(json!({
859 "type": "m.login.password",
860 "refresh_token": true,
861 "identifier": {
862 "type": "m.id.user",
863 "user": "alice",
864 },
865 "password": "password",
866 }));
867 let response = state.request(request).await;
868 response.assert_status(StatusCode::OK);
869 let response: serde_json::Value = response.json();
870 let access_token = response["access_token"].as_str().unwrap();
871 let refresh_token = response["refresh_token"].as_str().unwrap();
872 let device_id = response["device_id"].as_str().unwrap();
873 let expected_scope: Scope =
874 format!("urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{device_id}")
875 .parse()
876 .unwrap();
877
878 let request = Request::post(OAuth2Introspection::PATH)
880 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
881 .form(json!({ "token": access_token }));
882 let response = state.request(request).await;
883 response.assert_status(StatusCode::OK);
884 let response: IntrospectionResponse = response.json();
885 assert!(response.active);
886 assert_eq!(response.username.as_deref(), Some("alice"));
887 assert_eq!(response.client_id.as_deref(), Some("legacy"));
888 assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken));
889 assert_eq!(response.scope.as_ref(), Some(&expected_scope));
890 assert_eq!(response.device_id.as_deref(), Some(device_id));
891
892 let request = Request::post(OAuth2Introspection::PATH)
895 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
896 .header("X-MAS-Supports-Device-Id", "1")
897 .form(json!({ "token": access_token }));
898 let response = state.request(request).await;
899 response.assert_status(StatusCode::OK);
900 let response: IntrospectionResponse = response.json();
901 assert!(response.active);
902 assert_eq!(response.username.as_deref(), Some("alice"));
903 assert_eq!(response.client_id.as_deref(), Some("legacy"));
904 assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken));
905 assert_eq!(
906 response.scope.map(|s| s.to_string()),
907 Some("urn:matrix:org.matrix.msc2967.client:api:*".to_owned())
908 );
909 assert_eq!(response.device_id.as_deref(), Some(device_id));
910
911 let request = Request::post(OAuth2Introspection::PATH)
913 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
914 .form(json!({"token": access_token, "token_type_hint": "access_token"}));
915 let response = state.request(request).await;
916 response.assert_status(StatusCode::OK);
917 let response: IntrospectionResponse = response.json();
918 assert!(response.active);
919
920 let request = Request::post(OAuth2Introspection::PATH)
922 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
923 .form(json!({"token": access_token, "token_type_hint": "refresh_token"}));
924 let response = state.request(request).await;
925 response.assert_status(StatusCode::OK);
926 let response: IntrospectionResponse = response.json();
927 assert!(!response.active); let request = Request::post(OAuth2Introspection::PATH)
931 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
932 .form(json!({ "token": refresh_token }));
933 let response = state.request(request).await;
934 response.assert_status(StatusCode::OK);
935 let response: IntrospectionResponse = response.json();
936 assert!(response.active);
937 assert_eq!(response.username.as_deref(), Some("alice"));
938 assert_eq!(response.client_id.as_deref(), Some("legacy"));
939 assert_eq!(response.token_type, Some(OAuthTokenTypeHint::RefreshToken));
940 assert_eq!(response.scope.as_ref(), Some(&expected_scope));
941 assert_eq!(response.device_id.as_deref(), Some(device_id));
942
943 let request = Request::post(OAuth2Introspection::PATH)
945 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
946 .form(json!({"token": refresh_token, "token_type_hint": "refresh_token"}));
947 let response = state.request(request).await;
948 response.assert_status(StatusCode::OK);
949 let response: IntrospectionResponse = response.json();
950 assert!(response.active);
951
952 let request = Request::post(OAuth2Introspection::PATH)
954 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
955 .form(json!({"token": refresh_token, "token_type_hint": "access_token"}));
956 let response = state.request(request).await;
957 response.assert_status(StatusCode::OK);
958 let response: IntrospectionResponse = response.json();
959 assert!(!response.active); state.clock.advance(Duration::try_hours(1).unwrap());
963
964 let request = Request::post(OAuth2Introspection::PATH)
965 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
966 .form(json!({ "token": access_token }));
967 let response = state.request(request).await;
968 response.assert_status(StatusCode::OK);
969 let response: IntrospectionResponse = response.json();
970 assert!(!response.active); let request = Request::post(OAuth2Introspection::PATH)
974 .basic_auth(&introspecting_client_id, &introspecting_client_secret)
975 .form(json!({ "token": refresh_token }));
976 let response = state.request(request).await;
977 response.assert_status(StatusCode::OK);
978 let response: IntrospectionResponse = response.json();
979 assert!(response.active);
980 }
981}