--- a/inc/Auth/CimdResolver.php +++ b/inc/Auth/CimdResolver.php @@ -243,6 +243,11 @@ // Only public clients are supported (no shared secret to authenticate). $auth_method = isset( $doc['token_endpoint_auth_method'] ) ? (string) $doc['token_endpoint_auth_method'] : 'none'; + // AdMindra: negotiate the public method supported by both parties (MCP SEP-3149). + if ( array_key_exists( 'token_endpoint_auth_methods_supported', $doc ) ) { + $methods = $doc['token_endpoint_auth_methods_supported']; + $auth_method = is_array( $methods ) && in_array( 'none', $methods, true ) ? 'none' : 'unsupported'; + } if ( 'none' !== $auth_method ) { McpLogger::log( 'CIMD', --- a/inc/Auth/ClaudeClientVerifier.php +++ b/inc/Auth/ClaudeClientVerifier.php @@ -142,6 +142,10 @@ // the publisher's CIMD document includes additional redirect URIs for other products. $auth_method = (string) ( $doc['token_endpoint_auth_method'] ?? 'none' ); + // AdMindra: a legacy preference is not binding when plural methods are present. + if ( array_key_exists( 'token_endpoint_auth_methods_supported', $doc ) ) { + return is_array( $doc['token_endpoint_auth_methods_supported'] ) && in_array( 'none', $doc['token_endpoint_auth_methods_supported'], true ); + } return 'none' === $auth_method; } } --- a/inc/Auth/JWT.php +++ b/inc/Auth/JWT.php @@ -76,7 +76,7 @@ return null; } - if ( $verify_expiry && isset( $payload['exp'] ) && (int) $payload['exp'] < time() ) { + if ( $verify_expiry && ( ! isset( $payload['exp'] ) || ! is_numeric( $payload['exp'] ) || (int) $payload['exp'] <= time() || ( isset( $payload['nbf'] ) && (int) $payload['nbf'] > time() ) ) ) { return null; } --- a/inc/Transport/OAuthHttpTransport.php +++ b/inc/Transport/OAuthHttpTransport.php @@ -156,6 +156,10 @@ return $this->unauthenticated_error( 'invalid_token', 'JWT signature invalid or token expired.' ); } + if ( 'mcp' !== ( $claims['scope'] ?? '' ) || isset( $claims['type'] ) ) { + return $this->unauthenticated_error( 'invalid_token', 'Access token with mcp scope required.' ); + } + // Audience must match the MCP OAuth REST endpoint. $expected_aud = get_rest_url( null, 'mcp/mcp-oauth-server' ); $token_aud = $claims['aud'] ?? ''; --- a/inc/Auth/TokenEndpoint.php +++ b/inc/Auth/TokenEndpoint.php @@ -124,6 +124,12 @@ return; } + // AdMindra: authorization codes are bound to the requesting public client. + if ( ! isset( $body['client_id'] ) || ! is_string( $body['client_id'] ) || ! hash_equals( (string) ( $code_data['client_id'] ?? '' ), $body['client_id'] ) ) { + $this->send_error( 400, 'invalid_client', 'client_id must match the authorization request.' ); + return; + } + if ( ! delete_transient( $code_key ) ) { McpLogger::log( 'TOKEN', 'rejected: auth code already consumed (concurrent redemption)' ); $this->send_error( 400, 'invalid_grant', 'Code is invalid or has expired.' ); @@ -274,6 +280,12 @@ 'app_pass_uuid' => $app_pass_uuid, ] ); + + // AdMindra: refresh tokens remain bound to the original public client. + if ( ! isset( $body['client_id'] ) || ! is_string( $body['client_id'] ) || ! hash_equals( $client_id, $body['client_id'] ) ) { + $this->send_error( 400, 'invalid_client', 'client_id must match the refresh token.' ); + return; + } // Revocation check: if the Application Password was deleted the session is gone. $app_pass = \WP_Application_Passwords::get_user_application_password( $user_id, $app_pass_uuid );