diff --git a/inc/class-client.php b/inc/class-client.php index 6ea60c7..9871ffe 100644 --- a/inc/class-client.php +++ b/inc/class-client.php @@ -130,6 +130,35 @@ public function get_redirect_uris() { return (array) get_post_meta( $this->get_post_id(), static::REDIRECT_URI_KEY, true ); } + /** + * Does the client require secrets to be validated? + * + * Clients marked as confidential are required to have their client + * credentials (i.e. secret) checked. + * + * @link https://tools.ietf.org/html/rfc6749#section-3.2.1 + * + * @return bool True if secret must be verified, false otherwise. + */ + public function requires_secret() { + $type = $this->get_type(); + return $type === 'private'; + } + + /** + * Check if a secret is valid. + * + * This method ensures that the secret is correctly checked using + * constant-time comparison. + * + * @param string $supplied Supplied secret to check. + * @return boolean True if valid secret, false otherwise. + */ + public function check_secret( $supplied ) { + $stored = $this->get_secret(); + return hash_equals( $supplied, $stored ); + } + /** * Validate a callback URL. * diff --git a/inc/endpoints/class-token.php b/inc/endpoints/class-token.php index e97914b..dc64822 100644 --- a/inc/endpoints/class-token.php +++ b/inc/endpoints/class-token.php @@ -54,18 +54,62 @@ public function validate_grant_type( $type ) { * @return array|WP_Error Token data on success, or error on failure. */ public function exchange_token( WP_REST_Request $request ) { - $client = Client::get_by_id( $request['client_id'] ); + // Check headers for client authentication. + // https://tools.ietf.org/html/rfc6749#section-2.3.1 + if ( isset( $_SERVER['PHP_AUTH_USER'] ) ) { + $client_id = $_SERVER['PHP_AUTH_USER']; + $client_secret = $_SERVER['PHP_AUTH_PW']; + } else { + $client_id = $request['client_id']; + $client_secret = $request['client_secret']; + } + + if ( empty( $client_id ) ) { + // invalid_client + return new WP_Error( + 'oauth2.endpoints.token.exchange_token.no_client_id', + __( 'Missing client ID.'), + array( + 'status' => WP_Http::UNAUTHORIZED, + ) + ); + } + + $client = Client::get_by_id( $client_id ); if ( empty( $client ) ) { return new WP_Error( 'oauth2.endpoints.token.exchange_token.invalid_client', - sprintf( __( 'Client ID %s is invalid.', 'oauth2' ), $request['client_id'] ), + sprintf( __( 'Client ID %s is invalid.', 'oauth2' ), $client_id ), array( 'status' => WP_Http::BAD_REQUEST, - 'client_id' => $request['client_id'], + 'client_id' => $client_id, ) ); } + if ( $client->requires_secret() ) { + // Confidential client, secret must be verified. + if ( empty( $client_secret ) ) { + // invalid_request + return new WP_Error( + 'oauth2.endpoints.token.exchange_token.secret_required', + __( 'Secret is required for confidential clients.', 'oauth2' ), + array( + 'status' => WP_Http::UNAUTHORIZED + ) + ); + } + if ( ! $client->check_secret( $client_secret ) ) { + return new WP_Error( + 'oauth2.endpoints.token.exchange_token.invalid_secret', + __( 'Supplied secret is not valid for the client.', 'oauth2' ), + array( + 'status' => WP_Http::UNAUTHORIZED + ) + ); + } + } + $auth_code = $client->get_authorization_code( $request['code'] ); if ( is_wp_error( $auth_code ) ) { return $auth_code;