updated plugin ActivityPub version 8.3.0

This commit is contained in:
2026-06-03 21:28:46 +00:00
committed by Gitium
parent a4b78ec277
commit 6fe182458a
340 changed files with 43232 additions and 7568 deletions

View File

@ -0,0 +1,406 @@
<?php
/**
* OAuth 2.0 Authorization REST Controller.
*
* @package Activitypub
*/
namespace Activitypub\Rest\OAuth;
use Activitypub\OAuth\Authorization_Code;
use Activitypub\OAuth\Client;
use Activitypub\OAuth\Scope;
use function Activitypub\get_client_ip;
/**
* Authorization_Controller class for handling the OAuth 2.0 authorization endpoint.
*
* Implements:
* - Authorization endpoint (GET/POST /oauth/authorize)
*
* @since 8.1.0
*/
class Authorization_Controller extends \WP_REST_Controller {
/**
* The namespace of this controller's route.
*
* @var string
*/
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
/**
* The base of this controller's route.
*
* @var string
*/
protected $rest_base = 'oauth';
/**
* Register routes.
*/
public function register_routes() {
// Authorization endpoint - GET displays consent form, POST handles approval.
\register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/authorize',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'authorize' ),
'permission_callback' => '__return_true',
'args' => array(
'response_type' => array(
'description' => 'OAuth response type (must be "code").',
'type' => 'string',
'required' => true,
'enum' => array( 'code' ),
),
'client_id' => array(
'description' => 'The OAuth client identifier.',
'type' => 'string',
'required' => true,
),
'redirect_uri' => array(
'description' => 'The URI to redirect to after authorization. Supports custom URI schemes for native apps.',
'type' => 'string',
'required' => true,
),
'scope' => array(
'description' => 'Space-separated list of requested scopes.',
'type' => 'string',
),
'state' => array(
'description' => 'Opaque value for CSRF protection.',
'type' => 'string',
),
'code_challenge' => array(
'description' => 'PKCE code challenge (recommended).',
'type' => 'string',
),
'code_challenge_method' => array(
'description' => 'PKCE code challenge method.',
'type' => 'string',
'enum' => array( 'S256' ),
'default' => 'S256',
),
),
),
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'authorize_submit' ),
'permission_callback' => array( $this, 'authorize_submit_permissions_check' ),
'args' => array(
'response_type' => array(
'description' => 'OAuth response type (must be "code").',
'type' => 'string',
'required' => true,
'enum' => array( 'code' ),
),
'client_id' => array(
'description' => 'The OAuth client identifier.',
'type' => 'string',
'required' => true,
),
'redirect_uri' => array(
'description' => 'The URI to redirect to after authorization. Supports custom URI schemes for native apps.',
'type' => 'string',
'required' => true,
),
'scope' => array(
'description' => 'Space-separated list of requested scopes.',
'type' => 'string',
),
'state' => array(
'description' => 'Opaque value for CSRF protection.',
'type' => 'string',
),
'code_challenge' => array(
'description' => 'PKCE code challenge (recommended).',
'type' => 'string',
),
'code_challenge_method' => array(
'description' => 'PKCE code challenge method.',
'type' => 'string',
'enum' => array( 'S256' ),
'default' => 'S256',
),
'approve' => array(
'description' => 'Whether the user approved the authorization.',
'type' => 'boolean',
'required' => true,
),
'_wpnonce' => array(
'description' => 'WordPress nonce for CSRF protection.',
'type' => 'string',
'required' => true,
),
),
),
)
);
}
/**
* Handle authorization request (GET /oauth/authorize).
*
* Validates request parameters and redirects to wp-admin consent page.
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public function authorize( \WP_REST_Request $request ) {
// Rate-limit authorization requests to prevent abuse (max 20 per minute per IP).
$ip = get_client_ip();
if ( '' === $ip ) {
return new \WP_Error(
'activitypub_rate_limit',
\__( 'Too many authorization requests. Please try again later.', 'activitypub' ),
array( 'status' => 429 )
);
}
$transient_key = 'ap_oauth_auth_' . \md5( $ip );
$count = (int) \get_transient( $transient_key );
if ( $count >= 20 ) {
return new \WP_Error(
'activitypub_rate_limit',
\__( 'Too many authorization requests. Please try again later.', 'activitypub' ),
array( 'status' => 429 )
);
}
\set_transient( $transient_key, $count + 1, MINUTE_IN_SECONDS );
$client_id = $request->get_param( 'client_id' );
$redirect_uri = $request->get_param( 'redirect_uri' );
$response_type = $request->get_param( 'response_type' );
$scope = $request->get_param( 'scope' );
$state = $request->get_param( 'state' );
// Validate client.
$client = Client::get( $client_id );
if ( \is_wp_error( $client ) ) {
return $this->error_page( $client );
}
// Validate redirect URI.
if ( ! $client->is_valid_redirect_uri( $redirect_uri ) ) {
return $this->error_page(
new \WP_Error(
'activitypub_invalid_redirect_uri',
\__( 'Invalid redirect URI for this client.', 'activitypub' ),
array( 'status' => 400 )
)
);
}
// Only support 'code' response type.
if ( 'code' !== $response_type ) {
return $this->redirect_with_error(
$redirect_uri,
'unsupported_response_type',
'Only authorization code flow is supported.',
$state
);
}
// Check for PKCE (recommended but optional for compatibility).
$code_challenge = $request->get_param( 'code_challenge' );
/*
* Redirect to wp-login.php with action=activitypub_authorize.
* This uses WordPress's login_form_{action} hook for proper cookie auth.
*/
$login_url = \wp_login_url();
$login_url = \add_query_arg(
array(
'action' => 'activitypub_authorize',
'client_id' => $client_id,
'redirect_uri' => $redirect_uri,
'response_type' => $response_type,
'scope' => $scope,
'state' => $state,
'code_challenge' => $code_challenge,
'code_challenge_method' => $request->get_param( 'code_challenge_method' ) ?: 'S256',
),
$login_url
);
return new \WP_REST_Response(
null,
302,
array( 'Location' => $login_url )
);
}
/**
* Handle authorization approval (POST /oauth/authorize).
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public function authorize_submit( \WP_REST_Request $request ) {
$client_id = $request->get_param( 'client_id' );
$redirect_uri = $request->get_param( 'redirect_uri' );
$scope = $request->get_param( 'scope' );
$state = $request->get_param( 'state' );
$code_challenge = $request->get_param( 'code_challenge' );
$code_challenge_method = $request->get_param( 'code_challenge_method' ) ?: 'S256';
$approve = $request->get_param( 'approve' );
// Re-validate client and redirect URI (form fields could be tampered with).
$client = Client::get( $client_id );
if ( \is_wp_error( $client ) ) {
return $this->error_page( $client );
}
if ( ! $client->is_valid_redirect_uri( $redirect_uri ) ) {
return $this->error_page(
new \WP_Error(
'activitypub_invalid_redirect_uri',
\__( 'Invalid redirect URI for this client.', 'activitypub' ),
array( 'status' => 400 )
)
);
}
// User denied authorization.
if ( ! $approve ) {
return $this->redirect_with_error(
$redirect_uri,
'access_denied',
'The user denied the authorization request.',
$state
);
}
// Create authorization code.
$scopes = Scope::validate( Scope::parse( $scope ) );
$code = Authorization_Code::create(
\get_current_user_id(),
$client_id,
$redirect_uri,
$scopes,
$code_challenge,
$code_challenge_method
);
if ( \is_wp_error( $code ) ) {
return $this->redirect_with_error(
$redirect_uri,
'server_error',
$code->get_error_message(),
$state
);
}
// Redirect back to client with code.
$redirect_url = \add_query_arg(
array(
'code' => $code,
'state' => $state,
),
$redirect_uri
);
return new \WP_REST_Response(
null,
302,
array( 'Location' => $redirect_url )
);
}
/**
* Permission check for authorization submission.
*
* @param \WP_REST_Request $request The request object.
* @return bool|\WP_Error True if allowed, error otherwise.
*/
public function authorize_submit_permissions_check( \WP_REST_Request $request ) {
if ( ! \is_user_logged_in() ) {
return new \WP_Error(
'activitypub_not_logged_in',
\__( 'You must be logged in to authorize applications.', 'activitypub' ),
array( 'status' => 401 )
);
}
// Verify nonce.
$nonce = $request->get_param( '_wpnonce' );
if ( ! \wp_verify_nonce( $nonce, 'activitypub_oauth_authorize' ) ) {
return new \WP_Error(
'activitypub_invalid_nonce',
\__( 'Invalid security token. Please try again.', 'activitypub' ),
array( 'status' => 403 )
);
}
return true;
}
/**
* Redirect to wp-login.php with a styled error message.
*
* These errors occur before a valid redirect URI is confirmed, so we
* cannot safely redirect back to the client. Instead, redirect to
* wp-login.php where the error is rendered using login_header/login_footer
* for a consistent, user-friendly appearance.
*
* The error message is stored in a short-lived transient (5 minutes)
* keyed by a random token. Only the opaque token is passed in the URL,
* preventing social-engineering attacks where an attacker crafts a URL
* with arbitrary error text displayed inside WordPress login chrome.
*
* @since 8.1.0
*
* @param \WP_Error $error The error to display.
* @return \WP_REST_Response Redirect response to wp-login.php.
*/
private function error_page( $error ) {
$token = \wp_generate_password( 20, false );
\set_transient( 'ap_oauth_err_' . $token, $error->get_error_message(), 5 * MINUTE_IN_SECONDS );
$login_url = \add_query_arg(
array(
'action' => 'activitypub_authorize',
'auth_error' => $token,
),
\wp_login_url()
);
return new \WP_REST_Response(
null,
302,
array( 'Location' => $login_url )
);
}
/**
* Redirect with an OAuth error.
*
* @param string $redirect_uri The redirect URI.
* @param string $error Error code.
* @param string $description Error description.
* @param string $state The state parameter.
* @return \WP_REST_Response
*/
private function redirect_with_error( $redirect_uri, $error, $description, $state = null ) {
$params = array(
'error' => $error,
'error_description' => $description,
);
if ( $state ) {
$params['state'] = $state;
}
$redirect_url = \add_query_arg( $params, $redirect_uri );
return new \WP_REST_Response(
null,
302,
array( 'Location' => $redirect_url )
);
}
}

