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