Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine Tickets #259

Merged
merged 9 commits into from
Dec 21, 2023
4 changes: 2 additions & 2 deletions includes/class-external-token-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ public function column_expiration( $item ) {
}

public function column_issued_at( $item ) {
if ( ! array_key_exists( 'issued_at', $item ) ) {
if ( ! array_key_exists( 'iat', $item ) ) {
return __( 'None', 'indieauth' );
}

return wp_date( get_option( 'date_format' ), $item['issued_at'] );
return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['iat'] );
}

public function column_refresh_token( $item ) {
Expand Down
173 changes: 173 additions & 0 deletions includes/class-indieauth-client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php
/**
* IndieAuth Client
*/
class IndieAuth_Client {

/*
* Metadata including endpoints
*/
public $meta;

/*
* Client ID. Defaults to Home URL.
*/
public $client_id;

public function __construct() {
$this->client_id = trailingslashit( home_url() );
}

public function remote_get( $url ) {
$resp = wp_remote_get(
$url,
array(
'headers' => array(
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $resp ) ) {
return wp_error_to_oauth_response( $resp );
}

$code = (int) wp_remote_retrieve_response_code( $resp );
$body = wp_remote_retrieve_body( $resp );

if ( ( $code / 100 ) !== 2 ) {
return new WP_OAuth_Response( 'unable_retrieve', __( 'Unable to Retrieve', 'indieauth' ), $code, $body );
}

$body = json_decode( $body, true );

if ( ! is_array( $body ) ) {
return new WP_OAuth_Response( 'server_error', __( 'The endpoint did not return a JSON response', 'indieauth' ), 500 );
}

return $body;
}

public function remote_post( $url, $post_args ) {
$resp = wp_remote_post(
$url,
array(
'headers' => array(
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
),
'body' => $post_args,
)
);

$error = get_oauth_error( $resp );

if ( is_oauth_error( $error ) ) {
// Pass through well-formed error messages from the endpoint
return $error;
}

$code = (int) wp_remote_retrieve_response_code( $resp );
$body = wp_remote_retrieve_body( $resp );

if ( ( $code / 100 ) !== 2 ) {
return new WP_OAuth_Response( 'unable_retrieve', __( 'Unable to Retrieve', 'indieauth' ), $code, $body );
}

$body = json_decode( $body, true );

if ( ! is_array( $body ) ) {
return new WP_OAuth_Response( 'server_error', __( 'The endpoint did not return a JSON response', 'indieauth' ), 500 );
}

return $body;
}

/**
* Discover IndieAuth Metadata either from a Metadata Endpoint or Otherwise.
*
* @param string $url URL
*/
public function discover_endpoints( $url ) {
$endpoints = get_transient( 'indieauth_discovery_' . base64_urlencode( $url ) );
if ( $endpoints ) {
$this->meta = $endpoints;
return true;
}
$endpoints = find_rels( $url, array( 'indieauth-metadata', 'authorization_endpoint', 'token_endpoint', 'ticket_endpoint', 'micropub', 'microsub' ) );

if ( ! $endpoints ) {
return false;
}

if ( array_key_exists( 'indieauth-metadata', $endpoints ) ) {
$resp = $this->remote_get( $endpoints['indieauth-metadata'] );
if ( is_oauth_error( $resp ) ) {
return $resp;
}

$this->meta = $resp;

// Store endpoint discovery results for this URL for 3 hours.
set_transient( 'indieauth_discovery_' . base64_urlencode( $url ), $this->meta, 10800 );
return true;
} elseif ( array_key_exists( 'authorization_endpoint', $endpoints ) && array_key_exists( 'token_endpoint', $endpoints ) ) {
$this->meta = $endpoints;
// Store endpoint discovery results for this URL for 3 hours.
set_transient( 'indieauth_discovery_' . base64_urlencode( $url ), $this->meta, 10800 );
return true;
}
return false;
}

/*
* Redeem Authorization Code
*
* @param array $post_args {
* Array of Arguments to Be Passed to the the Redemption Request.
* @type string $code Authorizaton Code to be redeemed.
* @type string $redirect_uri The client's redirect URI
* @type string $code_verifier
* }
* @param boolean $token Redeem For a Token or User Profile.
* @return WP_OAuth_Response|array Return Error or Response Array.
*/
public function redeem_authorization_code( $post_args, $token = true ) {
if ( empty( $this->meta ) ) {
return new WP_OAuth_Response( 'server_error', __( 'Valid Endpoint Not Provided', 'indieauth' ), 500 );
}

$endpoint = $token ? $this->meta['token_endpoint'] : $this->meta['authorization_endpoint'];

$defaults = array(
'client_id' => $this->client_id,
'grant_type' => 'authorization_code',
);

$post_args = wp_parse_args( $post_args, $defaults );

if ( ! empty( array_diff( array( 'redirect_uri', 'code', 'code_verifier', 'client_id', 'grant_type' ), array_keys( $post_args ) ) ) ) {
return new WP_OAuth_Response( 'missing_arguments', __( 'Arguments are missing from redemption flow', 'indieauth' ), 500 );
}

$response = $this->remote_post( $endpoint, $post_args );
if ( is_oauth_error( $error ) ) {
// Pass through well-formed error messages from the endpoint
return $error;
}

// The endpoint acknowledged that the authorization code is valid and returned a me property.
if ( isset( $response['me'] ) ) {
// If this redemption is at the token endpoint
if ( $token ) {
if ( ! array_key_exists( 'access_token', $response ) ) {
return new WP_OAuth_Response( 'unknown_error', __( 'Token Endpoint did Not Return a Token', 'indieauth' ), 500 );
}
}
return $response;
}

$error = new WP_OAuth_Response( 'server_error', __( 'There was an error verifying the authorization code, the authorization server returned an expected response', 'indieauth' ), 500 );
$error->set_debug( array( 'debug' => $response ) );
return $error;
}
}
120 changes: 77 additions & 43 deletions includes/class-indieauth-ticket-endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public function __construct() {
add_action( 'template_redirect', array( $this, 'http_header' ) );
add_action( 'wp_head', array( $this, 'html_header' ) );
add_action( 'indieauth_metadata', array( $this, 'metadata' ) );
add_action( 'indieauth_ticket_redeemed', array( $this, 'notify' ) );
}

public static function get_endpoint() {
Expand Down Expand Up @@ -62,10 +63,16 @@ public function register_routes() {
/* The access token is used when acting on behalf of this URL
*/
'subject' => array(
'validate_callback' => 'rest_is_valid_url',
'validate_callback' => 'indieauth_validate_user_identifier',
'sanitize_callback' => 'esc_url_raw',
'required' => true,
),
/* The Server Issue Identifie
*/
'iss' => array(
'validate_callback' => 'indieauth_validate_issuer_identifier',
'sanitize_callback' => 'esc_url_raw',
),
),
'permission_callback' => '__return_true',
),
Expand All @@ -76,22 +83,43 @@ public function register_routes() {

// Request or revoke a token
public function post( $request ) {
$params = $request->get_params();

if ( is_array( $params['resource'] ) ) {
$token_endpoint = find_rels( $params['resource'][0], 'token_endpoint' );
$params = $request->get_params();
$clean_params = wp_array_slice_assoc( $params, array( 'subject', 'resource', 'iss' ) );
// Fires when a ticket is received with the parameters. Excludes ticket code itself
do_action( 'indieauth_ticket_received', $clean_params );
$client = new IndieAuth_Client();
$endpoints = false;

if ( array_key_exists( 'subject', $params ) ) {
$user = get_user_by_identifier( $params['subject'] );
if ( ! $user instanceof WP_User ) {
return new WP_OAuth_Response( 'invalid_request', __( 'Subject is not a user on this site', 'indieauth' ), 400 );
}
}
if ( array_key_exists( 'iss', $params ) ) {
$endpoints = $client->discover_endpoints( $params['iss'] );
} elseif ( array_key_exists( 'resource', $params ) ) {
if ( is_array( $params['resource'] ) ) {
$endpoints = $client->discover_endpoints( $params['resource'][0] );
} else {
$endpoints = $client->discover_endpoints( $params['resource'] );
}
} else {
$token_endpoint = find_rels( $params['resource'], 'token_endpoint' );
return new WP_OAuth_Response( 'invalid_request', __( 'Missing Parameters', 'indieauth' ), 400 );
}

if ( ! $endpoints ) {
return new WP_OAuth_Response( 'invalid_request', __( 'Unable to Find Endpoints', 'indieauth' ), 400 );
}

//If there is no token endpoint found return an error.
if ( ! wp_http_validate_url( $token_endpoint ) ) {
return new WP_OAuth_Response( 'invalid_request', __( 'Cannot Find Token Endpoint', 'indieauth' ), 400 );
if ( is_oauth_error( $endpoints ) ) {
return $endpoints;
}

$return = $this->request_token( $token_endpoint, $params );
$return = $this->request_token( $client->meta['token_endpoint'], $params );

if ( is_oauth_error( $return ) ) {
do_action( 'indieauth_ticket_redemption_failed', $clean_params, $return );
return $return;
}

Expand All @@ -100,16 +128,23 @@ public function post( $request ) {
$return['resource'] = $params['resource'];
}

if ( ! array_key_exists( 'iss', $return ) && array_key_exists( 'iss', $params ) ) {
$return['iss'] = $params['iss'];
}

// Add time this token was issued.
$return['iat'] = time();

// Store the Token Endpoint so it does not have to be discovered again.
$return['token_endpoint'] = $token_endpoint;
$return['token_endpoint'] = $client->meta['token_endpoint'];

$save = $this->save_token( $return );
if ( is_oauth_error( $save ) ) {
return $save;
}

// Fires when Ticket is Successfully Redeemed, omits token info.
do_action( 'indieauth_ticket_redeemed', wp_array_slice_assoc( $return, array( 'me', 'expires_in', 'iat', 'expiration', 'resource', 'iss', 'token_endpoint', 'uuid' ) ) );
return new WP_REST_Response(
array(
'success' => __( 'Your Ticket Has Been Redeemed. Thank you for your trust!', 'indieauth' ),
Expand All @@ -127,10 +162,13 @@ public function save_token( $token ) {
return new WP_OAuth_Response( 'invalid_request', __( 'Me Property Missing From Response', 'indieauth' ), 400 );
}

if ( ! indieauth_validate_user_identifier( $token['me'] ) ) {
return new WP_OAuth_Response( 'invalid_request', __( 'Invalid Me Property', 'indieauth' ), 400, $token['me'] );
}
$user = get_user_by_identifier( $token['me'] );

if ( ! $user instanceof WP_User ) {
return new WP_OAuth_Response( 'unknown', __( 'Unable to Identify User Associated with Me Property', 'indieauth' ), 500 );
return new WP_OAuth_Response( 'unknown', __( 'Unable to Identify User Associated with Me Property', 'indieauth' ), 500, $token['me'] );
}

$tokens = new External_User_Token( $user->ID );
Expand All @@ -140,44 +178,40 @@ public function save_token( $token ) {
}

public function request_token( $url, $params ) {
$resp = wp_safe_remote_post(
$client = new IndieAuth_Client();
return $client->remote_post(
$url,
array(
'headers' => array(
'Accept' => 'application/json',
),
'body' => array(
'grant_type' => 'ticket',
'ticket' => $params['ticket'],
),
'grant_type' => 'ticket',
'ticket' => $params['ticket'],
)
);
}

if ( is_wp_error( $resp ) ) {
return wp_error_to_oauth_response( $resp );
}

$code = wp_remote_retrieve_response_code( $resp );
$body = wp_remote_retrieve_body( $resp );
$return = json_decode( $body, true );

// check if response was json or not
if ( ! is_array( $return ) ) {
return new WP_OAuth_Response( 'indieauth_response_error', __( 'On Trying to Redeem a Token the Response was Invalid', 'indieauth' ), 401, $body );
}

if ( array_key_exists( 'error', $return ) ) {
return new WP_OAuth_Response( 'indieauth_' . $return['error'], esc_html( $return['error_description'] ) );
public function notify( $params ) {
$user = get_user_by_identifier( $params['me'] );
if ( ! $user ) {
return;
}

if ( 2 === (int) ( $code / 100 ) ) {
return $return;
$body = __( 'A new ticket was received and successfully redeemed', 'indieauth' ) . "\r\n";
foreach ( $params as $key => $value ) {
switch ( $key ) {
case 'iat':
$iat = new DateTime( 'now', wp_timezone() );
$iat->setTimeStamp( $value );
$body .= sprintf( 'Issued at: %s', $iat->format( DATE_W3C ) ) . "\r\n";
break;
case 'expires_in':
break;
default:
$body .= sprintf( '%s: %s', $key, $value ) . "\r\n";
}
}

return new WP_OAuth_Response(
'indieauth.invalid_access_token',
__( 'Unable to Redeem Ticket for Unknown Reasons.', 'indieauth' ),
$code
wp_mail(
$user->user_email,
wp_specialchars_decode( __( 'IndieAuth Ticket Redeemed', 'indieauth' ) ),
$body,
''
);
}
}
Loading