-
Notifications
You must be signed in to change notification settings - Fork 42
Added PKCE support #46
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
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 ); | ||
|
Member
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' ); | ||
|
Member
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 ) { | ||
|
Member
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 ) ); | ||
|
Member
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']; | ||
|
Member
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; | ||
|
Member
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
Collaborator
Author
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'], | ||
| ] | ||
| ); | ||
|
Member
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; | ||
|
Member
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
Collaborator
Author
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 ) { | ||
|
Member
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( [ | ||
|
Member
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 { | ||
|
|
||
|
Member
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; | ||
|
Member
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; | ||
|
Member
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']; | ||
|
Member
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 ) ) ) { | ||
|
Member
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' ); | ||
|
Member
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
Collaborator
Author
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() { | ||
|
Member
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; | ||
|
Member
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 ) ) { | ||
|
Member
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 ) ) { | ||
|
Member
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 ), | ||
|
Member
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 ) ) { | ||
|
Member
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 ), | ||
|
Member
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 { | ||
|
Member
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.' ); | ||
|
Member
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 ); | ||
|
Member
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 ) ); | ||
|
Member
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, | ||
|
Member
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.
Collaborator
Author
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 ) )' ] ); | ||
|
Member
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.)