@@ -15,7 +15,9 @@ use mas_axum_utils::{
1515 client_authorization:: { ClientAuthorization , CredentialsVerificationError } ,
1616 record_error,
1717} ;
18- use mas_data_model:: { BoxClock , Clock , Device , TokenFormatError , TokenType } ;
18+ use mas_data_model:: {
19+ BoxClock , Clock , Device , TokenFormatError , TokenType , personal:: session:: PersonalSessionOwner ,
20+ } ;
1921use mas_iana:: oauth:: { OAuthClientAuthenticationMethod , OAuthTokenTypeHint } ;
2022use mas_keystore:: Encrypter ;
2123use mas_matrix:: HomeserverConnection ;
@@ -93,6 +95,14 @@ pub enum RouteError {
9395 #[ error( "unknown compat session {0}" ) ]
9496 CantLoadCompatSession ( Ulid ) ,
9597
98+ /// The personal access token session is not valid.
99+ #[ error( "invalid personal access token session {0}" ) ]
100+ InvalidPersonalSession ( Ulid ) ,
101+
102+ /// The personal access token session could not be found in the database.
103+ #[ error( "unknown personal access token session {0}" ) ]
104+ CantLoadPersonalSession ( Ulid ) ,
105+
96106 /// The Device ID in the compat session can't be encoded as a scope
97107 #[ error( "device ID contains characters that are not allowed in a scope" ) ]
98108 CantEncodeDeviceID ( #[ from] mas_data_model:: ToScopeTokenError ) ,
@@ -103,6 +113,9 @@ pub enum RouteError {
103113 #[ error( "unknown user {0}" ) ]
104114 CantLoadUser ( Ulid ) ,
105115
116+ #[ error( "unknown OAuth2 client {0}" ) ]
117+ CantLoadOAuth2Client ( Ulid ) ,
118+
106119 #[ error( "bad request" ) ]
107120 BadRequest ,
108121
@@ -131,7 +144,9 @@ impl IntoResponse for RouteError {
131144 e @ ( Self :: Internal ( _)
132145 | Self :: CantLoadCompatSession ( _)
133146 | Self :: CantLoadOAuthSession ( _)
147+ | Self :: CantLoadPersonalSession ( _)
134148 | Self :: CantLoadUser ( _)
149+ | Self :: CantLoadOAuth2Client ( _)
135150 | Self :: FailedToVerifyToken ( _) ) => (
136151 StatusCode :: INTERNAL_SERVER_ERROR ,
137152 Json (
@@ -167,6 +182,7 @@ impl IntoResponse for RouteError {
167182 | Self :: InvalidUser ( _)
168183 | Self :: InvalidCompatSession ( _)
169184 | Self :: InvalidOAuthSession ( _)
185+ | Self :: InvalidPersonalSession ( _)
170186 | Self :: InvalidTokenFormat ( _)
171187 | Self :: CantEncodeDeviceID ( _) => {
172188 INTROSPECTION_COUNTER . add ( 1 , & [ KeyValue :: new ( ACTIVE . clone ( ) , false ) ] ) ;
@@ -627,8 +643,94 @@ pub(crate) async fn post(
627643 }
628644
629645 TokenType :: PersonalAccessToken => {
630- // TODO
631- return Err ( RouteError :: UnknownToken ( TokenType :: PersonalAccessToken ) ) ;
646+ let access_token = repo
647+ . personal_access_token ( )
648+ . find_by_token ( token)
649+ . await ?
650+ . ok_or ( RouteError :: UnknownToken ( TokenType :: AccessToken ) ) ?;
651+
652+ if !access_token. is_valid ( clock. now ( ) ) {
653+ return Err ( RouteError :: InvalidToken ( TokenType :: AccessToken ) ) ;
654+ }
655+
656+ let session = repo
657+ . personal_session ( )
658+ . lookup ( access_token. session_id )
659+ . await ?
660+ . ok_or ( RouteError :: CantLoadPersonalSession ( access_token. session_id ) ) ?;
661+
662+ if !session. is_valid ( ) {
663+ return Err ( RouteError :: InvalidPersonalSession ( session. id ) ) ;
664+ }
665+
666+ let actor_user = repo
667+ . user ( )
668+ . lookup ( session. actor_user_id )
669+ . await ?
670+ . ok_or ( RouteError :: CantLoadUser ( session. actor_user_id ) ) ?;
671+
672+ if !actor_user. is_valid ( ) {
673+ return Err ( RouteError :: InvalidUser ( actor_user. id ) ) ;
674+ }
675+
676+ let client_id = match session. owner {
677+ PersonalSessionOwner :: User ( owner_user_id) => {
678+ let owner_user = repo
679+ . user ( )
680+ . lookup ( owner_user_id)
681+ . await ?
682+ . ok_or ( RouteError :: CantLoadUser ( owner_user_id) ) ?;
683+
684+ if !owner_user. is_valid ( ) {
685+ return Err ( RouteError :: InvalidUser ( owner_user. id ) ) ;
686+ }
687+
688+ None
689+ }
690+ PersonalSessionOwner :: OAuth2Client ( owner_client_id) => {
691+ let owner_client = repo
692+ . oauth2_client ( )
693+ . lookup ( owner_client_id)
694+ . await ?
695+ . ok_or ( RouteError :: CantLoadOAuth2Client ( owner_client_id) ) ?;
696+
697+ // OAuth2 clients are always valid if they're in the database
698+ Some ( owner_client. client_id . clone ( ) )
699+ }
700+ } ;
701+
702+ activity_tracker
703+ . record_personal_access_token_session ( & clock, & session, ip)
704+ . await ;
705+
706+ INTROSPECTION_COUNTER . add (
707+ 1 ,
708+ & [
709+ KeyValue :: new ( KIND , "personal_access_token" ) ,
710+ KeyValue :: new ( ACTIVE , true ) ,
711+ ] ,
712+ ) ;
713+
714+ let scope = normalize_scope ( session. scope ) ;
715+
716+ IntrospectionResponse {
717+ active : true ,
718+ scope : Some ( scope) ,
719+ client_id,
720+ username : Some ( actor_user. username ) ,
721+ token_type : Some ( OAuthTokenTypeHint :: AccessToken ) ,
722+ exp : access_token. expires_at ,
723+ expires_in : access_token
724+ . expires_at
725+ . map ( |expires_at| expires_at. signed_duration_since ( clock. now ( ) ) ) ,
726+ iat : Some ( access_token. created_at ) ,
727+ nbf : Some ( access_token. created_at ) ,
728+ sub : Some ( actor_user. sub ) ,
729+ aud : None ,
730+ iss : None ,
731+ jti : None ,
732+ device_id : None ,
733+ }
632734 }
633735 } ;
634736
@@ -641,7 +743,9 @@ pub(crate) async fn post(
641743mod tests {
642744 use chrono:: Duration ;
643745 use hyper:: { Request , StatusCode } ;
644- use mas_data_model:: { AccessToken , Clock , RefreshToken } ;
746+ use mas_data_model:: {
747+ AccessToken , Clock , RefreshToken , TokenType , personal:: session:: PersonalSessionOwner ,
748+ } ;
645749 use mas_iana:: oauth:: OAuthTokenTypeHint ;
646750 use mas_matrix:: { HomeserverConnection , MockHomeserverConnection , ProvisionRequest } ;
647751 use mas_router:: { OAuth2Introspection , OAuth2RegistrationEndpoint , SimpleRoute } ;
@@ -1074,4 +1178,125 @@ mod tests {
10741178 let response: ClientError = response. json ( ) ;
10751179 assert_eq ! ( response. error, ClientErrorCode :: AccessDenied ) ;
10761180 }
1181+
1182+ #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
1183+ async fn test_introspect_personal_access_tokens ( pool : PgPool ) {
1184+ setup ( ) ;
1185+ let state = TestState :: from_pool ( pool) . await . unwrap ( ) ;
1186+
1187+ // Provision a client which will be used to do introspection requests
1188+ let request = Request :: post ( OAuth2RegistrationEndpoint :: PATH ) . json ( json ! ( {
1189+ "client_uri" : "https://introspecting.com/" ,
1190+ "grant_types" : [ ] ,
1191+ "token_endpoint_auth_method" : "client_secret_basic" ,
1192+ } ) ) ;
1193+
1194+ let response = state. request ( request) . await ;
1195+ response. assert_status ( StatusCode :: CREATED ) ;
1196+ let client: ClientRegistrationResponse = response. json ( ) ;
1197+ let introspecting_client_id = client. client_id ;
1198+ let introspecting_client_secret = client. client_secret . unwrap ( ) ;
1199+
1200+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1201+
1202+ // Provision an owner user (who provisions the personal session)
1203+ let owner_user = repo
1204+ . user ( )
1205+ . add ( & mut state. rng ( ) , & state. clock , "admin" . to_owned ( ) )
1206+ . await
1207+ . unwrap ( ) ;
1208+
1209+ // Provision an actor user (which the token represents)
1210+ let actor_user = repo
1211+ . user ( )
1212+ . add ( & mut state. rng ( ) , & state. clock , "bruce" . to_owned ( ) )
1213+ . await
1214+ . unwrap ( ) ;
1215+
1216+ // admin creates a personal session to control bruce's account
1217+ let personal_session = repo
1218+ . personal_session ( )
1219+ . add (
1220+ & mut state. rng ( ) ,
1221+ & state. clock ,
1222+ PersonalSessionOwner :: User ( owner_user. id ) ,
1223+ & actor_user,
1224+ "Test Personal Access Token" . to_owned ( ) ,
1225+ Scope :: from_iter ( [ OPENID ] ) ,
1226+ )
1227+ . await
1228+ . unwrap ( ) ;
1229+
1230+ // Generate a personal access token with proper token format
1231+ let token_string = TokenType :: PersonalAccessToken . generate ( & mut state. rng ( ) ) ;
1232+ let _personal_access_token = repo
1233+ . personal_access_token ( )
1234+ . add (
1235+ & mut state. rng ( ) ,
1236+ & state. clock ,
1237+ & personal_session,
1238+ & token_string,
1239+ Some ( Duration :: try_hours ( 1 ) . unwrap ( ) ) ,
1240+ )
1241+ . await
1242+ . unwrap ( ) ;
1243+
1244+ repo. save ( ) . await . unwrap ( ) ;
1245+
1246+ // Now that we have a personal access token, we can introspect it
1247+ let request = Request :: post ( OAuth2Introspection :: PATH )
1248+ . basic_auth ( & introspecting_client_id, & introspecting_client_secret)
1249+ . form ( json ! ( { "token" : token_string } ) ) ;
1250+ let response = state. request ( request) . await ;
1251+ response. assert_status ( StatusCode :: OK ) ;
1252+ let response: IntrospectionResponse = response. json ( ) ;
1253+ assert ! ( response. active) ;
1254+ // Actor user
1255+ assert_eq ! ( response. username, Some ( "bruce" . to_owned( ) ) ) ;
1256+ // Not owned by a client
1257+ assert_eq ! ( response. client_id, None ) ;
1258+ assert_eq ! ( response. token_type, Some ( OAuthTokenTypeHint :: AccessToken ) ) ;
1259+ assert_eq ! ( response. scope, Some ( Scope :: from_iter( [ OPENID ] ) ) ) ;
1260+
1261+ // Do the same request, but with a token_type_hint
1262+ let last_active = state. clock . now ( ) ;
1263+ let request = Request :: post ( OAuth2Introspection :: PATH )
1264+ . basic_auth ( & introspecting_client_id, & introspecting_client_secret)
1265+ . form ( json ! ( { "token" : token_string, "token_type_hint" : "access_token" } ) ) ;
1266+ let response = state. request ( request) . await ;
1267+ response. assert_status ( StatusCode :: OK ) ;
1268+ let response: IntrospectionResponse = response. json ( ) ;
1269+ assert ! ( response. active) ;
1270+
1271+ // Do the same request, but with the wrong token_type_hint
1272+ let request = Request :: post ( OAuth2Introspection :: PATH )
1273+ . basic_auth ( & introspecting_client_id, & introspecting_client_secret)
1274+ . form ( json ! ( { "token" : token_string, "token_type_hint" : "refresh_token" } ) ) ;
1275+ let response = state. request ( request) . await ;
1276+ response. assert_status ( StatusCode :: OK ) ;
1277+ let response: IntrospectionResponse = response. json ( ) ;
1278+ assert ! ( !response. active) ; // It shouldn't be active with wrong hint
1279+
1280+ // Advance the clock to invalidate the access token
1281+ state. clock . advance ( Duration :: try_hours ( 2 ) . unwrap ( ) ) ;
1282+
1283+ let request = Request :: post ( OAuth2Introspection :: PATH )
1284+ . basic_auth ( & introspecting_client_id, & introspecting_client_secret)
1285+ . form ( json ! ( { "token" : token_string } ) ) ;
1286+ let response = state. request ( request) . await ;
1287+ response. assert_status ( StatusCode :: OK ) ;
1288+ let response: IntrospectionResponse = response. json ( ) ;
1289+ assert ! ( !response. active) ; // It shouldn't be active anymore
1290+
1291+ state. activity_tracker . flush ( ) . await ;
1292+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1293+ let session = repo
1294+ . personal_session ( )
1295+ . lookup ( personal_session. id )
1296+ . await
1297+ . unwrap ( )
1298+ . unwrap ( ) ;
1299+ assert_eq ! ( session. last_active_at, Some ( last_active) ) ;
1300+ repo. save ( ) . await . unwrap ( ) ;
1301+ }
10771302}
0 commit comments