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