@@ -1176,17 +1176,22 @@ impl AuthorizationManager {
11761176 . ok_or_else ( || AuthError :: InternalError ( "OAuth client not configured" . to_string ( ) ) ) ?;
11771177
11781178 let stored = self . credential_store . load ( ) . await ?;
1179- let current_credentials = stored
1180- . and_then ( |s| s. token_response )
1181- . ok_or_else ( || AuthError :: AuthorizationRequired ) ?;
1179+ let stored_credentials = stored. ok_or ( AuthError :: AuthorizationRequired ) ?;
1180+ let current_credentials = stored_credentials
1181+ . token_response
1182+ . ok_or ( AuthError :: AuthorizationRequired ) ?;
11821183
11831184 let refresh_token = current_credentials. refresh_token ( ) . ok_or_else ( || {
11841185 AuthError :: TokenRefreshFailed ( "No refresh token available" . to_string ( ) )
11851186 } ) ?;
11861187 debug ! ( "refresh token present, attempting refresh" ) ;
11871188
1188- let token_result = oauth_client
1189- . exchange_refresh_token ( & RefreshToken :: new ( refresh_token. secret ( ) . to_string ( ) ) )
1189+ let refresh_token_value = RefreshToken :: new ( refresh_token. secret ( ) . to_string ( ) ) ;
1190+ let mut refresh_request = oauth_client. exchange_refresh_token ( & refresh_token_value) ;
1191+ for scope in & stored_credentials. granted_scopes {
1192+ refresh_request = refresh_request. add_scope ( Scope :: new ( scope. clone ( ) ) ) ;
1193+ }
1194+ let token_result = refresh_request
11901195 . request_async ( & OAuthReqwestClient ( self . http_client . clone ( ) ) )
11911196 . await
11921197 . map_err ( |e| AuthError :: TokenRefreshFailed ( e. to_string ( ) ) ) ?;
@@ -3580,4 +3585,170 @@ mod tests {
35803585 "io.modelcontextprotocol/oauth-client-credentials"
35813586 ) ;
35823587 }
3588+
3589+ // -- refresh_token --
3590+
3591+ fn make_token_response_with_refresh (
3592+ access_token : & str ,
3593+ refresh_token : & str ,
3594+ ) -> OAuthTokenResponse {
3595+ use oauth2:: RefreshToken ;
3596+ let mut resp = make_token_response ( access_token, Some ( 3600 ) ) ;
3597+ resp. set_refresh_token ( Some ( RefreshToken :: new ( refresh_token. to_string ( ) ) ) ) ;
3598+ resp
3599+ }
3600+
3601+ #[ tokio:: test]
3602+ async fn refresh_token_returns_error_when_no_stored_credentials ( ) {
3603+ let mut manager = manager_with_metadata ( None ) . await ;
3604+ manager. configure_client ( test_client_config ( ) ) . unwrap ( ) ;
3605+
3606+ let err = manager. refresh_token ( ) . await . unwrap_err ( ) ;
3607+ assert ! (
3608+ matches!( err, AuthError :: AuthorizationRequired ) ,
3609+ "expected AuthorizationRequired when no credentials stored, got: {err:?}"
3610+ ) ;
3611+ }
3612+
3613+ #[ tokio:: test]
3614+ async fn refresh_token_returns_error_when_no_token_response ( ) {
3615+ let mut manager = manager_with_metadata ( None ) . await ;
3616+ manager. configure_client ( test_client_config ( ) ) . unwrap ( ) ;
3617+
3618+ let stored = StoredCredentials {
3619+ client_id : "my-client" . to_string ( ) ,
3620+ token_response : None ,
3621+ granted_scopes : vec ! [ ] ,
3622+ token_received_at : None ,
3623+ } ;
3624+ manager. credential_store . save ( stored) . await . unwrap ( ) ;
3625+
3626+ let err = manager. refresh_token ( ) . await . unwrap_err ( ) ;
3627+ assert ! (
3628+ matches!( err, AuthError :: AuthorizationRequired ) ,
3629+ "expected AuthorizationRequired when token_response is None, got: {err:?}"
3630+ ) ;
3631+ }
3632+
3633+ #[ tokio:: test]
3634+ async fn refresh_token_returns_error_when_no_refresh_token ( ) {
3635+ let mut manager = manager_with_metadata ( None ) . await ;
3636+ manager. configure_client ( test_client_config ( ) ) . unwrap ( ) ;
3637+
3638+ let stored = StoredCredentials {
3639+ client_id : "my-client" . to_string ( ) ,
3640+ token_response : Some ( make_token_response ( "old-token" , Some ( 3600 ) ) ) ,
3641+ granted_scopes : vec ! [ ] ,
3642+ token_received_at : Some ( AuthorizationManager :: now_epoch_secs ( ) ) ,
3643+ } ;
3644+ manager. credential_store . save ( stored) . await . unwrap ( ) ;
3645+
3646+ let err = manager. refresh_token ( ) . await . unwrap_err ( ) ;
3647+ assert ! (
3648+ matches!( err, AuthError :: TokenRefreshFailed ( _) ) ,
3649+ "expected TokenRefreshFailed when no refresh token, got: {err:?}"
3650+ ) ;
3651+ }
3652+
3653+ async fn start_token_server ( ) -> ( String , Arc < std:: sync:: Mutex < Option < String > > > ) {
3654+ use axum:: { Router , body:: Body , http:: Response , routing:: post} ;
3655+ let captured: Arc < std:: sync:: Mutex < Option < String > > > = Arc :: new ( std:: sync:: Mutex :: new ( None ) ) ;
3656+ let captured_clone = Arc :: clone ( & captured) ;
3657+
3658+ let app = Router :: new ( ) . route (
3659+ "/token" ,
3660+ post ( move |body : axum:: body:: Bytes | {
3661+ let cap = Arc :: clone ( & captured_clone) ;
3662+ async move {
3663+ * cap. lock ( ) . unwrap ( ) =
3664+ Some ( String :: from_utf8 ( body. to_vec ( ) ) . unwrap ( ) ) ;
3665+ Response :: builder ( )
3666+ . status ( 200 )
3667+ . header ( "content-type" , "application/json" )
3668+ . body ( Body :: from (
3669+ r#"{"access_token":"new-token","token_type":"Bearer","expires_in":3600}"# ,
3670+ ) )
3671+ . unwrap ( )
3672+ }
3673+ } ) ,
3674+ ) ;
3675+
3676+ let listener = tokio:: net:: TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
3677+ let addr = listener. local_addr ( ) . unwrap ( ) ;
3678+ tokio:: spawn ( async move { axum:: serve ( listener, app) . await . unwrap ( ) } ) ;
3679+
3680+ ( format ! ( "http://{}" , addr) , captured)
3681+ }
3682+
3683+ #[ tokio:: test]
3684+ async fn refresh_token_sends_granted_scopes_in_request ( ) {
3685+ let ( base_url, captured) = start_token_server ( ) . await ;
3686+
3687+ let mut manager = manager_with_metadata ( Some ( AuthorizationMetadata {
3688+ authorization_endpoint : format ! ( "{}/authorize" , base_url) ,
3689+ token_endpoint : format ! ( "{}/token" , base_url) ,
3690+ ..Default :: default ( )
3691+ } ) )
3692+ . await ;
3693+ manager. configure_client ( test_client_config ( ) ) . unwrap ( ) ;
3694+
3695+ let stored = StoredCredentials {
3696+ client_id : "my-client" . to_string ( ) ,
3697+ token_response : Some ( make_token_response_with_refresh (
3698+ "old-token" ,
3699+ "my-refresh-token" ,
3700+ ) ) ,
3701+ granted_scopes : vec ! [ "read" . to_string( ) , "write" . to_string( ) ] ,
3702+ token_received_at : Some ( AuthorizationManager :: now_epoch_secs ( ) ) ,
3703+ } ;
3704+ manager. credential_store . save ( stored) . await . unwrap ( ) ;
3705+
3706+ manager. refresh_token ( ) . await . unwrap ( ) ;
3707+
3708+ let body = captured. lock ( ) . unwrap ( ) . take ( ) . unwrap ( ) ;
3709+ let params: std:: collections:: HashMap < _ , _ > = url:: form_urlencoded:: parse ( body. as_bytes ( ) )
3710+ . into_owned ( )
3711+ . collect ( ) ;
3712+ let scope = params
3713+ . get ( "scope" )
3714+ . expect ( "scope should be present in refresh request" ) ;
3715+ let mut scope_parts: Vec < & str > = scope. split_whitespace ( ) . collect ( ) ;
3716+ scope_parts. sort_unstable ( ) ;
3717+ assert_eq ! ( scope_parts, vec![ "read" , "write" ] ) ;
3718+ }
3719+
3720+ #[ tokio:: test]
3721+ async fn refresh_token_omits_scope_when_granted_scopes_is_empty ( ) {
3722+ let ( base_url, captured) = start_token_server ( ) . await ;
3723+
3724+ let mut manager = manager_with_metadata ( Some ( AuthorizationMetadata {
3725+ authorization_endpoint : format ! ( "{}/authorize" , base_url) ,
3726+ token_endpoint : format ! ( "{}/token" , base_url) ,
3727+ ..Default :: default ( )
3728+ } ) )
3729+ . await ;
3730+ manager. configure_client ( test_client_config ( ) ) . unwrap ( ) ;
3731+
3732+ let stored = StoredCredentials {
3733+ client_id : "my-client" . to_string ( ) ,
3734+ token_response : Some ( make_token_response_with_refresh (
3735+ "old-token" ,
3736+ "my-refresh-token" ,
3737+ ) ) ,
3738+ granted_scopes : vec ! [ ] ,
3739+ token_received_at : Some ( AuthorizationManager :: now_epoch_secs ( ) ) ,
3740+ } ;
3741+ manager. credential_store . save ( stored) . await . unwrap ( ) ;
3742+
3743+ manager. refresh_token ( ) . await . unwrap ( ) ;
3744+
3745+ let body = captured. lock ( ) . unwrap ( ) . take ( ) . unwrap ( ) ;
3746+ let params: std:: collections:: HashMap < _ , _ > = url:: form_urlencoded:: parse ( body. as_bytes ( ) )
3747+ . into_owned ( )
3748+ . collect ( ) ;
3749+ assert ! (
3750+ !params. contains_key( "scope" ) ,
3751+ "scope should be absent when granted_scopes is empty, body: {body}"
3752+ ) ;
3753+ }
35833754}
0 commit comments