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

Add support for "verified" clients. #25

Open
wants to merge 1 commit into
base: dynamic-client-registration
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions inc/class-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace WP\OAuth2;

use WP\JWT\JWT;
use function cli\prompt;
use function WP_CLI\Utils\get_flag_value;

class Command {
Expand All @@ -30,6 +31,9 @@ class Command {
* --redirect_uri=<redirect_uri>
* : The URI users will be redirected to after connecting.
*
* [--sign=<sign>]
* : Path to key file to sign the software statement with.
*
* [--<field>=<value>]
* : Additional claims.
*
Expand All @@ -47,6 +51,7 @@ public function create_software_statement( $args, $assoc_args ) {

$name = get_flag_value( $assoc_args, 'client_name' );
$redirect_uri = get_flag_value( $assoc_args, 'redirect_uri' );
$sign = get_flag_value( $assoc_args, 'sign' );

$statement = array(
'client_uri' => $client_uri,
Expand All @@ -55,7 +60,7 @@ public function create_software_statement( $args, $assoc_args ) {
'client_name' => $name,
);

unset( $assoc_args['client_name'], $assoc_args['redirect_uri'] );
unset( $assoc_args['client_name'], $assoc_args['redirect_uri'], $assoc_args['sign'] );
$statement = array_merge( $assoc_args, $statement );

$valid = DynamicClient::validate_statement( (object) $statement );
Expand All @@ -64,7 +69,22 @@ public function create_software_statement( $args, $assoc_args ) {
\WP_CLI::error( $valid );
}

$signed = JWT::encode( $statement, '', 'none' );
if ( $sign ) {
$passphrase = prompt( 'Passphrase', '', ': ', true );
$key = openssl_pkey_get_private( 'file://' . $sign, $passphrase );

if ( ! is_resource( $key ) ) {
\WP_CLI::error( 'Invalid private key: ' . openssl_error_string() );
}

if ( ! isset( $statement['iss'] ) ) {
$statement['iss'] = $client_uri;
}

$signed = JWT::encode( $statement, $key, 'RS256' );
} else {
$signed = JWT::encode( $statement, '', 'none' );
}

if ( is_wp_error( $signed ) ) {
\WP_CLI::error( $signed );
Expand Down
105 changes: 100 additions & 5 deletions inc/class-dynamicclient.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ class DynamicClient implements ClientInterface {

const SOFTWARE_ID_KEY = '_oauth2_software_id_';
const SOFTWARE_STATEMENT_KEY = '_oauth2_software_statement';
const VERIFIED_KEY = '_oauth2_verified_statement';
const SCHEMA = array(
'type' => 'object',
'properties' => array(
'software_id' => array(
'type' => 'string',
'format' => 'uuid', // Todo support in rest_validate
'format' => 'uuid',
'required' => true,
),
'client_name' => array(
Expand Down Expand Up @@ -58,16 +59,21 @@ class DynamicClient implements ClientInterface {
/** @var \stdClass */
private $statement;

/** @var bool */
private $verified;

/** @var Client|WP_Error */
private $persisted = false;

/**
* DynamicClient constructor.
*
* @param \stdClass $statement Software Statement.
* @param bool $verified Whether the statement was verified.
*/
protected function __construct( $statement ) {
protected function __construct( $statement, $verified ) {
$this->statement = $statement;
$this->verified = $verified;
}

/**
Expand All @@ -78,14 +84,85 @@ protected function __construct( $statement ) {
* @return DynamicClient|WP_Error
*/
public static function from_jwt( $jwt ) {
$statement = JWT::decode( $jwt, '', array( 'none' ), 'unsecure' );
$valid = static::validate_statement( $statement );
$iss = JWT::get_claim( $jwt, 'iss' );

if ( ! is_wp_error( $iss ) ) {
$key = self::get_signing_key( $iss );

if ( is_wp_error( $key ) ) {
return $key;
}

$statement = JWT::decode( $jwt, $key, array( 'RS256' ) );
$verified = true;
} else {
$statement = JWT::decode( $jwt, '', array( 'none' ), 'unsecure' );
$verified = false;
}

$valid = static::validate_statement( $statement );

if ( is_wp_error( $valid ) ) {
return $valid;
}

return new static( $statement );
return new static( $statement, $verified );
}

/**
* Gets the signing key for a JWT based on its ISS.
*
* @param string $iss
*
* @return resource|WP_Error
*/
protected static function get_signing_key( $iss ) {
$host = parse_url( $iss, PHP_URL_HOST );

if ( ! $host ) {
return new WP_Error( 'invalid_host', __( 'Could not get a valid host.', 'oauth2' ) );
}

$body = self::fetch_signing_key( $host );

if ( ! $body ) {
return new WP_Error( 'empty_body', __( 'Empty body returned by at the well known URL.', 'oauth2' ) );
}

$key = openssl_pkey_get_public( $body );

if ( ! is_resource( $key ) ) {
return new WP_Error( 'invalid_key', sprintf( __( 'Invalid public key: %s.', 'oauth2' ), openssl_error_string() ?: 'unknown' ) );
}

return $key;
}

/**
* Fetch the signing key from the given hostname.
*
* @param string $host
*
* @return string|WP_Error
*/
protected static function fetch_signing_key( $host ) {
$transient = 'oauth2_key_' . $host;

if ( false === ( $body = get_site_transient( $transient ) ) || ! is_string( $body ) ) {
$url = 'https://' . $host . '/.well-known/wp-api/oauth2.pem';

$response = wp_safe_remote_get( $url );

if ( is_wp_error( $response ) ) {
$body = '';
} else {
$body = trim( wp_remote_retrieve_body( $response ) );
}

set_site_transient( $transient, $body, 5 * MINUTE_IN_SECONDS );
}

return $body;
}

/**
Expand All @@ -112,6 +189,14 @@ public static function validate_statement( $statement ) {
}
}

if ( isset( $statement->iss ) ) {
$iss_host = parse_url( $statement->iss, PHP_URL_HOST );

if ( ! $iss_host || $iss_host !== $client_host ) {
return new WP_Error( 'client_uri_mismatch', __( 'The statement issuing URI is not on the same domain as the client URI.', 'oauth2' ) );
}
}

return true;
}

Expand Down Expand Up @@ -233,6 +318,15 @@ public function get_software_statement() {
return $this->statement;
}

/**
* Checks if the software statement was verified as being signed by the client_uri.
*
* @return bool
*/
public function is_verified() {
return $this->verified;
}

/**
* Persists a dynamic client to a real client.
*
Expand Down Expand Up @@ -295,6 +389,7 @@ protected function create_persisted_dynamic_client() {

update_post_meta( $client->get_post_id(), static::SOFTWARE_ID_KEY . $this->get_id(), 1 );
update_post_meta( $client->get_post_id(), static::SOFTWARE_STATEMENT_KEY, $this->statement );
update_post_meta( $client->get_post_id(), static::VERIFIED_KEY, $this->is_verified() );

if ( current_user_can( 'publish_post', $client->get_post_id() ) ) {
$approved = $client->approve();
Expand Down
45 changes: 33 additions & 12 deletions theme/oauth2-authorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,25 @@
float: left;
}

.new-client-warning {
#login .notice {
margin: 5px 0 15px;
background-color: #fff8e5;
background-color: #fff;
border: 1px solid #ccd0d4;
border-left-color: #ffb900;
border-left-width: 4px;
padding: 1px 12px;
}

#login .new-client-warning p {
#login .notice-warning {
background-color: #fff8e5;
border-left-color: #ffb900;
}

#login .notice-success {
background-color: #ecf7ed;
border-left-color: #46b450;
}

#login .notice p {
margin: 0.5em 0;
padding: 2px;
}
Expand Down Expand Up @@ -105,15 +114,27 @@
);

if ( $client instanceof \WP\OAuth2\DynamicClient ) {
printf(
'<p class="client-description">%s</p>',
sprintf(
if ( $client->is_verified() ) {
printf(
'<div class="notice notice-success notice-alt"><p>%s</p></div>',
sprintf(
/* translators: %1$s: client name. %2$s: the app URI. */
__( '%1$s is an application by %2$s.', 'oauth2' ),
esc_html( $client->get_name() ),
sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer"><code>%1$s</code></a>', esc_url( $client->get_software_statement()->client_uri ) )
)
);
__( '%1$s is verified to be an application by %2$s.', 'oauth2' ),
esc_html( $client->get_name() ),
sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer"><code>%1$s</code></a>', esc_url( $client->get_software_statement()->client_uri ) )
)
);
} else {
printf(
'<p class="client-description">%s</p>',
sprintf(
/* translators: %1$s: client name. %2$s: the app URI. */
__( '%1$s is an application by %2$s.', 'oauth2' ),
esc_html( $client->get_name() ),
sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer"><code>%1$s</code></a>', esc_url( $client->get_software_statement()->client_uri ) )
)
);
}
}
?>

Expand Down