-
Notifications
You must be signed in to change notification settings - Fork 41
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
Added PKCE support #46
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,4 +14,7 @@ _book | |
*.epub | ||
*.mobi | ||
.idea | ||
.idea | ||
.vscode | ||
vendor | ||
composer.lock |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,11 @@ public function register_routes() { | |
'type' => 'string', | ||
'validate_callback' => 'rest_validate_request_arg', | ||
], | ||
'code_verifier' => [ | ||
'required' => false, | ||
'type' => 'string', | ||
'validate_callback' => 'rest_validate_request_arg', | ||
], | ||
], | ||
] ); | ||
} | ||
|
@@ -71,7 +76,7 @@ public function exchange_token( WP_REST_Request $request ) { | |
return $auth_code; | ||
} | ||
|
||
$is_valid = $auth_code->validate(); | ||
$is_valid = $auth_code->validate( $request ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather pass the args in separately here to avoid having the |
||
if ( is_wp_error( $is_valid ) ) { | ||
// Invalid request, but code itself exists, so we should delete | ||
// (and silently ignore errors). | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,12 @@ function bootstrap() { | |
// Admin-related. | ||
add_action( 'init', __NAMESPACE__ . '\\rest_oauth2_load_authorize_page' ); | ||
add_action( 'admin_menu', __NAMESPACE__ . '\\Admin\\register' ); | ||
|
||
// WP-Cli | ||
if ( class_exists( __NAMESPACE__ . '\\Utilities\\Oauth2_Wp_Cli' ) ) { | ||
\WP_CLI::add_command( 'oauth2', __NAMESPACE__ . '\\Utilities\\Oauth2_Wp_Cli' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
Admin\Profile\bootstrap(); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,6 +108,36 @@ public function get_expiration() { | |
return (int) $value['expiration']; | ||
} | ||
|
||
private function validate_code_verifier( $args ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
$value = $this->get_value(); | ||
|
||
if ( empty( $value['code_challenge'] ) ) { | ||
return true; | ||
} | ||
|
||
$code_verifier = $args['code_verifier']; | ||
$is_valid = false; | ||
|
||
switch ( strtolower( $value['code_challenge_method'] ) ) { | ||
case 's256': | ||
$decoded = base64_encode( hash( 'sha256', $code_verifier ) ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be |
||
$is_valid = $decoded === $value['code_challenge']; | ||
break; | ||
case 'plain': | ||
$is_valid = $code_verifier === $value['code_challenge']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both this equality check and the one above should use |
||
break; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "If the server supporting PKCE does not support the requested |
||
} | ||
|
||
if ( ! $is_valid ) { | ||
return new WP_Error( | ||
'oauth2.tokens.authorization_code.validate_code_verifier.invalid_grant', | ||
__( 'Invalid code verifier.', 'oauth2' ) | ||
); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Validate the code for use. | ||
* | ||
|
@@ -129,7 +159,15 @@ public function validate( $args = [] ) { | |
); | ||
} | ||
|
||
return true; | ||
$code_verifier = $this->validate_code_verifier( [ | ||
'code_verifier' => $args['code_verifier'], | ||
] | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Formatting is a little funky here; should be $code_verifier = $this->validate_code_verifier( [
'code_verifier' => ...,
] ); |
||
if ( is_wp_error( $code_verifier ) ) { | ||
return $code_verifier; | ||
} | ||
|
||
return $code_verifier; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather just return |
||
} | ||
|
||
/** | ||
|
@@ -183,13 +221,13 @@ public static function get_by_code( Client $client, $code ) { | |
* | ||
* @return Authorization_Code|WP_Error Authorization code instance, or error on failure. | ||
*/ | ||
public static function create( Client $client, WP_User $user ) { | ||
public static function create( Client $client, WP_User $user, $data ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
$code = wp_generate_password( static::KEY_LENGTH, false ); | ||
$meta_key = static::KEY_PREFIX . $code; | ||
$data = [ | ||
$data = \array_merge( [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initial |
||
'user' => (int) $user->ID, | ||
'expiration' => time() + static::MAX_AGE, | ||
]; | ||
], $data ); | ||
$result = add_post_meta( $client->get_post_id(), wp_slash( $meta_key ), wp_slash( $data ), true ); | ||
if ( ! $result ) { | ||
return new WP_Error( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
use WP\OAuth2\Client; | ||
|
||
abstract class Base implements Type { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be here :) |
||
/** | ||
* Handle submission of authorisation page. | ||
* | ||
|
@@ -25,6 +26,8 @@ abstract protected function handle_authorization_submission( $submit, Client $cl | |
* Handle authorisation page. | ||
*/ | ||
public function handle_authorisation() { | ||
// Should probably keep this as an option in the application (e.g. force PKCE) | ||
$pkce = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is a todo, we should file as an issue for later follow-up. |
||
|
||
if ( empty( $_GET['client_id'] ) ) { | ||
return new WP_Error( | ||
|
@@ -34,10 +37,20 @@ public function handle_authorisation() { | |
} | ||
|
||
// Gather parameters. | ||
$client_id = wp_unslash( $_GET['client_id'] ); | ||
$redirect_uri = isset( $_GET['redirect_uri'] ) ? wp_unslash( $_GET['redirect_uri'] ) : null; | ||
$scope = isset( $_GET['scope'] ) ? wp_unslash( $_GET['scope'] ) : null; | ||
$state = isset( $_GET['state'] ) ? wp_unslash( $_GET['state'] ) : null; | ||
$client_id = wp_unslash( $_GET['client_id'] ); | ||
$redirect_uri = isset( $_GET['redirect_uri'] ) ? wp_unslash( $_GET['redirect_uri'] ) : null; | ||
$scope = isset( $_GET['scope'] ) ? wp_unslash( $_GET['scope'] ) : null; | ||
$state = isset( $_GET['state'] ) ? wp_unslash( $_GET['state'] ) : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alignment is off here. |
||
|
||
if ( $pkce ) { | ||
$pkce_data = $this->handle_pkce(); | ||
if ( is_wp_error( $pkce_data ) ) { | ||
return $pkce_data; | ||
} | ||
|
||
$code_challenge = $pkce_data['code_challenge']; | ||
$code_challenge_method = $pkce_data['code_challenge_method']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could just merge |
||
} | ||
|
||
$client = Client::get_by_id( $client_id ); | ||
if ( empty( $client ) ) { | ||
|
@@ -70,7 +83,7 @@ public function handle_authorisation() { | |
|
||
// Check nonce. | ||
$nonce_action = $this->get_nonce_action( $client ); | ||
if ( ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), $none_action ) ) { | ||
if ( ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), $this->get_nonce_action( $client ) ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use |
||
return new WP_Error( | ||
'oauth2.types.authorization_code.handle_authorisation.invalid_nonce', | ||
__( 'Invalid nonce.', 'oauth2' ) | ||
|
@@ -93,7 +106,7 @@ public function handle_authorisation() { | |
|
||
$submit = wp_unslash( $_POST['wp-submit'] ); | ||
|
||
$data = compact( 'redirect_uri', 'scope', 'state' ); | ||
$data = compact( 'redirect_uri', 'scope', 'state', 'code_challenge', 'code_challenge_method' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should have default values (or just merge There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rest of the code assumes if there is no |
||
return $this->handle_authorization_submission( $submit, $client, $data ); | ||
} | ||
|
||
|
@@ -152,4 +165,44 @@ protected function render_form( Client $client, WP_Error $errors = null ) { | |
protected function get_nonce_action( Client $client ) { | ||
return sprintf( 'oauth2_authorize:%s', $client->get_id() ); | ||
} | ||
|
||
/** | ||
* Get and validate PKCE parameters from a request. | ||
* | ||
* @return string[] code_challenge and code_challenge_method | ||
*/ | ||
private function handle_pkce() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
$code_challenge = isset( $_GET['code_challenge'] ) ? wp_unslash( $_GET['code_challenge'] ) : null; | ||
$code_challenge_method = isset( $_GET['code_challenge_method'] ) ? wp_unslash( $_GET['code_challenge_method'] ) : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we instead pass |
||
|
||
if ( ! is_null( $code_challenge ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be inverted to return early instead. |
||
if ( '' === \trim( $code_challenge ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary |
||
return new WP_Error( | ||
'oauth2.types.authorization_code.handle_authorisation.code_challenge_empty', | ||
sprintf( __( 'Code challenge cannot be empty', 'oauth2' ), $client_id ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary |
||
[ | ||
'status' => WP_Http::BAD_REQUEST, | ||
'client_id' => $client_id, | ||
] | ||
); | ||
} | ||
|
||
$code_challenge_method = is_null( $code_challenge_method ) ? 'plain' : $code_challenge_method; | ||
|
||
if ( ! \in_array( \strtolower( $code_challenge_method ), [ 'plain', 's256' ], true ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary |
||
return new WP_Error( | ||
'oauth2.types.authorization_code.handle_authorisation.wrong_challenge_method', | ||
sprintf( __( 'Challenge method must be S256 or plain', 'oauth2' ), $client_id ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary |
||
[ | ||
'status' => WP_Http::BAD_REQUEST, | ||
'client_id' => $client_id, | ||
] | ||
); | ||
} | ||
|
||
return [ 'code_challenge' => $code_challenge, 'code_challenge_method' => $code_challenge_method ]; | ||
} | ||
|
||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<?php | ||
|
||
namespace WP\OAuth2\Utilities; | ||
|
||
class Oauth2_Wp_Cli extends \WP_CLI_Command { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class should just be called something like |
||
/** | ||
* Generate a code challenge. | ||
* | ||
* ## OPTIONS | ||
* | ||
* [<random_string>] | ||
* : The string to be hashed. | ||
* | ||
* | ||
* [--length=<length>] | ||
* : The length of the random seed string. | ||
* --- | ||
* default: 64 | ||
* --- | ||
* | ||
* ## EXAMPLES | ||
* | ||
* wp oauth2 generate-code-challenge --length=64 | ||
* | ||
* @alias generate-code-challenge | ||
*/ | ||
function generate_code_challenge( $args, $assoc_args ) { | ||
if ( ! empty( $args[0] ) && ! empty( $assoc_args['length'] ) ) { | ||
\WP_CLI::warning( 'Length parameter will be ignored since the input string was provided.' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
$length = empty( $assoc_args['length'] ) ? 64 : intval( $assoc_args['length'] ); | ||
|
||
if ( $length < 43 || $length > 128 ) { | ||
\WP_CLI::error( 'Length should be >= 43 and <= 128.' ); | ||
} | ||
|
||
if ( ! empty( $args[0] ) ) { | ||
$random_seed = $args[0]; | ||
|
||
if ( strlen( $random_seed ) < 43 || strlen( $random_seed ) > 128 ) { | ||
\WP_CLI::error( 'Length of the provided random seed should be >= 43 and <= 128.' ); | ||
} | ||
|
||
\WP_CLI::warning( "Using provided string {$random_seed} as a random seed. It is recommended to use this command without parameters, 64 characters long random key will be generated automatically." ); | ||
} else { | ||
$is_strong_crypto = true; | ||
$random_seed = \bin2hex( \openssl_random_pseudo_bytes( $length / 2 + $length % 2, $is_strong_crypto ) ); | ||
$random_seed = \substr( $random_seed, 0, $length ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
if ( ! $is_strong_crypto ) { | ||
\WP_CLI::error( 'openssl_random_pseudo_bytes failed to generate a cryptographically strong random string.' ); | ||
} | ||
} | ||
|
||
$code_challenge = \base64_encode( hash( 'sha256', $random_seed ) ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary |
||
|
||
$items = [ | ||
[ | ||
'code_verifier' => $random_seed, | ||
'code_challenge = base64( sha256( code_verifier ) )' => $code_challenge, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep the title a little shorter, but not sure what this actually looks like in practice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The keys are longer anyways |
||
], | ||
]; | ||
|
||
\WP_CLI\Utils\format_items( 'table', $items, [ 'code_verifier', 'code_challenge = base64( sha256( code_verifier ) )' ] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be wrapped in a code block. (Actually, we should eventually move into the proper docs, but that can happen later.)