@@ -4,7 +4,10 @@ use rmcp::{
44 service:: RequestContext ,
55 transport:: {
66 AuthClient , AuthorizationManager , StreamableHttpClientTransport ,
7- auth:: { AuthorizationCallback , AuthorizationRequest , InMemoryCredentialStore , OAuthState } ,
7+ auth:: {
8+ AuthorizationCallback , AuthorizationRequest , ClientCredentialsConfig ,
9+ InMemoryCredentialStore , JwtSigningAlgorithm , OAuthState ,
10+ } ,
811 streamable_http_client:: StreamableHttpClientTransportConfig ,
912 } ,
1013} ;
@@ -651,49 +654,43 @@ async fn run_client_credentials_jwt(
651654) -> anyhow:: Result < ( ) > {
652655 let client_id = ctx
653656 . client_id
654- . as_deref ( )
655- . unwrap_or ( "conformance-test-client" ) ;
656- let _pem = ctx
657+ . clone ( )
658+ . unwrap_or_else ( || "conformance-test-client" . to_string ( ) ) ;
659+ let signing_key = ctx
657660 . private_key_pem
658- . as_deref ( )
659- . ok_or_else ( || anyhow:: anyhow!( "Missing private_key_pem" ) ) ?;
660- let _alg = ctx
661+ . as_ref ( )
662+ . ok_or_else ( || anyhow:: anyhow!( "Missing private_key_pem" ) ) ?
663+ . as_bytes ( )
664+ . to_vec ( ) ;
665+ let signing_algorithm = match ctx
661666 . signing_algorithm
662667 . as_deref ( )
663- . ok_or_else ( || anyhow:: anyhow!( "Missing signing_algorithm" ) ) ?;
664-
665- // Discover metadata to get token endpoint
666- let mut manager = AuthorizationManager :: new ( server_url) . await ?;
667- let metadata = manager. discover_metadata ( ) . await ?;
668- let token_endpoint = metadata. token_endpoint . clone ( ) ;
669- manager. set_metadata ( metadata) ;
670-
671- // Build JWT assertion
672- // Parse the PEM private key
673- let key = openssl_free_ec_sign ( _pem, client_id, & token_endpoint) ?;
674-
675- let http = reqwest:: Client :: new ( ) ;
676- let form_body = format ! (
677- "grant_type=client_credentials&client_assertion_type={}&client_assertion={}" ,
678- urlencoding:: encode( "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ) ,
679- urlencoding:: encode( & key) ,
680- ) ;
681- let resp = http
682- . post ( & token_endpoint)
683- . header ( "content-type" , "application/x-www-form-urlencoded" )
684- . body ( form_body)
685- . send ( )
686- . await ?;
687-
688- let token_resp: serde_json:: Value = resp. json ( ) . await ?;
689- let access_token = token_resp[ "access_token" ]
690- . as_str ( )
691- . ok_or_else ( || anyhow:: anyhow!( "No access_token: {}" , token_resp) ) ?;
668+ . ok_or_else ( || anyhow:: anyhow!( "Missing signing_algorithm" ) ) ?
669+ {
670+ "RS256" => JwtSigningAlgorithm :: RS256 ,
671+ "RS384" => JwtSigningAlgorithm :: RS384 ,
672+ "RS512" => JwtSigningAlgorithm :: RS512 ,
673+ "ES256" => JwtSigningAlgorithm :: ES256 ,
674+ "ES384" => JwtSigningAlgorithm :: ES384 ,
675+ algorithm => anyhow:: bail!( "Unsupported signing_algorithm: {algorithm}" ) ,
676+ } ;
677+ let config = ClientCredentialsConfig :: PrivateKeyJwt {
678+ client_id,
679+ signing_key,
680+ signing_algorithm,
681+ token_endpoint_audience : None ,
682+ scopes : vec ! [ ] ,
683+ resource : Some ( server_url. to_string ( ) ) ,
684+ } ;
685+ let mut oauth_state = OAuthState :: new ( server_url, None ) . await ?;
686+ oauth_state. authenticate_client_credentials ( config) . await ?;
687+ let manager = oauth_state
688+ . into_authorization_manager ( )
689+ . ok_or_else ( || anyhow:: anyhow!( "Client credentials flow did not authorize" ) ) ?;
692690
693691 let transport = StreamableHttpClientTransport :: with_client (
694- reqwest:: Client :: default ( ) ,
695- StreamableHttpClientTransportConfig :: with_uri ( server_url)
696- . auth_header ( access_token. to_string ( ) ) ,
692+ AuthClient :: new ( reqwest:: Client :: default ( ) , manager) ,
693+ StreamableHttpClientTransportConfig :: with_uri ( server_url) ,
697694 ) ;
698695
699696 let client = BasicClientHandler . serve ( transport) . await ?;
@@ -709,73 +706,6 @@ async fn run_client_credentials_jwt(
709706 Ok ( ( ) )
710707}
711708
712- /// Minimal ES256 JWT signing without heavy deps.
713- /// We use ring or pure-Rust approach. For simplicity, use the p256 + base64 crates
714- /// that are already transitive deps of oauth2.
715- fn openssl_free_ec_sign ( pem : & str , client_id : & str , audience : & str ) -> anyhow:: Result < String > {
716- use std:: time:: { SystemTime , UNIX_EPOCH } ;
717-
718- // Decode PEM → DER
719- let pem_body = pem
720- . lines ( )
721- . filter ( |l| !l. starts_with ( "-----" ) )
722- . collect :: < String > ( ) ;
723- let der = base64_decode ( & pem_body) ?;
724-
725- // Parse PKCS#8 DER to get the raw EC private key bytes
726- // PKCS#8 for EC P-256: the raw 32-byte key is at the end of the structure
727- let raw_key = extract_ec_private_key ( & der) ?;
728-
729- let now = SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) ?. as_secs ( ) ;
730- let header = base64url_encode ( br#"{"alg":"ES256","typ":"JWT"}"# ) ;
731- let payload_json = serde_json:: json!( {
732- "iss" : client_id,
733- "sub" : client_id,
734- "aud" : audience,
735- "iat" : now,
736- "exp" : now + 300 ,
737- "jti" : format!( "jti-{}" , now) ,
738- } ) ;
739- let payload = base64url_encode ( payload_json. to_string ( ) . as_bytes ( ) ) ;
740- let signing_input = format ! ( "{}.{}" , header, payload) ;
741-
742- // Sign with p256
743- let secret_key = p256:: ecdsa:: SigningKey :: from_slice ( raw_key. as_slice ( ) )
744- . map_err ( |e| anyhow:: anyhow!( "Invalid EC key: {}" , e) ) ?;
745- use p256:: ecdsa:: signature:: Signer ;
746- let sig: p256:: ecdsa:: Signature = secret_key. sign ( signing_input. as_bytes ( ) ) ;
747- let sig_bytes = sig. to_bytes ( ) ;
748- let sig_b64 = base64url_encode ( & sig_bytes) ;
749-
750- Ok ( format ! ( "{}.{}" , signing_input, sig_b64) )
751- }
752-
753- fn base64url_encode ( data : & [ u8 ] ) -> String {
754- use base64:: Engine ;
755- base64:: engine:: general_purpose:: URL_SAFE_NO_PAD . encode ( data)
756- }
757-
758- fn base64_decode ( s : & str ) -> anyhow:: Result < Vec < u8 > > {
759- use base64:: Engine ;
760- Ok ( base64:: engine:: general_purpose:: STANDARD . decode ( s. trim ( ) ) ?)
761- }
762-
763- /// Extract the raw 32-byte EC private key from a PKCS#8 DER blob.
764- fn extract_ec_private_key ( der : & [ u8 ] ) -> anyhow:: Result < Vec < u8 > > {
765- // PKCS#8 wraps an ECPrivateKey. We look for the octet string containing
766- // the 32-byte private key. A simple heuristic: find 0x04 0x20 (OCTET STRING, len 32)
767- // followed by exactly 32 bytes that form the key.
768- // More robust: parse ASN.1. But for conformance testing this suffices.
769- for i in 0 ..der. len ( ) . saturating_sub ( 33 ) {
770- if der[ i] == 0x04 && der[ i + 1 ] == 0x20 && i + 34 <= der. len ( ) {
771- return Ok ( der[ i + 2 ..i + 34 ] . to_vec ( ) ) ;
772- }
773- }
774- Err ( anyhow:: anyhow!(
775- "Could not extract 32-byte EC private key from PKCS#8 DER"
776- ) )
777- }
778-
779709/// Cross-app access flow (SEP-1046 extension).
780710async fn run_cross_app_access_client (
781711 server_url : & str ,
0 commit comments