@@ -39,7 +39,7 @@ use oauth2_types::{
3939 scope,
4040} ;
4141use thiserror:: Error ;
42- use tracing:: debug;
42+ use tracing:: { debug, info } ;
4343use ulid:: Ulid ;
4444
4545use super :: { generate_id_token, generate_token_pair} ;
@@ -98,6 +98,20 @@ pub(crate) enum RouteError {
9898 #[ error( "failed to load oauth session" ) ]
9999 NoSuchOAuthSession ,
100100
101+ #[ error(
102+ "failed to load the next refresh token ({next:?}) from the previous one ({previous:?})"
103+ ) ]
104+ NoSuchNextRefreshToken { next : Ulid , previous : Ulid } ,
105+
106+ #[ error( "failed to load the access token ({access_token:?}) associated with the next refresh token ({refresh_token:?})" ) ]
107+ NoSuchNextAccessToken {
108+ access_token : Ulid ,
109+ refresh_token : Ulid ,
110+ } ,
111+
112+ #[ error( "no access token associated with the refresh token {refresh_token:?}" ) ]
113+ NoAccessTokenOnRefreshToken { refresh_token : Ulid } ,
114+
101115 #[ error( "device code grant expired" ) ]
102116 DeviceCodeExpired ,
103117
@@ -122,7 +136,10 @@ impl IntoResponse for RouteError {
122136 Self :: Internal ( _)
123137 | Self :: NoSuchBrowserSession
124138 | Self :: NoSuchOAuthSession
125- | Self :: ProvisionDeviceFailed ( _) => (
139+ | Self :: ProvisionDeviceFailed ( _)
140+ | Self :: NoSuchNextRefreshToken { .. }
141+ | Self :: NoSuchNextAccessToken { .. }
142+ | Self :: NoAccessTokenOnRefreshToken { .. } => (
126143 StatusCode :: INTERNAL_SERVER_ERROR ,
127144 Json ( ClientError :: from ( ClientErrorCode :: ServerError ) ) ,
128145 ) ,
@@ -482,6 +499,7 @@ async fn authorization_code_grant(
482499 Ok ( ( params, repo) )
483500}
484501
502+ #[ allow( clippy:: too_many_lines) ]
485503async fn refresh_token_grant (
486504 rng : & mut BoxRng ,
487505 clock : & impl Clock ,
@@ -518,10 +536,6 @@ async fn refresh_token_grant(
518536 . await ?;
519537 }
520538
521- if !refresh_token. is_valid ( ) {
522- return Err ( RouteError :: RefreshTokenInvalid ( refresh_token. id ) ) ;
523- }
524-
525539 if !session. is_valid ( ) {
526540 return Err ( RouteError :: SessionInvalid ( session. id ) ) ;
527541 }
@@ -534,6 +548,77 @@ async fn refresh_token_grant(
534548 } ) ;
535549 }
536550
551+ if !refresh_token. is_valid ( ) {
552+ // We're seing a refresh token that already has been consumed, this might be a
553+ // double-refresh or a replay attack
554+
555+ // First, get the next refresh token
556+ let Some ( next_refresh_token_id) = refresh_token. next_refresh_token_id ( ) else {
557+ // If we don't have a 'next' refresh token, it may just be because this was
558+ // before we were recording those. Let's just treat it as a replay.
559+ return Err ( RouteError :: RefreshTokenInvalid ( refresh_token. id ) ) ;
560+ } ;
561+
562+ let Some ( next_refresh_token) = repo
563+ . oauth2_refresh_token ( )
564+ . lookup ( next_refresh_token_id)
565+ . await ?
566+ else {
567+ return Err ( RouteError :: NoSuchNextRefreshToken {
568+ next : next_refresh_token_id,
569+ previous : refresh_token. id ,
570+ } ) ;
571+ } ;
572+
573+ // Check if the next refresh token was already consumed or not
574+ if !next_refresh_token. is_valid ( ) {
575+ // XXX: This is a replay, we *may* want to invalidate the session
576+ return Err ( RouteError :: RefreshTokenInvalid ( next_refresh_token. id ) ) ;
577+ }
578+
579+ // Check if the associated access token was already used
580+ let Some ( access_token_id) = next_refresh_token. access_token_id else {
581+ // This should in theory not happen: this means an access token got cleaned up,
582+ // but the refresh token was still valid.
583+ return Err ( RouteError :: NoAccessTokenOnRefreshToken {
584+ refresh_token : next_refresh_token. id ,
585+ } ) ;
586+ } ;
587+
588+ // Load it
589+ let next_access_token = repo
590+ . oauth2_access_token ( )
591+ . lookup ( access_token_id)
592+ . await ?
593+ . ok_or ( RouteError :: NoSuchNextAccessToken {
594+ access_token : access_token_id,
595+ refresh_token : next_refresh_token_id,
596+ } ) ?;
597+
598+ if next_access_token. is_used ( ) {
599+ // XXX: This is a replay, we *may* want to invalidate the session
600+ return Err ( RouteError :: RefreshTokenInvalid ( next_refresh_token. id ) ) ;
601+ }
602+
603+ // Looks like it's a double-refresh, client lost their refresh token on
604+ // the way back. Let's revoke the two new access and refresh tokens, and
605+ // issue new ones
606+ info ! (
607+ oauth2_session. id = %session. id,
608+ oauth2_client. id = %client. id,
609+ %refresh_token. id,
610+ "A refresh token was used twice, but the new refresh token was lost. Revoking the old ones and issuing new ones."
611+ ) ;
612+
613+ repo. oauth2_access_token ( )
614+ . revoke ( clock, next_access_token)
615+ . await ?;
616+
617+ repo. oauth2_refresh_token ( )
618+ . revoke ( clock, next_refresh_token)
619+ . await ?;
620+ }
621+
537622 activity_tracker
538623 . record_oauth2_session ( clock, & session)
539624 . await ;
@@ -550,9 +635,12 @@ async fn refresh_token_grant(
550635 if let Some ( access_token_id) = refresh_token. access_token_id {
551636 let access_token = repo. oauth2_access_token ( ) . lookup ( access_token_id) . await ?;
552637 if let Some ( access_token) = access_token {
553- repo. oauth2_access_token ( )
554- . revoke ( clock, access_token)
555- . await ?;
638+ // If it is a double-refresh, it might already be revoked
639+ if !access_token. state . is_revoked ( ) {
640+ repo. oauth2_access_token ( )
641+ . revoke ( clock, access_token)
642+ . await ?;
643+ }
556644 }
557645 }
558646
@@ -1123,6 +1211,175 @@ mod tests {
11231211 let _: AccessTokenResponse = response. json ( ) ;
11241212 }
11251213
1214+ #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
1215+ async fn test_double_refresh ( pool : PgPool ) {
1216+ setup ( ) ;
1217+ let state = TestState :: from_pool ( pool) . await . unwrap ( ) ;
1218+
1219+ // Provision a client
1220+ let request =
1221+ Request :: post ( mas_router:: OAuth2RegistrationEndpoint :: PATH ) . json ( serde_json:: json!( {
1222+ "client_uri" : "https://example.com/" ,
1223+ "redirect_uris" : [ "https://example.com/callback" ] ,
1224+ "token_endpoint_auth_method" : "none" ,
1225+ "response_types" : [ "code" ] ,
1226+ "grant_types" : [ "authorization_code" , "refresh_token" ] ,
1227+ } ) ) ;
1228+
1229+ let response = state. request ( request) . await ;
1230+ response. assert_status ( StatusCode :: CREATED ) ;
1231+
1232+ let ClientRegistrationResponse { client_id, .. } = response. json ( ) ;
1233+
1234+ // Let's provision a user and create a session for them. This part is hard to
1235+ // test with just HTTP requests, so we'll use the repository directly.
1236+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1237+
1238+ let user = repo
1239+ . user ( )
1240+ . add ( & mut state. rng ( ) , & state. clock , "alice" . to_owned ( ) )
1241+ . await
1242+ . unwrap ( ) ;
1243+
1244+ let browser_session = repo
1245+ . browser_session ( )
1246+ . add ( & mut state. rng ( ) , & state. clock , & user, None )
1247+ . await
1248+ . unwrap ( ) ;
1249+
1250+ // Lookup the client in the database.
1251+ let client = repo
1252+ . oauth2_client ( )
1253+ . find_by_client_id ( & client_id)
1254+ . await
1255+ . unwrap ( )
1256+ . unwrap ( ) ;
1257+
1258+ // Get a token pair
1259+ let session = repo
1260+ . oauth2_session ( )
1261+ . add_from_browser_session (
1262+ & mut state. rng ( ) ,
1263+ & state. clock ,
1264+ & client,
1265+ & browser_session,
1266+ Scope :: from_iter ( [ OPENID ] ) ,
1267+ )
1268+ . await
1269+ . unwrap ( ) ;
1270+
1271+ let ( AccessToken { access_token, .. } , RefreshToken { refresh_token, .. } ) =
1272+ generate_token_pair (
1273+ & mut state. rng ( ) ,
1274+ & state. clock ,
1275+ & mut repo,
1276+ & session,
1277+ Duration :: microseconds ( 5 * 60 * 1000 * 1000 ) ,
1278+ )
1279+ . await
1280+ . unwrap ( ) ;
1281+
1282+ repo. save ( ) . await . unwrap ( ) ;
1283+
1284+ // First check that the token is valid
1285+ assert ! ( state. is_access_token_valid( & access_token) . await ) ;
1286+
1287+ // Now call the token endpoint to get an access token.
1288+ let request =
1289+ Request :: post ( mas_router:: OAuth2TokenEndpoint :: PATH ) . form ( serde_json:: json!( {
1290+ "grant_type" : "refresh_token" ,
1291+ "refresh_token" : refresh_token,
1292+ "client_id" : client. client_id,
1293+ } ) ) ;
1294+
1295+ let first_response = state. request ( request) . await ;
1296+ first_response. assert_status ( StatusCode :: OK ) ;
1297+ let first_response: AccessTokenResponse = first_response. json ( ) ;
1298+
1299+ // Call a second time, it should work, as we haven't done anything yet with the
1300+ // token
1301+ let request =
1302+ Request :: post ( mas_router:: OAuth2TokenEndpoint :: PATH ) . form ( serde_json:: json!( {
1303+ "grant_type" : "refresh_token" ,
1304+ "refresh_token" : refresh_token,
1305+ "client_id" : client. client_id,
1306+ } ) ) ;
1307+
1308+ let second_response = state. request ( request) . await ;
1309+ second_response. assert_status ( StatusCode :: OK ) ;
1310+ let second_response: AccessTokenResponse = second_response. json ( ) ;
1311+
1312+ // Check that we got new tokens
1313+ assert_ne ! ( first_response. access_token, second_response. access_token) ;
1314+ assert_ne ! ( first_response. refresh_token, second_response. refresh_token) ;
1315+
1316+ // Check that the old-new token is invalid
1317+ assert ! (
1318+ !state
1319+ . is_access_token_valid( & first_response. access_token)
1320+ . await
1321+ ) ;
1322+
1323+ // Check that the new-new token is valid
1324+ assert ! (
1325+ state
1326+ . is_access_token_valid( & second_response. access_token)
1327+ . await
1328+ ) ;
1329+
1330+ // Do a third refresh, this one should not work, as we've used the new
1331+ // access token
1332+ let request =
1333+ Request :: post ( mas_router:: OAuth2TokenEndpoint :: PATH ) . form ( serde_json:: json!( {
1334+ "grant_type" : "refresh_token" ,
1335+ "refresh_token" : refresh_token,
1336+ "client_id" : client. client_id,
1337+ } ) ) ;
1338+
1339+ let third_response = state. request ( request) . await ;
1340+ third_response. assert_status ( StatusCode :: BAD_REQUEST ) ;
1341+
1342+ // The other reason we consider a new refresh token to be 'used' is if
1343+ // it was already used in a refresh
1344+ // So, if we do a refresh with the second_response.refresh_token, then
1345+ // another refresh with the result, redoing one with
1346+ // second_response.refresh_token again should fail
1347+ let request =
1348+ Request :: post ( mas_router:: OAuth2TokenEndpoint :: PATH ) . form ( serde_json:: json!( {
1349+ "grant_type" : "refresh_token" ,
1350+ "refresh_token" : second_response. refresh_token,
1351+ "client_id" : client. client_id,
1352+ } ) ) ;
1353+
1354+ // This one is fine
1355+ let fourth_response = state. request ( request) . await ;
1356+ fourth_response. assert_status ( StatusCode :: OK ) ;
1357+ let fourth_response: AccessTokenResponse = fourth_response. json ( ) ;
1358+
1359+ // Do another one, it should be fine as well
1360+ let request =
1361+ Request :: post ( mas_router:: OAuth2TokenEndpoint :: PATH ) . form ( serde_json:: json!( {
1362+ "grant_type" : "refresh_token" ,
1363+ "refresh_token" : fourth_response. refresh_token,
1364+ "client_id" : client. client_id,
1365+ } ) ) ;
1366+
1367+ let fifth_response = state. request ( request) . await ;
1368+ fifth_response. assert_status ( StatusCode :: OK ) ;
1369+
1370+ // But now, if we re-do with the second_response.refresh_token, it should
1371+ // fail
1372+ let request =
1373+ Request :: post ( mas_router:: OAuth2TokenEndpoint :: PATH ) . form ( serde_json:: json!( {
1374+ "grant_type" : "refresh_token" ,
1375+ "refresh_token" : second_response. refresh_token,
1376+ "client_id" : client. client_id,
1377+ } ) ) ;
1378+
1379+ let sixth_response = state. request ( request) . await ;
1380+ sixth_response. assert_status ( StatusCode :: BAD_REQUEST ) ;
1381+ }
1382+
11261383 #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
11271384 async fn test_client_credentials ( pool : PgPool ) {
11281385 setup ( ) ;
0 commit comments