From 997eed1f99fe57d0fb182a40bf6d005f2703560b Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 22 Feb 2024 15:27:57 -0500 Subject: [PATCH 1/8] Starting on authenticated users --- README.md | 27 ++++++++------------------- cli.php | 13 ++++++++++--- plugin.php | 32 ++++++++++++++++++++++---------- tests/test-rest-api-guard.php | 9 ++++++--- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 918148f..ff46f03 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ add_filter( ); ``` -### Require JSON Web Token (JWT) Authentication +### Require JSON Web Token (JWT) Authentication for Anonymous Users Anonymous users can be required to authenticate via a JSON Web Token (JWT) to access the REST API. This can be configured in the plugin's settings or via @@ -134,36 +134,25 @@ Out of the box, the plugin will look for a JWT in the `Authorization: Bearer plugin's settings or via code: ```php -add_filter( - 'rest_api_guard_jwt_audience', - function ( string $audience ): string { - return 'custom-audience'; - } -); +add_filter( 'rest_api_guard_jwt_audience', fn ( string $audience ) => 'custom-audience' ); -add_filter( - 'rest_api_guard_jwt_issuer', - function ( string $issuer ): string { - return 'https://example.com'; - } -); +add_filter( 'rest_api_guard_jwt_issuer', fn ( string $issuer ) => 'https://example.com' ); ``` The JWT's secret will be autogenerated and stored in the `rest_api_guard_jwt_secret` option. The secret can also be filtered via code: ```php -add_filter( - 'rest_api_guard_jwt_secret', - function ( string $secret ): string { - return 'my-custom-secret'; - } -); +add_filter( 'rest_api_guard_jwt_secret', fn ( string $secret ) => 'my-custom-secret' ); ``` You can generate a JWT for use with the REST API by calling the `wp rest-api-guard generate-jwt` command. +### Allow JWT Authentication for Authenticated Users + +Authenticated users can be authenticated with the REST API via a JSON Web Token. + ## Testing Run `composer test` to run tests against PHPUnit and the PHP code in the plugin. diff --git a/cli.php b/cli.php index 9eadb43..6bb2480 100644 --- a/cli.php +++ b/cli.php @@ -9,10 +9,17 @@ WP_CLI::add_command( 'rest-api-guard generate-jwt', - function () { - echo generate_jwt() . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + function ( $args, $assoc_args ) { + $expiration = isset( $assoc_args['expiration'] ) ? (int) $assoc_args['expiration'] : null; + $user = isset( $assoc_args['user'] ) ? (int) $assoc_args['user'] : null; + + echo generate_jwt( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + expiration: $expiration, + user: $user, + ) . PHP_EOL; }, [ 'shortdesc' => __( 'Generate a JSON Web Token (JWT).', 'rest-api-guard' ), - ] + 'synopsis' => '[--expiration=] [--user=]', + ], ); diff --git a/plugin.php b/plugin.php index 315b0f2..c3bc314 100644 --- a/plugin.php +++ b/plugin.php @@ -23,6 +23,7 @@ use WP_Error; use WP_REST_Request; use WP_REST_Server; +use WP_User; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -297,18 +298,29 @@ function get_jwt_secret(): string { /** * Generate a JSON Web Token (JWT). * + * The JWT payload is intentionally not filtered to prevent + * + * @param int|null $expiration The expiration time of the JWT in seconds or null for no expiration. + * @param WP_User|null $user The user to include in the JWT or null for no user. * @return string */ -function generate_jwt(): string { - return JWT::encode( - [ - 'iss' => get_jwt_issuer(), - 'aud' => get_jwt_audience(), - 'iat' => time(), - ], - get_jwt_secret(), - 'HS256' - ); +function generate_jwt( ?int $expiration = null, ?WP_User $user = null ): string { + $payload = [ + 'iss' => get_jwt_issuer(), + 'aud' => get_jwt_audience(), + 'iat' => time(), + ]; + + if ( null !== $expiration ) { + $payload['exp'] = time() + $expiration; + } + + if ( null !== $user ) { + $payload['sub'] = $user->ID; + $payload['user_login'] = $user->user_login; + } + + return JWT::encode( $payload, get_jwt_secret(), 'HS256' ); } if ( defined( 'WP_CLI' ) && WP_CLI ) { diff --git a/tests/test-rest-api-guard.php b/tests/test-rest-api-guard.php index b2b4a9f..89968b0 100644 --- a/tests/test-rest-api-guard.php +++ b/tests/test-rest-api-guard.php @@ -214,9 +214,9 @@ public function test_prevent_access_denylist_priority() { } /** - * @dataProvider jwtDataProvider + * @dataProvider jwtDataProviderAnonymous */ - public function test_jwt_authentication( $type, $token ) { + public function test_jwt_authentication_anonymous( string $type, string $token ) { $this->expectApplied( 'rest_api_guard_authentication_jwt' ); add_filter( 'rest_api_guard_authentication_jwt', fn () => true ); @@ -237,9 +237,12 @@ public function test_jwt_authentication( $type, $token ) { } else { $request->assertUnauthorized(); } + + // Ensure they are unauthenticated. + $this->get( '/wp-json/wp/v2/users/me' )->assertUnauthorized(); } - public static function jwtDataProvider(): array { + public static function jwtDataProviderAnonymous(): array { return [ 'valid' => [ 'valid', generate_jwt() ], 'invalid' => [ 'invalid', 'invalid' ], From d1fd76ae4fb9d8173758865e9c89f37c365751b1 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 22 Feb 2024 16:46:13 -0500 Subject: [PATCH 2/8] Wrapping up feature --- CHANGELOG.md | 4 + README.md | 22 +++++- composer.json | 6 +- plugin.php | 134 +++++++++++++++++++++------------- readme.txt | 28 ++++++- settings.php | 20 +++++ tests/test-rest-api-guard.php | 34 ++++++++- 7 files changed, 182 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a9a89..1c799a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `wp-rest-guard` will be documented in this file. +## v1.2.0 - 2024-02-22 + +- Add support for authenticated users interacting with the REST API. + ## v1.1.1 - 2024-01-15 - Re-releasing to re-trigger the deployment to WordPress.org. diff --git a/README.md b/README.md index ff46f03..f854d55 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ add_filter( 'rest_api_guard_allow_user_access', fn () => true ); ### Preventing Access to Index (`/`) or Namespace Endpoints (`wp/v2`) -To prevent anonymous users from browing your site and discovering what plugins/post types are setup, the plugin restricts access to the index (`/`) and namespace (`wp/v2`) endpoints. This can be prevented in the plugin's settings or via code: +To prevent anonymous users from browsing your site and discovering what plugins/post types are set up, the plugin restricts access to the index (`/`) and namespace (`wp/v2`) endpoints. This can be prevented in the plugin's settings or via code: ```php // Allow index access. @@ -146,12 +146,26 @@ The JWT's secret will be autogenerated and stored in the add_filter( 'rest_api_guard_jwt_secret', fn ( string $secret ) => 'my-custom-secret' ); ``` -You can generate a JWT for use with the REST API by calling the -`wp rest-api-guard generate-jwt` command. - ### Allow JWT Authentication for Authenticated Users Authenticated users can be authenticated with the REST API via a JSON Web Token. +This can be configured in the plugin's settings or via code: + +```php +add_filter( 'rest_api_guard_user_authentication_jwt', fn () => true ); +``` + +### Generating JWTs for Anonymous and Authenticated Users + +JWTs can be generated by calling the `wp rest-api-guard generate-jwt [--user=]` +command or with the `generate_jwt()` method: + +```php +$jwt = generate_jwt( + expiration: 3600, // Optional. The expiration time in seconds from now. + user: 1, // Optional. The user ID to generate the JWT for. Supports `WP_User` or user ID. +); +``` ## Testing diff --git a/composer.json b/composer.json index 2bc8bb6..630fdcb 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,7 @@ "require-dev": { "alleyinteractive/alley-coding-standards": "^2.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", - "mantle-framework/testkit": "^0.9", - "nunomaduro/collision": "^5.0" + "mantle-framework/testkit": "^0.12" }, "config": { "allow-plugins": { @@ -32,9 +31,6 @@ "dealerdirect/phpcodesniffer-composer-installer": true, "pestphp/pest-plugin": true }, - "platform": { - "php": "8.0" - }, "sort-packages": true }, "extra": { diff --git a/plugin.php b/plugin.php index c3bc314..617280e 100644 --- a/plugin.php +++ b/plugin.php @@ -3,7 +3,7 @@ * Plugin Name: REST API Guard * Plugin URI: https://github.com/alleyinteractive/wp-rest-api-guard * Description: Restrict and control access to the REST API - * Version: 1.1.2 + * Version: 1.2.0 * Author: Sean Fisher * Author URI: https://alley.co/ * Requires at least: 6.0 @@ -59,55 +59,77 @@ function should_prevent_anonymous_access( WP_REST_Server $server, WP_REST_Reques $settings = []; } - /** - * Check if the anonymous request requires a JSON Web Token (JWT). - * - * @param bool $require Whether to require a JWT, default false. - * @param \WP_REST_Request $request REST API Request. - */ - if ( class_exists( JWT::class ) && true === apply_filters( 'rest_api_guard_authentication_jwt', $settings['authentication_jwt'] ?? false, $request ) ) { - try { - $jwt = $request->get_header( 'Authorization' ); - - if ( empty( $jwt ) ) { - throw new InvalidArgumentException( __( 'No authorization header was found.', 'rest-api-guard' ) ); - } - - if ( 0 !== strpos( $jwt, 'Bearer ' ) ) { - throw new InvalidArgumentException( __( 'Invalid authorization header.', 'rest-api-guard' ) ); - } - - $decoded = JWT::decode( - substr( $jwt, 7 ), - new Key( get_jwt_secret(), 'HS256' ), - ); - - // Verify the contents of the JWT. - if ( empty( $decoded->iss ) || get_jwt_issuer() !== $decoded->iss ) { - throw new InvalidArgumentException( __( 'Invalid JWT issuer.', 'rest-api-guard' ) ); - } - - if ( empty( $decoded->aud ) || get_jwt_audience() !== $decoded->aud ) { - throw new InvalidArgumentException( __( 'Invalid JWT audience.', 'rest-api-guard' ) ); + if ( class_exists( JWT::class ) ) { + /** + * Check if the anonymous request requires a JSON Web Token (JWT). + * + * @param bool $require Whether to require a JWT, default false. + * @param \WP_REST_Request $request REST API Request. + */ + $require_anonymous_jwt = true === apply_filters( 'rest_api_guard_authentication_jwt', $settings['authentication_jwt'] ?? false, $request ); + $allow_user_jwt = true === apply_filters( 'rest_api_guard_user_authentication_jwt', $settings['user_authentication_jwt'] ?? false, $request ); + + if ( $require_anonymous_jwt || $allow_user_jwt ) { + try { + $jwt = $request->get_header( 'Authorization' ); + + if ( empty( $jwt ) && $require_anonymous_jwt ) { + throw new InvalidArgumentException( __( 'No authorization header token was found and is required for this request.', 'rest-api-guard' ) ); + } + + if ( ! empty( $jwt ) ) { + if ( 0 !== strpos( $jwt, 'Bearer ' ) ) { + throw new InvalidArgumentException( __( 'Invalid authorization header.', 'rest-api-guard' ) ); + } + + $decoded = JWT::decode( + substr( $jwt, 7 ), + new Key( get_jwt_secret(), 'HS256' ), + ); + + // Verify the contents of the JWT. + if ( empty( $decoded->iss ) || get_jwt_issuer() !== $decoded->iss ) { + throw new InvalidArgumentException( __( 'Invalid JWT issuer.', 'rest-api-guard' ) ); + } + + if ( empty( $decoded->aud ) || get_jwt_audience() !== $decoded->aud ) { + throw new InvalidArgumentException( __( 'Invalid JWT audience.', 'rest-api-guard' ) ); + } + + if ( $allow_user_jwt && ! empty( $decoded->sub ) ) { + $user = get_user_by( 'id', $decoded->sub ); + + if ( ! $user instanceof WP_User ) { + throw new InvalidArgumentException( __( 'Invalid user in JWT sub.', 'rest-api-guard' ) ); + } + + wp_set_current_user( $user->ID ); + + return false; + } + } + } catch ( \Exception $error ) { + return new WP_Error( + 'rest_api_guard_unauthorized', + /** + * Filter the authorization error message. + * + * @param string $message The error message being returned. + * @param string $error_message The error message from the exception. + * @param \Throwable $error The error that occurred. + */ + apply_filters( + 'rest_api_guard_invalid_jwt_message', + /* translators: %s: The error message. */ + __( 'Error authentication with token: %s', 'rest-api-guard' ), + $error->getMessage(), + $error, + ), + [ + 'status' => rest_authorization_required_code(), + ] + ); } - } catch ( \Exception $error ) { - return new WP_Error( - 'rest_api_guard_unauthorized', - /** - * Filter the authorization error message. - * - * @param string $message The error message. - * @param \Throwable $error The error that occurred. - */ - apply_filters( - 'rest_api_guard_invalid_jwt_message', - __( 'Invalid authorization header.', 'rest-api-guard' ), - $error, - ), - [ - 'status' => rest_authorization_required_code(), - ] - ); } } @@ -300,11 +322,13 @@ function get_jwt_secret(): string { * * The JWT payload is intentionally not filtered to prevent * - * @param int|null $expiration The expiration time of the JWT in seconds or null for no expiration. - * @param WP_User|null $user The user to include in the JWT or null for no user. + * @param int|null $expiration The expiration time of the JWT in seconds or null for no expiration. + * @param WP_User|int|null $user The user to include in the JWT or null for no user. * @return string + * + * @throws InvalidArgumentException If the user is invalid or unknown. */ -function generate_jwt( ?int $expiration = null, ?WP_User $user = null ): string { +function generate_jwt( ?int $expiration = null, WP_User|int|null $user = null ): string { $payload = [ 'iss' => get_jwt_issuer(), 'aud' => get_jwt_audience(), @@ -316,6 +340,12 @@ function generate_jwt( ?int $expiration = null, ?WP_User $user = null ): string } if ( null !== $user ) { + $user = $user instanceof WP_User ? $user : get_user_by( 'id', $user ); + + if ( ! $user instanceof WP_User ) { + throw new InvalidArgumentException( esc_html__( 'Invalid user.', 'rest-api-guard' ) ); + } + $payload['sub'] = $user->ID; $payload['user_login'] = $user->user_login; } diff --git a/readme.txt b/readme.txt index 0ec4239..deec546 100644 --- a/readme.txt +++ b/readme.txt @@ -71,7 +71,11 @@ Anonymous users can be granted access only to specific namespaces/routes. Reques ### Restrict Anonymous Access to Specific Namespaces/Routes (Denylist) -Anonymous users can be restricted from specific namespaces/routes. This acts as a denylist for specific paths that an anonymous user cannot access. The paths support regular expressions for matching. The use of the [Allowlist](#limit-anonymous-access-to-specific-namespacesroutes-allowlist) takes priority over this denylist. This can be configured in the plugin's settings or via code: +Anonymous users can be restricted from specific namespaces/routes. This acts as +a denylist for specific paths that an anonymous user cannot access. The paths +support regular expressions for matching. The use of the allowlist takes +priority over this denylist. This can be configured in the plugin's settings or +via code: add_filter( 'rest_api_guard_anonymous_requests_denylist', @@ -94,7 +98,9 @@ code: add_filter( 'rest_api_guard_authentication_jwt', fn () => true ); Out of the box, the plugin will look for a JWT in the `Authorization: Bearer -` header. The JWT will be expected to have an audience of 'wordpress-rest-api' and issuer of the site's URL. This can be configured in the plugin's settings or via code: +` header. The JWT will be expected to have an audience of +'wordpress-rest-api' and issuer of the site's URL. This can be configured in the +plugin's settings or via code: add_filter( 'rest_api_guard_jwt_audience', @@ -120,5 +126,19 @@ The JWT's secret will be autogenerated and stored in the database in the } ); -You can generate a JWT for use with the REST API by calling the -`wp rest-api-guard generate-jwt` command. +### Allow JWT Authentication for Authenticated Users + +Authenticated users can be authenticated with the REST API via a JSON Web Token. +This can be configured in the plugin's settings or via code: + + add_filter( 'rest_api_guard_user_authentication_jwt', fn () => true ); + +### Generating JWTs for Anonymous and Authenticated Users + +JWTs can be generated by calling the `wp rest-api-guard generate-jwt [--user=]` +command or with the `generate_jwt()` method: + + $jwt = generate_jwt( + expiration: 3600, // Optional. The expiration time in seconds from now. + user: 1, // Optional. The user ID to generate the JWT for. Supports `WP_User` or user ID. + ); diff --git a/settings.php b/settings.php index e5a394e..dbd4708 100644 --- a/settings.php +++ b/settings.php @@ -184,6 +184,26 @@ function on_admin_init() { 'type' => 'checkbox', ], ); + + add_settings_field( + 'user_authentication_jwt', + __( 'Allow User Authentication with JSON Web Token', 'rest-api-guard' ), + __NAMESPACE__ . '\render_field', + SETTINGS_KEY, + SETTINGS_KEY, + [ + 'description' => __( 'Allow user authentication with a JSON Web Token (JWT) for all requests.', 'rest-api-guard' ), + 'additional' => sprintf( + /* translators: 1: The JWT audience. 2: The JWT issuer. */ + __( 'When enabled, the plugin will allow JWTs to be generated against authenticated users. They can be passed as a "Authorization: Bearer " with the token being a valid JSON Web Token (JWT). The plugin will be expecting a JWT with an audience of "%1$s", issuer of "%2$s", and secret that matches the value of the "rest_api_guard_jwt_secret" option.', 'rest-api-guard' ), + get_jwt_audience(), + get_jwt_issuer(), + ), + 'filter' => 'rest_api_guard_user_authentication_jwt', + 'id' => 'user_authentication_jwt', + 'type' => 'checkbox', + ], + ); } } diff --git a/tests/test-rest-api-guard.php b/tests/test-rest-api-guard.php index 89968b0..343a60d 100644 --- a/tests/test-rest-api-guard.php +++ b/tests/test-rest-api-guard.php @@ -238,7 +238,7 @@ public function test_jwt_authentication_anonymous( string $type, string $token ) $request->assertUnauthorized(); } - // Ensure they are unauthenticated. + // Ensure they are always unauthenticated. $this->get( '/wp-json/wp/v2/users/me' )->assertUnauthorized(); } @@ -249,4 +249,36 @@ public static function jwtDataProviderAnonymous(): array { 'empty' => [ 'invalid', '' ], ]; } + + /** + * @dataProvider jwtDataProviderAuthenticated + */ + public function test_jwt_authentication_authenticated( string $type, string $token ) { + add_filter( 'rest_api_guard_authentication_jwt', fn () => true ); + add_filter( 'rest_api_guard_user_authentication_jwt', fn () => true ); + + $request = $this + ->with_header( 'Authorization', "Bearer $token" ) + ->get( '/wp-json/wp/v2/users/me' ); + + if ( 'valid' === $type ) { + $request->assertOk()->assertJsonPathExists( 'id' ); + + // Ensure they can access the REST API normally. + $this->get( '/wp-json/wp/v2/posts' )->assertOk(); + } else { + $request->assertUnauthorized(); + + // Ensure they cannot access the REST API normally. + $this->get( '/wp-json/wp/v2/posts' )->assertUnauthorized(); + } + } + + public static function jwtDataProviderAuthenticated(): array { + return [ + 'valid' => [ 'valid', generate_jwt( user: static::factory()->user->create_and_get() ) ], + 'invalid' => [ 'invalid', substr( generate_jwt(), 0, 20 ) ], + 'empty' => [ 'invalid', '' ], + ]; + } } From 58ea7c433b3693d1e83de927a9dab1ab3308c991 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 22 Feb 2024 16:49:41 -0500 Subject: [PATCH 3/8] Allow settings to be disabled, bump secret --- plugin.php | 2 +- settings.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index 617280e..971ab8f 100644 --- a/plugin.php +++ b/plugin.php @@ -306,7 +306,7 @@ function get_jwt_audience(): string { function get_jwt_secret(): string { // Generate the JWT secret if it does not exist. if ( empty( get_option( 'rest_api_guard_jwt_secret' ) ) ) { - update_option( 'rest_api_guard_jwt_secret', wp_generate_password( 12, false ) ); + update_option( 'rest_api_guard_jwt_secret', wp_generate_password( 24, false ) ); } /** diff --git a/settings.php b/settings.php index dbd4708..f372781 100644 --- a/settings.php +++ b/settings.php @@ -27,6 +27,15 @@ * Register the Admin Settings page. */ function on_admin_menu() { + /** + * Filter to disable the admin settings page. + * + * @param bool $disable Whether to disable the admin settings page. + */ + if ( true === apply_filters( 'rest_api_guard_disable_admin_settings', false ) ) { + return; + } + add_options_page( __( 'REST API Guard', 'rest-api-guard' ), __( 'REST API Guard', 'rest-api-guard' ), From b14c42206304a318862e5b7e999f2c9eb38a532e Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 22 Feb 2024 16:50:09 -0500 Subject: [PATCH 4/8] CHANGELOG --- CHANGELOG.md | 2 ++ plugin.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c799a5..e7c56db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to `wp-rest-guard` will be documented in this file. ## v1.2.0 - 2024-02-22 - Add support for authenticated users interacting with the REST API. +- Allow settings to be completely disabled via code. +- Increase the default length of the JWT secret to 32 characters. ## v1.1.1 - 2024-01-15 diff --git a/plugin.php b/plugin.php index 971ab8f..e3e4448 100644 --- a/plugin.php +++ b/plugin.php @@ -306,7 +306,7 @@ function get_jwt_audience(): string { function get_jwt_secret(): string { // Generate the JWT secret if it does not exist. if ( empty( get_option( 'rest_api_guard_jwt_secret' ) ) ) { - update_option( 'rest_api_guard_jwt_secret', wp_generate_password( 24, false ) ); + update_option( 'rest_api_guard_jwt_secret', wp_generate_password( 32, false ) ); } /** From 9c2b6eec44d26bd74f82a0c50cea6fcc0e710139 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 22 Feb 2024 17:02:37 -0500 Subject: [PATCH 5/8] Fixing feature --- plugin.php | 9 +++++---- settings.php | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index e3e4448..7434adc 100644 --- a/plugin.php +++ b/plugin.php @@ -115,14 +115,15 @@ function should_prevent_anonymous_access( WP_REST_Server $server, WP_REST_Reques * Filter the authorization error message. * * @param string $message The error message being returned. - * @param string $error_message The error message from the exception. * @param \Throwable $error The error that occurred. */ apply_filters( 'rest_api_guard_invalid_jwt_message', - /* translators: %s: The error message. */ - __( 'Error authentication with token: %s', 'rest-api-guard' ), - $error->getMessage(), + sprintf( + /* translators: %s: The error message. */ + __( 'Error authentication with token: %s', 'rest-api-guard' ), + $error->getMessage(), + ), $error, ), [ diff --git a/settings.php b/settings.php index f372781..3937c9b 100644 --- a/settings.php +++ b/settings.php @@ -234,6 +234,8 @@ function sanitize_settings( $input ) { 'allow_user_access' => ! empty( $input['allow_user_access'] ), 'anonymous_requests_allowlist' => ! empty( $input['anonymous_requests_allowlist'] ) ? sanitize_textarea_field( $input['anonymous_requests_allowlist'] ) : '', 'anonymous_requests_denylist' => ! empty( $input['anonymous_requests_denylist'] ) ? sanitize_textarea_field( $input['anonymous_requests_denylist'] ) : '', + 'authentication_jwt' => ! empty( $input['authentication_jwt'] ), + 'user_authentication_jwt' => ! empty( $input['user_authentication_jwt'] ), ]; } From e811b6ee75350384785024fbfe45291b20f4bd12 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 23 Feb 2024 09:30:37 -0500 Subject: [PATCH 6/8] Stable release 1.2.0 and updating docs --- README.md | 14 ++++++++------ readme.txt | 17 ++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f854d55..a4e90c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # REST API Guard -Stable tag: 1.1.2 +Stable tag: 1.2.0 Requires at least: 6.0 @@ -121,8 +121,8 @@ add_filter( ### Require JSON Web Token (JWT) Authentication for Anonymous Users Anonymous users can be required to authenticate via a JSON Web Token (JWT) to -access the REST API. This can be configured in the plugin's settings or via -code: +access the REST API. Users should pass an `Authorization: Bearer ` header +with their request. This can be configured in the plugin's settings or via code: ```php add_filter( 'rest_api_guard_authentication_jwt', fn () => true ); @@ -149,7 +149,9 @@ add_filter( 'rest_api_guard_jwt_secret', fn ( string $secret ) => 'my-custom-sec ### Allow JWT Authentication for Authenticated Users Authenticated users can be authenticated with the REST API via a JSON Web Token. -This can be configured in the plugin's settings or via code: +Similar to the anonymous JWT authentication, users should pass an +`Authorization: Bearer ` header with their request. This can be +configured in the plugin's settings or via code: ```php add_filter( 'rest_api_guard_user_authentication_jwt', fn () => true ); @@ -158,10 +160,10 @@ add_filter( 'rest_api_guard_user_authentication_jwt', fn () => true ); ### Generating JWTs for Anonymous and Authenticated Users JWTs can be generated by calling the `wp rest-api-guard generate-jwt [--user=]` -command or with the `generate_jwt()` method: +command or using the `Alley\WP\REST_API_Guard\generate_jwt()` method: ```php -$jwt = generate_jwt( +$jwt = \Alley\WP\REST_API_Guard\generate_jwt( expiration: 3600, // Optional. The expiration time in seconds from now. user: 1, // Optional. The user ID to generate the JWT for. Supports `WP_User` or user ID. ); diff --git a/readme.txt b/readme.txt index deec546..e9e0902 100644 --- a/readme.txt +++ b/readme.txt @@ -1,5 +1,5 @@ === REST API Guard === -Stable tag: 1.1.2 +Stable tag: 1.2.0 Requires at least: 6.0 Tested up to: 6.3 Requires PHP: 8.0 @@ -92,8 +92,8 @@ via code: ### Require JSON Web Token (JWT) Authentication Anonymous users can be required to authenticate via a JSON Web Token (JWT) to -access the REST API. This can be configured in the plugin's settings or via -code: +access the REST API. Users should pass an `Authorization: Bearer ` header +with their request. This can be configured in the plugin's settings or via code: add_filter( 'rest_api_guard_authentication_jwt', fn () => true ); @@ -129,16 +129,19 @@ The JWT's secret will be autogenerated and stored in the database in the ### Allow JWT Authentication for Authenticated Users Authenticated users can be authenticated with the REST API via a JSON Web Token. -This can be configured in the plugin's settings or via code: +Similar to the anonymous JWT authentication, users should pass an +`Authorization: Bearer ` header with their request. This can be +configured in the plugin's settings or via code: add_filter( 'rest_api_guard_user_authentication_jwt', fn () => true ); ### Generating JWTs for Anonymous and Authenticated Users -JWTs can be generated by calling the `wp rest-api-guard generate-jwt [--user=]` -command or with the `generate_jwt()` method: +JWTs can be generated by calling the +`wp rest-api-guard generate-jwt [--user=]` command or using the +`Alley\WP\REST_API_Guard\generate_jwt()` method: - $jwt = generate_jwt( + $jwt = \Alley\WP\REST_API_Guard\generate_jwt( expiration: 3600, // Optional. The expiration time in seconds from now. user: 1, // Optional. The user ID to generate the JWT for. Supports `WP_User` or user ID. ); From 661493a93d5848a14e506282b55e5fa589709cc2 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 23 Feb 2024 09:33:41 -0500 Subject: [PATCH 7/8] Update settings addon --- settings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.php b/settings.php index 3937c9b..0279dca 100644 --- a/settings.php +++ b/settings.php @@ -184,7 +184,7 @@ function on_admin_init() { 'description' => __( 'Require authentication with a JSON Web Token (JWT) for all anonymous requests.', 'rest-api-guard' ), 'additional' => sprintf( /* translators: 1: The JWT audience. 2: The JWT issuer. */ - __( 'When enabled, the plugin will require anonymous users to pass an "Authorization: Bearer " with the token being a valid JSON Web Token (JWT). The plugin will be expecting a JWT with an audience of "%1$s", issuer of "%2$s", and secret that matches the value of the "rest_api_guard_jwt_secret" option.', 'rest-api-guard' ), + __( 'When enabled, the plugin will require anonymous users to pass an "Authorization: Bearer " with the token being a valid JSON Web Token (JWT). The plugin will be expecting a JWT with an audience of "%1$s", issuer of "%2$s", and secret that matches the value of the "rest_api_guard_jwt_secret" option. When using the token, the user will have unrestricted read-only access to the REST API.', 'rest-api-guard' ), get_jwt_audience(), get_jwt_issuer(), ), @@ -204,7 +204,7 @@ function on_admin_init() { 'description' => __( 'Allow user authentication with a JSON Web Token (JWT) for all requests.', 'rest-api-guard' ), 'additional' => sprintf( /* translators: 1: The JWT audience. 2: The JWT issuer. */ - __( 'When enabled, the plugin will allow JWTs to be generated against authenticated users. They can be passed as a "Authorization: Bearer " with the token being a valid JSON Web Token (JWT). The plugin will be expecting a JWT with an audience of "%1$s", issuer of "%2$s", and secret that matches the value of the "rest_api_guard_jwt_secret" option.', 'rest-api-guard' ), + __( 'When enabled, the plugin will allow JWTs to be generated against authenticated users. They can be passed as a "Authorization: Bearer " with the token being a valid JSON Web Token (JWT). The plugin will be expecting a JWT with an audience of "%1$s", issuer of "%2$s", and secret that matches the value of the "rest_api_guard_jwt_secret" option. When using the token, the user will have unrestricted access to the REST API mirroring whatever permissions the user associated with the token would have.', 'rest-api-guard' ), get_jwt_audience(), get_jwt_issuer(), ), From 2f1b0d31a81bf7d7671199408a6b3577bd9159f9 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 23 Feb 2024 09:36:55 -0500 Subject: [PATCH 8/8] Migrating to testkit 1.0 and PSR-4 testing --- composer.json | 2 +- phpunit.xml | 24 +++++++++---------- ...est-api-guard.php => RestApiGuardTest.php} | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) rename tests/{test-rest-api-guard.php => RestApiGuardTest.php} (99%) diff --git a/composer.json b/composer.json index 630fdcb..71cdd63 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require-dev": { "alleyinteractive/alley-coding-standards": "^2.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", - "mantle-framework/testkit": "^0.12" + "mantle-framework/testkit": "^1.0" }, "config": { "allow-plugins": { diff --git a/phpunit.xml b/phpunit.xml index 0595632..80ad28a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,15 @@ + - - - tests - - + + + tests + + diff --git a/tests/test-rest-api-guard.php b/tests/RestApiGuardTest.php similarity index 99% rename from tests/test-rest-api-guard.php rename to tests/RestApiGuardTest.php index 343a60d..2b523f0 100644 --- a/tests/test-rest-api-guard.php +++ b/tests/RestApiGuardTest.php @@ -10,7 +10,7 @@ /** * Visit {@see https://mantle.alley.co/testing/test-framework.html} to learn more. */ -class Test_REST_API_Guard extends Test_Case { +class RestApiGuardTest extends Test_Case { protected function setUp(): void { parent::setUp();