From b6c7c28aa6d5c1091c07998890c68281d74bb27f Mon Sep 17 00:00:00 2001 From: David Shanske Date: Mon, 11 Dec 2023 04:02:30 +0000 Subject: [PATCH 1/9] Introduce IndieAuth Client Class to abstract client functionality. --- includes/class-indieauth-client.php | 136 ++++++++++++++++++++++++++++ includes/class-web-signin.php | 105 +++------------------ indieauth.php | 1 + 3 files changed, 148 insertions(+), 94 deletions(-) create mode 100644 includes/class-indieauth-client.php diff --git a/includes/class-indieauth-client.php b/includes/class-indieauth-client.php new file mode 100644 index 0000000..f09d339 --- /dev/null +++ b/includes/class-indieauth-client.php @@ -0,0 +1,136 @@ +client_id = trailingslashit( home_url() ); + } + + /** + * 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; + } elseif ( array_key_exists( 'indieauth-metadata', $endpoints ) ) { + $resp = wp_remote_get( + $endpoints['indieauth-metadata'], + array( + 'headers' => array( + 'Accept' => 'application/json', + ), + ) + ); + if ( is_wp_error( $resp ) ) { + return $resp; + } + + $code = (int) wp_remote_retrieve_response_code( $resp ); + + if ( ( $code / 100 ) !== 2 ) { + return new WP_Error( 'no_metadata_endpoint', __( 'No Metadata Endpoint Found', 'indieauth' ) ); + } + + $body = wp_remote_retrieve_body( $resp ); + $this->meta = json_decode( $body, true ); + // 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 ); + } + + $args = array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + 'body' => $post_args, + ); + $response = wp_remote_post( $endpoint, $args ); + $error = get_oauth_error( $response ); + if ( is_oauth_error( $error ) ) { + // Pass through well-formed error messages from the endpoint + return $error; + } + $code = wp_remote_retrieve_response_code( $response ); + $response = wp_remote_retrieve_body( $response ); + + $response = json_decode( $response, true ); + // check if response was json or not + if ( ! is_array( $response ) ) { + return new WP_OAuth_Response( 'server_error', __( 'The authorization endpoint did not return a JSON response', 'indieauth' ), 500 ); + } + + // The endpoint acknowledged that the authorization code is valid and returned a me property. + if ( 2 === (int) ( $code / 100 ) && 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; + } +} diff --git a/includes/class-web-signin.php b/includes/class-web-signin.php index 4c56eeb..db0cde6 100644 --- a/includes/class-web-signin.php +++ b/includes/class-web-signin.php @@ -44,8 +44,8 @@ public function websignin_redirect( $me, $redirect_uri ) { ) ); } - $endpoints = find_rels( $me, array( 'indieauth-metadata', 'authorization_endpoint' ) ); - + $client = new IndieAuth_Client(); + $endpoints = $client->discover_endpoints( $me ); if ( ! $endpoints ) { return new WP_Error( 'authentication_failed', @@ -54,22 +54,9 @@ public function websignin_redirect( $me, $redirect_uri ) { 'status' => 401, ) ); - } elseif ( array_key_exists( 'indieauth-metadata', $endpoints ) ) { - $state = $this->get_indieauth_metadata( $endpoints['indieauth-metadata'] ); - } elseif ( ! array_key_exists( 'authorization_endpoint', $endpoints ) ) { - return new WP_Error( - 'authentication_failed', - __( 'ERROR: Could not discover endpoints', 'indieauth' ), - array( - 'status' => 401, - ) - ); - } else { - $state = array( - 'me' => $me, - 'authorization_endpoint' => $endpoints['authorization_endpoint'], - ); } + + $state = $client->meta; $state['me'] = $me; $state['code_verifier'] = wp_generate_password( 128, false ); @@ -77,92 +64,19 @@ public function websignin_redirect( $me, $redirect_uri ) { $query = add_query_arg( array( 'response_type' => 'code', // In earlier versions of the specification this was ID. - 'client_id' => rawurlencode( home_url() ), + 'client_id' => rawurlencode( $client->client_id ), 'redirect_uri' => rawurlencode( $redirect_uri ), 'state' => $token->set_with_cookie( $state, 120 ), 'code_challenge' => base64_urlencode( indieauth_hash( $state['code_verifier'] ) ), 'code_challenge_method' => 'S256', 'me' => rawurlencode( $me ), ), - $endpoints['authorization_endpoint'] + $state['authorization_endpoint'] ); // redirect to authentication endpoint wp_redirect( $query ); } - // Retrieves the Metadata from an IndieAuth Metadata Endpoint. - public function get_indieauth_metadata( $url ) { - $resp = wp_remote_get( - $url, - array( - 'headers' => array( - 'Accept' => 'application/json', - ), - ) - ); - if ( is_wp_error( $resp ) ) { - return $resp; - } - - $code = (int) wp_remote_retrieve_response_code( $resp ); - - if ( ( $code / 100 ) !== 2 ) { - return new WP_Error( 'no_metadata_endpoint', __( 'No Metadata Endpoint Found', 'indieauth' ) ); - } - - $body = wp_remote_retrieve_body( $resp ); - return json_decode( $body, true ); - } - - - - // $args must consist of redirect_uri, client_id, and code - public function verify_authorization_code( $post_args, $endpoint ) { - if ( ! wp_http_validate_url( $endpoint ) ) { - return new WP_OAuth_Response( 'server_error', __( 'Did Not Receive a Valid Authorization Endpoint', 'indieauth' ), 500 ); - } - - $defaults = array( - 'client_id' => home_url(), - 'grant_type' => 'authorization_code', - ); - - $post_args = wp_parse_args( $post_args, $defaults ); - $args = array( - 'headers' => array( - 'Accept' => 'application/json', - 'Content-Type' => 'application/x-www-form-urlencoded', - ), - 'body' => $post_args, - ); - $response = wp_remote_post( $endpoint, $args ); - $error = get_oauth_error( $response ); - if ( is_oauth_error( $error ) ) { - // Pass through well-formed error messages from the authorization endpoint - return $error; - } - $code = wp_remote_retrieve_response_code( $response ); - $response = wp_remote_retrieve_body( $response ); - - $response = json_decode( $response, true ); - // check if response was json or not - if ( ! is_array( $response ) ) { - return new WP_OAuth_Response( 'server_error', __( 'The authorization endpoint did not return a JSON response', 'indieauth' ), 500 ); - } - - if ( 2 === (int) ( $code / 100 ) && isset( $response['me'] ) ) { - // The authorization endpoint acknowledged that the authorization code - // is valid and returned the authorization info - return $response; - } - - $error = new WP_OAuth_Response( 'server_error', __( 'There was an error verifying the authorization code, the authorization server return an expected response', 'indieauth' ), 500 ); - $error->set_debug( array( 'debug' => $response ) ); - return $error; - } - - - /** * Authenticate user to WordPress using IndieAuth. * @@ -200,14 +114,17 @@ public function authenticate( $user, $url ) { return new WP_Error( 'indieauth_iss_error', __( 'Issuer Parameter Present in Metadata Endpoint But Not Returned by Authorization Endpoint', 'indieauth' ) ); } - $response = $this->verify_authorization_code( + $client = new IndieAuth_Client(); + $client->meta = $state; + $response = $client->redeem_authorization_code( array( 'code' => $_REQUEST['code'], 'redirect_uri' => wp_login_url( $redirect_to ), 'code_verifier' => $state['code_verifier'], ), - $state['authorization_endpoint'] + false // Redeem at Authorization Endpoint ); + if ( is_wp_error( $response ) ) { return $response; } diff --git a/indieauth.php b/indieauth.php index 59d0d5a..adeb0fe 100644 --- a/indieauth.php +++ b/indieauth.php @@ -92,6 +92,7 @@ public static function plugins_loaded() { array( 'functions.php', // Global Functions 'class-oauth-response.php', // OAuth REST Error Class + 'class-indieauth-client.php', // IndieAuth Client Class 'class-indieauth-metadata-endpoint.php', // Metadata Endpoint 'class-token-generic.php', // Token Base Class 'class-token-user.php', From 85f8f5a23e880a43cf8ce9f0351cd1dca021f7d4 Mon Sep 17 00:00:00 2001 From: David Shanske Date: Mon, 11 Dec 2023 04:19:34 +0000 Subject: [PATCH 2/9] Add remote_get and remote_post functions that set common parameters --- includes/class-indieauth-client.php | 104 +++++++++++++++++++--------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/includes/class-indieauth-client.php b/includes/class-indieauth-client.php index f09d339..9b95440 100644 --- a/includes/class-indieauth-client.php +++ b/includes/class-indieauth-client.php @@ -18,6 +18,71 @@ public function __construct() { $this->client_id = trailingslashit( home_url() ); } + private 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; + } + + private 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( $response ); + + 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. * @@ -34,26 +99,13 @@ public function discover_endpoints( $url ) { if ( ! $endpoints ) { return false; } elseif ( array_key_exists( 'indieauth-metadata', $endpoints ) ) { - $resp = wp_remote_get( - $endpoints['indieauth-metadata'], - array( - 'headers' => array( - 'Accept' => 'application/json', - ), - ) - ); - if ( is_wp_error( $resp ) ) { + $resp = $this->remote_get( $endpoints['indieauth-metadata'] ); + if ( is_oauth_error( $resp ) ) { return $resp; } - $code = (int) wp_remote_retrieve_response_code( $resp ); - - if ( ( $code / 100 ) !== 2 ) { - return new WP_Error( 'no_metadata_endpoint', __( 'No Metadata Endpoint Found', 'indieauth' ) ); - } + $this->meta = $resp; - $body = wp_remote_retrieve_body( $resp ); - $this->meta = json_decode( $body, true ); // Store endpoint discovery results for this URL for 3 hours. set_transient( 'indieauth_discovery_' . base64_urlencode( $url ), $this->meta, 10800 ); return true; @@ -96,30 +148,14 @@ public function redeem_authorization_code( $post_args, $token = true ) { return new WP_OAuth_Response( 'missing_arguments', __( 'Arguments are missing from redemption flow', 'indieauth' ), 500 ); } - $args = array( - 'headers' => array( - 'Accept' => 'application/json', - 'Content-Type' => 'application/x-www-form-urlencoded', - ), - 'body' => $post_args, - ); - $response = wp_remote_post( $endpoint, $args ); - $error = get_oauth_error( $response ); + $response = $this->remote_post( $endpoint, $post_args ); if ( is_oauth_error( $error ) ) { // Pass through well-formed error messages from the endpoint return $error; } - $code = wp_remote_retrieve_response_code( $response ); - $response = wp_remote_retrieve_body( $response ); - - $response = json_decode( $response, true ); - // check if response was json or not - if ( ! is_array( $response ) ) { - return new WP_OAuth_Response( 'server_error', __( 'The authorization endpoint did not return a JSON response', 'indieauth' ), 500 ); - } // The endpoint acknowledged that the authorization code is valid and returned a me property. - if ( 2 === (int) ( $code / 100 ) && isset( $response['me'] ) ) { + if ( isset( $response['me'] ) ) { // If this redemption is at the token endpoint if ( $token ) { if ( ! array_key_exists( 'access_token', $response ) ) { From c8add54483ecb30cca89b7e3c52138ee7d6ccca7 Mon Sep 17 00:00:00 2001 From: David Shanske Date: Sun, 17 Dec 2023 22:00:45 +0000 Subject: [PATCH 3/9] Fix issues with discovery not checking html if it finds any matches in http. --- includes/class-indieauth-client.php | 10 +-- includes/class-indieauth-ticket-endpoint.php | 75 ++++++++------------ includes/functions.php | 42 ++++------- 3 files changed, 50 insertions(+), 77 deletions(-) diff --git a/includes/class-indieauth-client.php b/includes/class-indieauth-client.php index 9b95440..62d3234 100644 --- a/includes/class-indieauth-client.php +++ b/includes/class-indieauth-client.php @@ -18,7 +18,7 @@ public function __construct() { $this->client_id = trailingslashit( home_url() ); } - private function remote_get( $url ) { + public function remote_get( $url ) { $resp = wp_remote_get( $url, array( @@ -47,7 +47,7 @@ private function remote_get( $url ) { return $body; } - private function remote_post( $url, $post_args ) { + public function remote_post( $url, $post_args ) { $resp = wp_remote_post( $url, array( @@ -59,7 +59,7 @@ private function remote_post( $url, $post_args ) { ) ); - $error = get_oauth_error( $response ); + $error = get_oauth_error( $resp ); if ( is_oauth_error( $error ) ) { // Pass through well-formed error messages from the endpoint @@ -98,7 +98,9 @@ public function discover_endpoints( $url ) { if ( ! $endpoints ) { return false; - } elseif ( array_key_exists( 'indieauth-metadata', $endpoints ) ) { + } + + if ( array_key_exists( 'indieauth-metadata', $endpoints ) ) { $resp = $this->remote_get( $endpoints['indieauth-metadata'] ); if ( is_oauth_error( $resp ) ) { return $resp; diff --git a/includes/class-indieauth-ticket-endpoint.php b/includes/class-indieauth-ticket-endpoint.php index 282a8d2..92b11f7 100644 --- a/includes/class-indieauth-ticket-endpoint.php +++ b/includes/class-indieauth-ticket-endpoint.php @@ -76,20 +76,32 @@ 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(); + $client = new IndieAuth_Client(); + $endpoints = false; + + 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 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 ( ! $endpoints ) { + error_log( wp_json_encode( $client ) ); + return new WP_OAuth_Response( 'invalid_request', __( 'Unable to Find Endpoints', 'indieauth' ), 400 ); } - $return = $this->request_token( $token_endpoint, $params ); + if ( is_oauth_error( $endpoints ) ) { + return $endpoints; + } + + $return = $this->request_token( $client->meta['token_endpoint'], $params ); if ( is_oauth_error( $return ) ) { return $return; @@ -104,7 +116,7 @@ public function post( $request ) { $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 ) ) { @@ -127,6 +139,10 @@ 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 ); + } + $user = get_user_by_identifier( $token['me'] ); if ( ! $user instanceof WP_User ) { @@ -140,44 +156,13 @@ 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'] ) ); - } - - if ( 2 === (int) ( $code / 100 ) ) { - return $return; - } - - return new WP_OAuth_Response( - 'indieauth.invalid_access_token', - __( 'Unable to Redeem Ticket for Unknown Reasons.', 'indieauth' ), - $code - ); } } diff --git a/includes/functions.php b/includes/functions.php index 4186855..753ace5 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -63,7 +63,7 @@ function find_rels( $me, $endpoints = null ) { 'redirection' => 3, 'user-agent' => "$user_agent; finding rel properties", ); - $response = wp_safe_remote_head( $me, $args ); + $response = wp_safe_remote_get( $me, $args ); if ( is_wp_error( $response ) ) { return $response; } @@ -74,48 +74,34 @@ function find_rels( $me, $endpoints = null ) { if ( is_string( $links ) ) { $links = array( $links ); } - $return = parse_link_rels( $links, $me ); + $rels = parse_link_rels( $links, $me ); } - if ( $return ) { + if ( $rels ) { $code = (int) wp_remote_retrieve_response_code( $response ); switch ( $code ) { case 301: case 308: - $return['me'] = wp_remote_retrieve_header( $response, 'Location' ); + $rels['me'] = wp_remote_retrieve_header( $response, 'Location' ); break; } - if ( isset( $return['me'] ) ) { + if ( isset( $rels['me'] ) ) { $me = $return['me']; } - if ( is_array( $endpoints ) ) { - $return = wp_array_slice_assoc( $return, $endpoints ); - if ( ! empty( $return ) ) { - return $return; - } - } - if ( is_string( $endpoints ) && isset( $return[ $endpoints ] ) ) { - return $return[ $endpoints ]; - } } // not an (x)html, sgml, or xml page, no use going further - if ( preg_match( '#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'content-type' ) ) ) { - return false; - } - // now do a GET since we're going to look in the html headers (and we're sure its not a binary file) - $response = wp_safe_remote_get( $me, $args ); - if ( is_wp_error( $response ) ) { - return false; + if ( ! preg_match( '#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'content-type' ) ) ) { + $contents = wp_remote_retrieve_body( $response ); + $rels = array_merge( $rels, parse_html_rels( $contents, $me ) ); } - $contents = wp_remote_retrieve_body( $response ); - $return = parse_html_rels( $contents, $me ); if ( is_array( $endpoints ) ) { - $return = wp_array_slice_assoc( $return, $endpoints ); - if ( ! empty( $return ) ) { - return $return; + $endpoints[] = 'me'; + $rels = wp_array_slice_assoc( $rels, $endpoints ); + if ( ! empty( $rels ) ) { + return $rels; } - } elseif ( is_string( $endpoints ) && isset( $return[ $endpoints ] ) ) { - return $return[ $endpoints ]; + } elseif ( is_string( $endpoints ) && isset( $rels[ $endpoints ] ) ) { + return $rels[ $endpoints ]; } return false; } From 2fabf03c997d3b0b36b35bec7261acdccd671d50 Mon Sep 17 00:00:00 2001 From: David Shanske Date: Sun, 17 Dec 2023 23:14:46 +0000 Subject: [PATCH 4/9] Fix ticket endpoint and add additional checks. --- includes/class-external-token-table.php | 4 ++-- includes/class-indieauth-ticket-endpoint.php | 23 ++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/includes/class-external-token-table.php b/includes/class-external-token-table.php index 4691a5b..35b1c82 100644 --- a/includes/class-external-token-table.php +++ b/includes/class-external-token-table.php @@ -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 ) { diff --git a/includes/class-indieauth-ticket-endpoint.php b/includes/class-indieauth-ticket-endpoint.php index 92b11f7..bf11319 100644 --- a/includes/class-indieauth-ticket-endpoint.php +++ b/includes/class-indieauth-ticket-endpoint.php @@ -62,10 +62,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', ), @@ -80,6 +86,12 @@ public function post( $request ) { $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 ) ) { @@ -112,6 +124,10 @@ 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(); @@ -140,13 +156,12 @@ public function save_token( $token ) { } if ( ! indieauth_validate_user_identifier( $token['me'] ) ) { - return new WP_OAuth_Response( 'invalid_request', __( 'Invalid Me Property', 'indieauth' ), 400 ); + 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 ); From 1fe887722e48a916c8381fcffe572c4de6591339 Mon Sep 17 00:00:00 2001 From: David Shanske Date: Thu, 21 Dec 2023 17:02:06 +0000 Subject: [PATCH 5/9] Add hooks for ticket redemption flow --- includes/class-indieauth-ticket-endpoint.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-indieauth-ticket-endpoint.php b/includes/class-indieauth-ticket-endpoint.php index bf11319..1bf7425 100644 --- a/includes/class-indieauth-ticket-endpoint.php +++ b/includes/class-indieauth-ticket-endpoint.php @@ -83,6 +83,9 @@ public function register_routes() { // Request or revoke a token public function post( $request ) { $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; @@ -105,7 +108,6 @@ public function post( $request ) { } if ( ! $endpoints ) { - error_log( wp_json_encode( $client ) ); return new WP_OAuth_Response( 'invalid_request', __( 'Unable to Find Endpoints', 'indieauth' ), 400 ); } @@ -116,6 +118,7 @@ public function post( $request ) { $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; } @@ -138,6 +141,9 @@ public function post( $request ) { 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' ), From f8b1984599dafe8edeaaaa808277e79b9386397e Mon Sep 17 00:00:00 2001 From: David Shanske Date: Thu, 21 Dec 2023 17:16:47 +0000 Subject: [PATCH 6/9] Add email notification for tickets received and redeemed --- includes/class-indieauth-ticket-endpoint.php | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/includes/class-indieauth-ticket-endpoint.php b/includes/class-indieauth-ticket-endpoint.php index 1bf7425..d7ac189 100644 --- a/includes/class-indieauth-ticket-endpoint.php +++ b/includes/class-indieauth-ticket-endpoint.php @@ -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() { @@ -186,4 +187,32 @@ public function request_token( $url, $params ) { ) ); } + + public function notify( $params ) { + $user = get_user_by_identifier( $params['me'] ); + if ( ! $user ) { + return; + } + $body = __( 'A new ticket was received and successfully redeemed' ) . "\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"; + } + } + wp_mail( + $user->user_email, + wp_specialchars_decode( __( 'IndieAuth Ticket Redeemed', 'indieauth' ) ), + $body, + '' + ); + + } } From 94af0302d21de1b61757419259c3e5aa9487c50c Mon Sep 17 00:00:00 2001 From: David Shanske Date: Thu, 21 Dec 2023 17:19:46 +0000 Subject: [PATCH 7/9] Minor fixes highlighted by test --- includes/class-indieauth-ticket-endpoint.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/class-indieauth-ticket-endpoint.php b/includes/class-indieauth-ticket-endpoint.php index d7ac189..82c9002 100644 --- a/includes/class-indieauth-ticket-endpoint.php +++ b/includes/class-indieauth-ticket-endpoint.php @@ -83,7 +83,7 @@ public function register_routes() { // Request or revoke a token public function post( $request ) { - $params = $request->get_params(); + $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 ); @@ -93,7 +93,7 @@ public function post( $request ) { 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 ); + return new WP_OAuth_Response( 'invalid_request', __( 'Subject is not a user on this site', 'indieauth' ), 400 ); } } if ( array_key_exists( 'iss', $params ) ) { @@ -193,9 +193,9 @@ public function notify( $params ) { if ( ! $user ) { return; } - $body = __( 'A new ticket was received and successfully redeemed' ) . "\r\n"; - foreach( $params as $key => $value ) { - switch( $key ) { + $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 ); @@ -207,9 +207,9 @@ public function notify( $params ) { $body .= sprintf( '%s: %s', $key, $value ) . "\r\n"; } } - wp_mail( - $user->user_email, - wp_specialchars_decode( __( 'IndieAuth Ticket Redeemed', 'indieauth' ) ), + wp_mail( + $user->user_email, + wp_specialchars_decode( __( 'IndieAuth Ticket Redeemed', 'indieauth' ) ), $body, '' ); From ce026e9b2b3953e62460b31a68806fa606bef91a Mon Sep 17 00:00:00 2001 From: David Shanske Date: Thu, 21 Dec 2023 17:23:01 +0000 Subject: [PATCH 8/9] PHPCS fixes --- includes/class-indieauth-client.php | 5 ++--- includes/class-indieauth-ticket-endpoint.php | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/includes/class-indieauth-client.php b/includes/class-indieauth-client.php index 62d3234..45fe7c3 100644 --- a/includes/class-indieauth-client.php +++ b/includes/class-indieauth-client.php @@ -80,7 +80,6 @@ public function remote_post( $url, $post_args ) { } return $body; - } /** @@ -124,8 +123,8 @@ public function discover_endpoints( $url ) { * 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. + * 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 * } diff --git a/includes/class-indieauth-ticket-endpoint.php b/includes/class-indieauth-ticket-endpoint.php index 82c9002..9b0817e 100644 --- a/includes/class-indieauth-ticket-endpoint.php +++ b/includes/class-indieauth-ticket-endpoint.php @@ -213,6 +213,5 @@ public function notify( $params ) { $body, '' ); - } } From eaf8f27fc8eaff79941f69183cdb06114941164e Mon Sep 17 00:00:00 2001 From: David Shanske Date: Thu, 21 Dec 2023 17:30:00 +0000 Subject: [PATCH 9/9] Fix variable name change --- includes/functions.php | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 753ace5..72a64c6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -45,7 +45,7 @@ function parse_link_rels( $links, $url ) { if ( ! function_exists( 'find_rels' ) ) { function find_rels( $me, $endpoints = null ) { if ( ! $endpoints ) { - $endpoints = array( 'authorization_endpoint', 'token_endpoint', 'me' ); + $endpoints = array( 'indieauth-metadata', 'authorization_endpoint', 'token_endpoint', 'me' ); } if ( ! wp_http_validate_url( $me ) ) { // Not an URL. This should never happen. return false; @@ -67,7 +67,7 @@ function find_rels( $me, $endpoints = null ) { if ( is_wp_error( $response ) ) { return $response; } - $return = array(); + $rels = array(); // check link header $links = wp_remote_retrieve_header( $response, 'link' ); if ( $links ) { @@ -76,17 +76,16 @@ function find_rels( $me, $endpoints = null ) { } $rels = parse_link_rels( $links, $me ); } - if ( $rels ) { - $code = (int) wp_remote_retrieve_response_code( $response ); - switch ( $code ) { - case 301: - case 308: - $rels['me'] = wp_remote_retrieve_header( $response, 'Location' ); - break; - } - if ( isset( $rels['me'] ) ) { - $me = $return['me']; - } + + $code = (int) wp_remote_retrieve_response_code( $response ); + switch ( $code ) { + case 301: + case 308: + $rels['me'] = wp_remote_retrieve_header( $response, 'Location' ); + break; + } + if ( isset( $rels['me'] ) ) { + $me = $rels['me']; } // not an (x)html, sgml, or xml page, no use going further @@ -119,13 +118,13 @@ function parse_html_rels( $contents, $url ) { libxml_use_internal_errors( true ); $doc = new DOMDocument(); $doc->loadHTML( $contents ); - $xpath = new DOMXPath( $doc ); - $return = array(); + $xpath = new DOMXPath( $doc ); + $results = array(); // check and elements foreach ( $xpath->query( '//a[@rel and @href] | //link[@rel and @href]' ) as $hyperlink ) { - $return[ $hyperlink->getAttribute( 'rel' ) ] = WP_Http::make_absolute_url( $hyperlink->getAttribute( 'href' ), $url ); + $results[ $hyperlink->getAttribute( 'rel' ) ] = WP_Http::make_absolute_url( $hyperlink->getAttribute( 'href' ), $url ); } - return $return; + return $results; } }