Skip to content

Commit

Permalink
Merge pull request #17 from alleyinteractive/feature/jwt-token
Browse files Browse the repository at this point in the history
Allow authentication with a JWT
  • Loading branch information
srtfisher authored Jan 15, 2024
2 parents b36a99b + 1c0981e commit a829684
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 10 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/built-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Built Release

on:
push:
branches:
- develop

jobs:
built-release:
uses: alleyinteractive/.github/.github/workflows/built-release.yml@main
if: ${{ github.repository != 'alleyinteractive/create-wordpress-plugin' }}
secrets:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
with:
node: 16
php: '8.2'
composer_install: true
draft: false
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to `wp-rest-guard` will be documented in this file.

## v1.1.0 - 2024-012-12

- Drops support for PHP 7.4 and requires PHP 8.0.
- Add feature to allow anonymous authentication with a JSON Web Token (JWT).

## v1.0.4 - 2024-01-12

- Fixing an issue splitting lines by `\n` instead of `\r\n` on Windows.
Expand Down
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# REST API Guard

Stable tag: 1.0.4
Stable tag: 1.1.0

Requires at least: 6.0

Tested up to: 6.0

Requires PHP: 7.4
Requires PHP: 8.0

License: GPL v2 or later

Expand Down Expand Up @@ -118,6 +118,52 @@ add_filter(
);
```

### 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:

```php
add_filter( 'rest_api_guard_authentication_jwt', fn () => true );
```

Out of the box, the plugin will look for a JWT in the `Authorization: Bearer
<token>` 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:

```php
add_filter(
'rest_api_guard_jwt_audience',
function ( string $audience ): string {
return 'custom-audience';
}
);