View File

@ -0,0 +1,186 @@
<?php
/**
* OAuth 2.0 Client Registration and Metadata REST Controller.
*
* @package Activitypub
*/
namespace Activitypub\Rest\OAuth;
use Activitypub\OAuth\Client;
use Activitypub\OAuth\Scope;
use Activitypub\OAuth\Server as OAuth_Server;
use function Activitypub\get_client_ip;
/**
* Clients_Controller class for handling OAuth 2.0 client and metadata endpoints.
*
* Implements:
* - Dynamic client registration (POST /oauth/clients)
* - Authorization Server Metadata (GET /oauth/authorization-server-metadata)
*
* @since 8.1.0
*/
class Clients_Controller extends \WP_REST_Controller {
/**
* The namespace of this controller's route.
*
* @var string
*/
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
/**
* The base of this controller's route.
*
* @var string
*/
protected $rest_base = 'oauth';
/**
* Register routes.
*/
public function register_routes() {
// Dynamic client registration (RFC 7591).
\register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/clients',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'register_client' ),
'permission_callback' => '__return_true',
'args' => array(
'client_name' => array(
'description' => 'Human-readable name of the client.',
'type' => 'string',
'required' => true,
),
'redirect_uris' => array(
'description' => 'Array of redirect URIs. Supports custom URI schemes for native apps.',
'type' => 'array',
'items' => array(
'type' => 'string',
),
'required' => true,
),
'client_uri' => array(
'description' => 'URL of the client homepage.',
'type' => 'string',
'format' => 'uri',
),
'scope' => array(
'description' => 'Space-separated list of requested scopes.',
'type' => 'string',
),
),
),
)
);
// Authorization Server Metadata (RFC 8414).
\register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/authorization-server-metadata',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_metadata' ),
'permission_callback' => '__return_true',
),
)
);
}
/**
* Handle dynamic client registration (POST /oauth/clients).
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public function register_client( \WP_REST_Request $request ) {
/**
* Filters whether RFC 7591 dynamic client registration is allowed.
*
* Enabled by default so C2S clients can register on the fly.
* Return false to restrict registration to pre-configured clients only.
*
* @param bool $allowed Whether dynamic registration is allowed. Default true.
*/
if ( ! \apply_filters( 'activitypub_allow_dynamic_client_registration', true ) ) {
return new \WP_Error(
'activitypub_registration_disabled',
\__( 'Dynamic client registration is not allowed.', 'activitypub' ),
array( 'status' => 403 )
);
}
// Rate-limit registrations to prevent DB spam (max 10 per minute per IP).
$ip = get_client_ip();
if ( '' === $ip ) {
return new \WP_Error(
'activitypub_rate_limited',
\__( 'Too many client registration requests. Please try again later.', 'activitypub' ),
array( 'status' => 429 )
);
}
$transient_key = 'ap_oauth_reg_' . \md5( $ip );
$count = (int) \get_transient( $transient_key );
if ( $count >= 10 ) {
return new \WP_Error(
'activitypub_rate_limited',
\__( 'Too many client registration requests. Please try again later.', 'activitypub' ),
array( 'status' => 429 )
);
}
\set_transient( $transient_key, $count + 1, MINUTE_IN_SECONDS );
$client_name = $request->get_param( 'client_name' );
$redirect_uris = $request->get_param( 'redirect_uris' );
$client_uri = $request->get_param( 'client_uri' );
$scope = $request->get_param( 'scope' );
$result = Client::register(
array(
'name' => $client_name,
'redirect_uris' => $redirect_uris,
'description' => $client_uri ?? '',
'is_public' => true, // Dynamic clients are always public.
'scopes' => $scope ? Scope::parse( $scope ) : Scope::ALL,
)
);
if ( \is_wp_error( $result ) ) {
return $result;
}
// RFC 7591 response format.
$response = array(
'client_id' => $result['client_id'],
'client_name' => $client_name,
'redirect_uris' => $redirect_uris,
'token_endpoint_auth_method' => 'none',
);
if ( isset( $result['client_secret'] ) ) {
$response['client_secret'] = $result['client_secret'];
}
return new \WP_REST_Response( $response, 201 );
}
/**
* Get OAuth server metadata.
*
* @return \WP_REST_Response
*/
public function get_metadata() {
return new \WP_REST_Response(
OAuth_Server::get_metadata(),
200,
array( 'Content-Type' => 'application/json' )
);
}
}

