Skip to content

Commit

Permalink
First pass at a basic form of verified software statements.
Browse files Browse the repository at this point in the history
This checks the JWT for an "iss" header. If one is present,
we fetch a public key at that host ".well-known/wp-api/oauth2.pem",
and use that public key to verify the JWT.

If a client is verified we display a "verified" message on the connect screen.

This also adds a new "--sign" flag to the CLI command to generate a signed
software statement.

Fixes #18.
  • Loading branch information
TimothyBJacobs committed May 21, 2020
1 parent ea4e11c commit ab1c964
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 19 deletions.
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

0 comments on commit ab1c964

Please sign in to comment.