add_filter(
'rest_api_guard_jwt_issuer',
function ( string $issuer ): string {
return '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';
}
);
```

You can generate a JWT for use with the REST API by calling the
`wp rest-api-guard generate-jwt` command.

## Testing

Run `composer test` to run tests against PHPUnit and the PHP code in the plugin.
Expand Down
18 changes: 18 additions & 0 deletions cli.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/**
* WP-CLI commands.
*
* @package rest-api-guard
*/

use function Alley\WP\REST_API_Guard\generate_jwt;

WP_CLI::add_command(
'rest-api-guard generate-jwt',
function () {
echo generate_jwt() . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
},
[
'shortdesc' => __( 'Generate a JSON Web Token (JWT).', 'rest-api-guard' ),
]
);
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
}
],
"require": {
"php": "^7.4|^8.0"
"php": "^8.0",
"firebase/php-jwt": "^6.10"
},
"require-dev": {
"alleyinteractive/alley-coding-standards": "^2.0",
"alleyinteractive/composer-wordpress-autoloader": "^1.0",
"mantle-framework/testkit": "^0.12",
"mantle-framework/testkit": "^0.7",
"nunomaduro/collision": "^5.0"
},
"config": {
Expand All @@ -31,6 +32,9 @@
"dealerdirect/phpcodesniffer-composer-installer": true,
"pestphp/pest-plugin": true
},
"platform": {
"php": "8.0"
},
"sort-packages": true
},
"extra": {
Expand Down
141 changes: 137 additions & 4 deletions plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.4
* Version: 1.1.0
* Author: Sean Fisher
* Author URI: https://alley.co/
* Requires at least: 6.0
Expand All @@ -17,6 +17,9 @@

namespace Alley\WP\REST_API_Guard;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use InvalidArgumentException;
use WP_Error;
use WP_REST_Request;
use WP_REST_Server;
Expand All @@ -29,6 +32,10 @@
* Instantiate the plugin.
*/
function main() {
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
require_once __DIR__ . '/vendor/autoload.php';
}

require_once __DIR__ . '/settings.php';

add_filter( 'rest_pre_dispatch', __NAMESPACE__ . '\on_rest_pre_dispatch', 10, 3 );
Expand All @@ -40,15 +47,69 @@ function main() {
*
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request The request object.
* @return bool
* @return WP_Error|bool
*
* @throws InvalidArgumentException If the JWT is invalid.
*/
function should_prevent_anonymous_access( WP_REST_Server $server, WP_REST_Request $request ): bool {
function should_prevent_anonymous_access( WP_REST_Server $server, WP_REST_Request $request ): WP_Error|bool {
$settings = (array) get_option( SETTINGS_KEY );

if ( ! is_array( $settings ) ) {
$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' ) );
}
} 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(),
]
);
}
}

/**
* Check if anonymous access is prevent by default.
*
Expand Down Expand Up @@ -161,7 +222,11 @@ function on_rest_pre_dispatch( $pre, $server, $request ) {
return $pre;
}

if ( should_prevent_anonymous_access( $server, $request ) ) {
$should_prevent = should_prevent_anonymous_access( $server, $request );

if ( is_wp_error( $should_prevent ) ) {
return $should_prevent;
} elseif ( $should_prevent ) {
return new WP_Error(
'rest_api_guard_unauthorized',
/**
Expand All @@ -181,3 +246,71 @@ function on_rest_pre_dispatch( $pre, $server, $request ) {

return $pre;
}

/**
* Get the JSON Web Token (JWT) issuer.
*
* @return string
*/
function get_jwt_issuer(): string {
/**
* Filter the issuer of the JWT.
*
* @param string $issuer The issuer of the JWT.
*/
return apply_filters( 'rest_api_guard_jwt_issuer', get_bloginfo( 'url' ) );
}

/**
* Get the JSON Web Token (JWT) audience.
*
* @return string
*/
function get_jwt_audience(): string {
/**
* Filter the audience of the JWT.
*
* @param string $audience The audience of the JWT.
*/
return apply_filters( 'rest_api_guard_jwt_audience', 'wordpress-rest-api' );
}

/**
* Get the JSON Web Token (JWT) secret.
*
* @return 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 ) );
}

/**
* Filter the secret of the JWT. By default, the WordPress secret key is used.
*
* @param string $secret The secret of the JWT.
*/
return apply_filters( 'rest_api_guard_jwt_secret', get_option( 'rest_api_guard_jwt_secret' ) );
}

/**
* Generate a JSON Web Token (JWT).
*
* @return string
*/
function generate_jwt(): string {
return JWT::encode(
[
'iss' => get_jwt_issuer(),
'aud' => get_jwt_audience(),
'iat' => time(),
],
get_jwt_secret(),
'HS256'
);
}

if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once __DIR__ . '/cli.php';
}
48 changes: 46 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
=== REST API Guard ===
Stable tag: 1.0.4
Stable tag: 1.1.0
Requires at least: 6.0
Tested up to: 6.3
Requires PHP: 7.4
Requires PHP: 8.0
License: GPL v2 or later
Tags: alleyinteractive, rest-api-guard
Contributors: sean212
Expand Down Expand Up @@ -84,3 +84,47 @@ Anonymous users can be restricted from specific namespaces/routes. This acts as
10,
2
);

### 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:

```php
add_filter( 'rest_api_guard_authentication_jwt', fn () => true );
```

Out of the box, the plugin will look for a JWT in the `Authorization: Bearer
<token>` 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:

```php
add_filter(
'rest_api_guard_jwt_audience',
function ( string $audience ): string {
return 'custom-audience';
}
);

add_filter(
'rest_api_guard_jwt_issuer',
function ( string $issuer ): string {
return 'https://example.com';
}
);
```

The JWT's secret will be autogenerated and stored in the database in the
`rest_api_guard_jwt_secret` option. The secret can also be changed via code:

```php
add_filter(
'rest_api_guard_jwt_secret',
function ( string $secret ): string {
return 'my-custom-secret';
}
);
```

You can generate a JWT for use with the REST API by calling the
`wp rest-api-guard generate-jwt` command.
Loading

0 comments on commit a829684

Please sign in to comment.