View File

@ -0,0 +1,432 @@
<?php
/**
* OAuth 2.0 Token REST Controller.
*
* @package Activitypub
*/
namespace Activitypub\Rest\OAuth;
use Activitypub\OAuth\Authorization_Code;
use Activitypub\OAuth\Client;
use Activitypub\OAuth\Scope;
use Activitypub\OAuth\Server as OAuth_Server;
use Activitypub\OAuth\Token;
use function Activitypub\get_client_ip;
/**
* Token_Controller class for handling OAuth 2.0 token endpoints.
*
* Implements:
* - Token endpoint (POST /oauth/token)
* - Revocation endpoint (POST /oauth/revoke)
* - Token introspection endpoint (POST /oauth/introspect)
*
* @since 8.1.0
*/
class Token_Controller extends \WP_REST_Controller {
/**
* The namespace of this controller's route.
*
* @var string
*/
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
/**
* The base of this controller's route.
*
* @var string
*/
protected $rest_base = 'oauth';
/**
* Register routes.
*/
public function register_routes() {
// Token endpoint.
\register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/token',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'token' ),
'permission_callback' => '__return_true',
'args' => array(
'grant_type' => array(
'description' => 'The grant type.',
'type' => 'string',
'required' => true,
'enum' => array( 'authorization_code', 'refresh_token' ),
),
'client_id' => array(
'description' => 'The OAuth client identifier.',
'type' => 'string',
),
'client_secret' => array(
'description' => 'The OAuth client secret (for confidential clients).',
'type' => 'string',
),
'code' => array(
'description' => 'The authorization code (for authorization_code grant).',
'type' => 'string',
),
'redirect_uri' => array(
'description' => 'The redirect URI (must match original for authorization_code grant). Supports custom URI schemes for native apps.',
'type' => 'string',
),
'code_verifier' => array(
'description' => 'PKCE code verifier.',
'type' => 'string',
),
'refresh_token' => array(
'description' => 'The refresh token (for refresh_token grant).',
'type' => 'string',
),
'scope' => array(
'description' => 'Space-separated list of requested scopes.',
'type' => 'string',
),
),
),
)
);
// Revocation endpoint (RFC 7009 — requires authentication).
\register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/revoke',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'revoke' ),
'permission_callback' => array( $this, 'revoke_permissions_check' ),
'args' => array(
'token' => array(
'description' => 'The token to revoke.',
'type' => 'string',
'required' => true,
),
'token_type_hint' => array(
'description' => 'Hint about the token type.',
'type' => 'string',
'enum' => array( 'access_token', 'refresh_token' ),
),
),
),
)
);
// Token introspection endpoint (RFC 7662).
\register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/introspect',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'introspect' ),
'permission_callback' => array( $this, 'introspect_permissions_check' ),
'args' => array(
'token' => array(
'description' => 'The token to introspect.',
'type' => 'string',
'required' => true,
),
'token_type_hint' => array(
'description' => 'Hint about the token type.',
'type' => 'string',
'enum' => array( 'access_token', 'refresh_token' ),
),
),
),
)
);
}
/**
* Handle token request (POST /oauth/token).
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public function token( \WP_REST_Request $request ) {
// Rate-limit token requests to prevent brute-force attacks (max 20 per minute per IP).
$ip = get_client_ip();
if ( '' === $ip ) {
return $this->token_error( 'rate_limited', 'Too many token requests. Please try again later.', 429 );
}
$transient_key = 'ap_oauth_tok_' . \md5( $ip );
$count = (int) \get_transient( $transient_key );
if ( $count >= 20 ) {
return $this->token_error( 'rate_limited', 'Too many token requests. Please try again later.', 429 );
}
\set_transient( $transient_key, $count + 1, MINUTE_IN_SECONDS );
$grant_type = $request->get_param( 'grant_type' );
/*
* Extract client credentials from either:
* - client_secret_basic: HTTP Basic Auth header (RFC 6749 Section 2.3.1)
* - client_secret_post: POST body parameters
*/
$client_id = null;
$client_secret = null;
$auth_header = $request->get_header( 'Authorization' );
if ( $auth_header && 0 === \strpos( $auth_header, 'Basic ' ) ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Required by OAuth spec.
$decoded = \base64_decode( \substr( $auth_header, 6 ), true );
if ( $decoded && false !== \strpos( $decoded, ':' ) ) {
list( $client_id, $client_secret ) = \explode( ':', $decoded, 2 );
$client_id = \urldecode( $client_id );
$client_secret = \urldecode( $client_secret );
}
}
// Fall back to POST body parameters (client_secret_post).
if ( ! $client_id ) {
$client_id = $request->get_param( 'client_id' );
$client_secret = $request->get_param( 'client_secret' );
}
// Validate client.
$client = Client::get( $client_id );
if ( \is_wp_error( $client ) ) {
return $this->token_error( 'invalid_client', 'Unknown client.' );
}
// Validate client credentials if confidential.
if ( ! $client->is_public() ) {
if ( ! Client::validate( $client_id, $client_secret ) ) {
return $this->token_error( 'invalid_client', 'Invalid client credentials.' );
}
}
switch ( $grant_type ) {
case 'authorization_code':
return $this->handle_authorization_code_grant( $request, $client_id );
case 'refresh_token':
return $this->handle_refresh_token_grant( $request, $client_id );
default:
return $this->token_error( 'unsupported_grant_type', 'Grant type not supported.' );
}
}
/**
* Handle authorization code grant.
*
* @param \WP_REST_Request $request The request object.
* @param string $client_id The client ID.
* @return \WP_REST_Response|\WP_Error
*/
private function handle_authorization_code_grant( \WP_REST_Request $request, $client_id ) {
$code = $request->get_param( 'code' );
$redirect_uri = $request->get_param( 'redirect_uri' );
$code_verifier = $request->get_param( 'code_verifier' );
if ( empty( $code ) ) {
return $this->token_error( 'invalid_request', 'Authorization code is required.' );
}
$result = Authorization_Code::exchange( $code, $client_id, $redirect_uri, $code_verifier );
if ( \is_wp_error( $result ) ) {
return $this->token_error( 'invalid_grant', $result->get_error_message() );
}
return $this->token_response( $result );
}
/**
* Handle refresh token grant.
*
* @param \WP_REST_Request $request The request object.
* @param string $client_id The client ID.
* @return \WP_REST_Response|\WP_Error
*/
private function handle_refresh_token_grant( \WP_REST_Request $request, $client_id ) {
$refresh_token = $request->get_param( 'refresh_token' );
if ( empty( $refresh_token ) ) {
return $this->token_error( 'invalid_request', 'Refresh token is required.' );
}
$result = Token::refresh( $refresh_token, $client_id );
if ( \is_wp_error( $result ) ) {
return $this->token_error( 'invalid_grant', $result->get_error_message() );
}
return $this->token_response( $result );
}
/**
* Handle token revocation (POST /oauth/revoke).
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response
*/
public function revoke( \WP_REST_Request $request ) {
$token = $request->get_param( 'token' );
if ( \current_user_can( 'manage_options' ) ) {
// Site admins may revoke any token. Null-null disables the ownership check.
Token::revoke( $token );
} else {
/*
* RFC 7009 §2.1: the server must verify the token was issued to
* the requesting client. When the caller authenticated with a
* bearer token we know the calling client, so require a client
* match and ignore the user — otherwise a low-trust client
* could revoke tokens the user had granted to a different
* client. For pure cookie-authenticated callers there is no
* client context, so user match is the only available check.
*/
$caller_token = OAuth_Server::get_current_token();
if ( $caller_token ) {
Token::revoke( $token, null, $caller_token->get_client_id() );
} else {
Token::revoke( $token, \get_current_user_id(), null );
}
}
// Per RFC 7009, always return 200 even if the token doesn't exist or was not owned.
return new \WP_REST_Response( null, 200 );
}
/**
* Handle token introspection (POST /oauth/introspect).
*
* Implements RFC 7662 Token Introspection.
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response
*/
public function introspect( \WP_REST_Request $request ) {
$token = $request->get_param( 'token' );
// Introspect the token.
$response = Token::introspect( $token );
// Scope introspection to same client: non-admin users can only
// introspect tokens belonging to the same client as their own.
if ( $response['active'] && ! \current_user_can( 'manage_options' ) ) {
$current_token = OAuth_Server::get_current_token();
if ( $current_token && $current_token->get_client_id() !== $response['client_id'] ) {
$response = array( 'active' => false );
}
}
return new \WP_REST_Response( $response, 200 );
}
/**
* Permission check for token revocation.
*
* Per RFC 7009, the revocation endpoint must be protected.
* Requires either a logged-in user or a valid Bearer token.
*
* @return bool|\WP_Error True if allowed, error otherwise.
*/
public function revoke_permissions_check() {
if ( \is_user_logged_in() ) {
return true;
}
$token = OAuth_Server::get_bearer_token();
if ( $token ) {
$validated = Token::validate( $token );
if ( ! \is_wp_error( $validated ) ) {
\wp_set_current_user( $validated->get_user_id() );
return true;
}
}
return new \WP_Error(
'activitypub_unauthorized',
\__( 'Authentication required.', 'activitypub' ),
array( 'status' => 401 )
);
}
/**
* Permission check for token introspection.
*
* Per RFC 7662, the introspection endpoint must be protected.
*
* @return bool|\WP_Error True if allowed, error otherwise.
*/
public function introspect_permissions_check() {
if ( \is_user_logged_in() ) {
return true;
}
// Support Bearer token auth for public OAuth clients.
$token = OAuth_Server::get_bearer_token();
if ( $token ) {
$validated = Token::validate( $token );
if ( ! \is_wp_error( $validated ) ) {
\wp_set_current_user( $validated->get_user_id() );
return true;
}
}
return new \WP_Error(
'activitypub_unauthorized',
\__( 'Authentication required.', 'activitypub' ),
array( 'status' => 401 )
);
}
/**
* Create a token error response.
*
* @param string $error Error code.
* @param string $error_description Error description.
* @param int $status Optional. HTTP status code. Defaults to 400 per RFC 6749 §5.2;
* callers should pass 429 for rate-limit responses (RFC 6585).
* @return \WP_REST_Response
*/
private function token_error( $error, $error_description, $status = 400 ) {
return new \WP_REST_Response(
array(
'error' => $error,
'error_description' => $error_description,
),
$status,
array(
'Content-Type' => 'application/json',
// RFC 6749 §5.1 requires the same no-cache headers on error responses as on success responses.
'Cache-Control' => 'no-store',
'Pragma' => 'no-cache',
)
);
}
/**
* Create a token success response.
*
* @param array $token_data Token data.
* @return \WP_REST_Response
*/
private function token_response( $token_data ) {
return new \WP_REST_Response(
$token_data,
200,
array(
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store',
'Pragma' => 'no-cache',
)
);
}
}