4
4
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
5
// Please see LICENSE files in the repository root for full details.
6
6
7
- use std:: sync:: LazyLock ;
7
+ use std:: sync:: { Arc , LazyLock } ;
8
8
9
9
use axum:: { Json , extract:: State , http:: HeaderValue , response:: IntoResponse } ;
10
10
use hyper:: { HeaderMap , StatusCode } ;
@@ -15,6 +15,7 @@ use mas_axum_utils::{
15
15
use mas_data_model:: { Device , TokenFormatError , TokenType } ;
16
16
use mas_iana:: oauth:: { OAuthClientAuthenticationMethod , OAuthTokenTypeHint } ;
17
17
use mas_keystore:: Encrypter ;
18
+ use mas_matrix:: HomeserverConnection ;
18
19
use mas_storage:: {
19
20
BoxClock , BoxRepository , Clock ,
20
21
compat:: { CompatAccessTokenRepository , CompatRefreshTokenRepository , CompatSessionRepository } ,
@@ -102,8 +103,14 @@ pub enum RouteError {
102
103
#[ error( "bad request" ) ]
103
104
BadRequest ,
104
105
106
+ #[ error( "failed to verify token" ) ]
107
+ FailedToVerifyToken ( #[ source] anyhow:: Error ) ,
108
+
105
109
#[ error( transparent) ]
106
110
ClientCredentialsVerification ( #[ from] CredentialsVerificationError ) ,
111
+
112
+ #[ error( "bearer token presented is invalid" ) ]
113
+ InvalidBearerToken ,
107
114
}
108
115
109
116
impl IntoResponse for RouteError {
@@ -114,13 +121,15 @@ impl IntoResponse for RouteError {
114
121
| Self :: CantLoadCompatSession ( _)
115
122
| Self :: CantLoadOAuthSession ( _)
116
123
| Self :: CantLoadUser ( _)
124
+ | Self :: FailedToVerifyToken ( _)
117
125
) ;
118
126
119
127
let response = match self {
120
128
e @ ( Self :: Internal ( _)
121
129
| Self :: CantLoadCompatSession ( _)
122
130
| Self :: CantLoadOAuthSession ( _)
123
- | Self :: CantLoadUser ( _) ) => (
131
+ | Self :: CantLoadUser ( _)
132
+ | Self :: FailedToVerifyToken ( _) ) => (
124
133
StatusCode :: INTERNAL_SERVER_ERROR ,
125
134
Json (
126
135
ClientError :: from ( ClientErrorCode :: ServerError ) . with_description ( e. to_string ( ) ) ,
@@ -140,6 +149,14 @@ impl IntoResponse for RouteError {
140
149
) ,
141
150
)
142
151
. 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 ( ) ,
143
160
144
161
Self :: UnknownToken ( _)
145
162
| Self :: UnexpectedTokenType
@@ -195,7 +212,7 @@ const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:adm
195
212
196
213
#[ tracing:: instrument(
197
214
name = "handlers.oauth2.introspection.post" ,
198
- fields( client. id = client_authorization . client_id( ) ) ,
215
+ fields( client. id = credentials . client_id( ) ) ,
199
216
skip_all,
200
217
) ]
201
218
#[ allow( clippy:: too_many_lines) ]
@@ -205,28 +222,41 @@ pub(crate) async fn post(
205
222
mut repo : BoxRepository ,
206
223
activity_tracker : ActivityTracker ,
207
224
State ( encrypter) : State < Encrypter > ,
225
+ State ( homeserver) : State < Arc < dyn HomeserverConnection > > ,
208
226
headers : HeaderMap ,
209
- client_authorization : ClientAuthorization < IntrospectionRequest > ,
227
+ ClientAuthorization { credentials , form } : ClientAuthorization < IntrospectionRequest > ,
210
228
) -> 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
+ // configuration 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 ) ;
220
238
}
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
+ } ;
223
253
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
+ }
228
258
229
- let Some ( form) = client_authorization . form else {
259
+ let Some ( form) = form else {
230
260
return Err ( RouteError :: BadRequest ) ;
231
261
} ;
232
262
@@ -578,10 +608,11 @@ mod tests {
578
608
use hyper:: { Request , StatusCode } ;
579
609
use mas_data_model:: { AccessToken , RefreshToken } ;
580
610
use mas_iana:: oauth:: OAuthTokenTypeHint ;
581
- use mas_matrix:: { HomeserverConnection , ProvisionRequest } ;
611
+ use mas_matrix:: { HomeserverConnection , MockHomeserverConnection , ProvisionRequest } ;
582
612
use mas_router:: { OAuth2Introspection , OAuth2RegistrationEndpoint , SimpleRoute } ;
583
613
use mas_storage:: Clock ;
584
614
use oauth2_types:: {
615
+ errors:: { ClientError , ClientErrorCode } ,
585
616
registration:: ClientRegistrationResponse ,
586
617
requests:: IntrospectionResponse ,
587
618
scope:: { OPENID , Scope } ,
@@ -984,4 +1015,29 @@ mod tests {
984
1015
let response: IntrospectionResponse = response. json ( ) ;
985
1016
assert ! ( response. active) ;
986
1017
}
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
+ }
987
1043
}
0 commit comments