44// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55// Please see LICENSE files in the repository root for full details.
66
7- use std:: sync:: LazyLock ;
7+ use std:: sync:: { Arc , LazyLock } ;
88
99use axum:: { Json , extract:: State , http:: HeaderValue , response:: IntoResponse } ;
1010use hyper:: { HeaderMap , StatusCode } ;
@@ -15,6 +15,7 @@ use mas_axum_utils::{
1515use mas_data_model:: { Device , TokenFormatError , TokenType } ;
1616use mas_iana:: oauth:: { OAuthClientAuthenticationMethod , OAuthTokenTypeHint } ;
1717use mas_keystore:: Encrypter ;
18+ use mas_matrix:: HomeserverConnection ;
1819use mas_storage:: {
1920 BoxClock , BoxRepository , Clock ,
2021 compat:: { CompatAccessTokenRepository , CompatRefreshTokenRepository , CompatSessionRepository } ,
@@ -102,8 +103,14 @@ pub enum RouteError {
102103 #[ error( "bad request" ) ]
103104 BadRequest ,
104105
106+ #[ error( "failed to verify token" ) ]
107+ FailedToVerifyToken ( #[ source] anyhow:: Error ) ,
108+
105109 #[ error( transparent) ]
106110 ClientCredentialsVerification ( #[ from] CredentialsVerificationError ) ,
111+
112+ #[ error( "bearer token presented is invalid" ) ]
113+ InvalidBearerToken ,
107114}
108115
109116impl IntoResponse for RouteError {
@@ -114,13 +121,15 @@ impl IntoResponse for RouteError {
114121 | Self :: CantLoadCompatSession ( _)
115122 | Self :: CantLoadOAuthSession ( _)
116123 | Self :: CantLoadUser ( _)
124+ | Self :: FailedToVerifyToken ( _)
117125 ) ;
118126
119127 let response = match self {
120128 e @ ( Self :: Internal ( _)
121129 | Self :: CantLoadCompatSession ( _)
122130 | Self :: CantLoadOAuthSession ( _)
123- | Self :: CantLoadUser ( _) ) => (
131+ | Self :: CantLoadUser ( _)
132+ | Self :: FailedToVerifyToken ( _) ) => (
124133 StatusCode :: INTERNAL_SERVER_ERROR ,
125134 Json (
126135 ClientError :: from ( ClientErrorCode :: ServerError ) . with_description ( e. to_string ( ) ) ,
@@ -140,6 +149,14 @@ impl IntoResponse for RouteError {
140149 ) ,
141150 )
142151 . into_response ( ) ,
152+ e @ Self :: InvalidBearerToken => (
153+ StatusCode :: UNAUTHORIZED ,
154+ Json (
155+ ClientError :: from ( ClientErrorCode :: AccessDenied )
156+ . with_description ( e. to_string ( ) ) ,
157+ ) ,
158+ )
159+ . into_response ( ) ,
143160
144161 Self :: UnknownToken ( _)
145162 | Self :: UnexpectedTokenType
@@ -195,7 +212,7 @@ const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:adm
195212
196213#[ tracing:: instrument(
197214 name = "handlers.oauth2.introspection.post" ,
198- fields( client. id = client_authorization . client_id( ) ) ,
215+ fields( client. id = credentials . client_id( ) ) ,
199216 skip_all,
200217) ]
201218#[ allow( clippy:: too_many_lines) ]
@@ -205,28 +222,41 @@ pub(crate) async fn post(
205222 mut repo : BoxRepository ,
206223 activity_tracker : ActivityTracker ,
207224 State ( encrypter) : State < Encrypter > ,
225+ State ( homeserver) : State < Arc < dyn HomeserverConnection > > ,
208226 headers : HeaderMap ,
209- client_authorization : ClientAuthorization < IntrospectionRequest > ,
227+ ClientAuthorization { credentials , form } : ClientAuthorization < IntrospectionRequest > ,
210228) -> 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 ) ) ;
229+ if let Some ( token ) = credentials . bearer_token ( ) {
230+ // If the client presented a bearer token, we check with the homeserver
231+ // connection if it is allowed to use the introspection endpoint
232+ if !homeserver
233+ . verify_token ( token )
234+ . await
235+ . map_err ( RouteError :: FailedToVerifyToken ) ?
236+ {
237+ return Err ( RouteError :: InvalidBearerToken ) ;
220238 }
221- Some ( c) => c,
222- } ;
239+ } else {
240+ // Otherwise, it presented regular client credentials, so we verify them
241+ let client = credentials
242+ . fetch ( & mut repo)
243+ . await ?
244+ . ok_or ( RouteError :: ClientNotFound ) ?;
245+
246+ // Only confidential clients are allowed to introspect
247+ let method = match & client. token_endpoint_auth_method {
248+ None | Some ( OAuthClientAuthenticationMethod :: None ) => {
249+ return Err ( RouteError :: NotAllowed ( client. id ) ) ;
250+ }
251+ Some ( c) => c,
252+ } ;
223253
224- client_authorization
225- . credentials
226- . verify ( & http_client , & encrypter , method , & client )
227- . await ? ;
254+ credentials
255+ . verify ( & http_client , & encrypter , method , & client )
256+ . await ? ;
257+ }
228258
229- let Some ( form) = client_authorization . form else {
259+ let Some ( form) = form else {
230260 return Err ( RouteError :: BadRequest ) ;
231261 } ;
232262
@@ -578,10 +608,11 @@ mod tests {
578608 use hyper:: { Request , StatusCode } ;
579609 use mas_data_model:: { AccessToken , RefreshToken } ;
580610 use mas_iana:: oauth:: OAuthTokenTypeHint ;
581- use mas_matrix:: { HomeserverConnection , ProvisionRequest } ;
611+ use mas_matrix:: { HomeserverConnection , MockHomeserverConnection , ProvisionRequest } ;
582612 use mas_router:: { OAuth2Introspection , OAuth2RegistrationEndpoint , SimpleRoute } ;
583613 use mas_storage:: Clock ;
584614 use oauth2_types:: {
615+ errors:: { ClientError , ClientErrorCode } ,
585616 registration:: ClientRegistrationResponse ,
586617 requests:: IntrospectionResponse ,
587618 scope:: { OPENID , Scope } ,
@@ -984,4 +1015,29 @@ mod tests {
9841015 let response: IntrospectionResponse = response. json ( ) ;
9851016 assert ! ( response. active) ;
9861017 }
1018+
1019+ #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
1020+ async fn test_introspect_with_bearer_token ( pool : PgPool ) {
1021+ setup ( ) ;
1022+ let state = TestState :: from_pool ( pool) . await . unwrap ( ) ;
1023+
1024+ // Check that talking to the introspection endpoint with the bearer token from
1025+ // the MockHomeserverConnection doens't error out
1026+ let request = Request :: post ( OAuth2Introspection :: PATH )
1027+ . bearer ( MockHomeserverConnection :: VALID_BEARER_TOKEN )
1028+ . form ( json ! ( { "token" : "some_token" } ) ) ;
1029+ let response = state. request ( request) . await ;
1030+ response. assert_status ( StatusCode :: OK ) ;
1031+ let response: IntrospectionResponse = response. json ( ) ;
1032+ assert ! ( !response. active) ;
1033+
1034+ // Check with another token, we should get a 401
1035+ let request = Request :: post ( OAuth2Introspection :: PATH )
1036+ . bearer ( "another_token" )
1037+ . form ( json ! ( { "token" : "some_token" } ) ) ;
1038+ let response = state. request ( request) . await ;
1039+ response. assert_status ( StatusCode :: UNAUTHORIZED ) ;
1040+ let response: ClientError = response. json ( ) ;
1041+ assert_eq ! ( response. error, ClientErrorCode :: AccessDenied ) ;
1042+ }
9871043}
0 commit comments