Skip to content

Commit

Permalink
Merge pull request #259 from dshanske/refinetickets
Browse files Browse the repository at this point in the history
Refine Tickets
  • Loading branch information
dshanske authored Dec 21, 2023
2 parents 7bf0ee1 + eaf8f27 commit 47ce29f
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 182 deletions.
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

0 comments on commit 47ce29f

Please sign in to comment.