baxtian / wp_jwt
JWT tokens with expiration/revocation for external clients of Merak-based plugins.
Requires
- php: >=8.1
- baxtian/php-singleton: ^0.6.6
- firebase/php-jwt: ^7.0
Requires (Dev)
- brain/monkey: ^2.6
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Shared JWT library for Merak-based plugins/themes: short-lived, expirable/revocable tokens for external clients — not a replacement for Application Passwords, which stays the simple option for internal use (admin-to-admin, own tools, no expiration to manage).
Registers its REST routes under jwt-auth/v1 (kept from the jwt-auth plugin this library replaces, for drop-in compatibility with existing clients).
Mantainers
Juan Sebastián Echeverry baxtian.echeverry@gmail.com
Usage Guide
Boot
\Baxtian\WP_Jwt\Auth::get_instance();
Required constants
| Constant | Required | Purpose |
|---|---|---|
JWT_AUTH_SECRET_KEY | Yes | Signs and verifies tokens. Without it, token responds with wp_jwt_bad_config. |
JWT_AUTH_CORS_ENABLE | No | Set true to add Access-Control-Allow-Headers on the normal REST cycle. |
API_HOST | No | String or array of allowed origin hosts. When defined, a token request from any other Origin is rejected. |
If JWT_AUTH_SECRET_KEY isn't defined, an admin_notices warning is shown automatically once the library boots — no need to build one per project.
Routes
| Route | Method | Purpose |
|---|---|---|
jwt-auth/v1/token | POST | Issue a token from username/password, or from the refresh_token cookie when present. |
jwt-auth/v1/token/validate | POST | Validate the Authorization: Bearer token. |
jwt-auth/v1/token/refresh | POST | Rotate the refresh_token cookie and issue a new access token. |
jwt-auth/v1/login | GET | Redeem a one-time login_key (see below) and redirect with a fresh refresh cookie. |
jwt-auth/v1/logout | POST | Clear the refresh_token cookie. |
jwt-auth/v1/forgot-password | POST | Send a password reset email, redirect_url pointing at the consuming app's own reset screen. |
jwt-auth/v1/reset-password | POST | Set a new password from a reset key. |
Extending the credential response
A successful token request fires jwt_auth_valid_credential_response with the built response array and the WP_User, same as the jwt-auth plugin did — this library only adds roles and a one-time login_key; anything project-specific (license state, custom flags) is added by hooking this filter in the consuming project, not in this library.
$response is not always an array. When API_HOST is defined and the request's Origin isn't on the allow-list, this same filter fires with a WP_REST_Response instead (the rejection response itself) — guard with is_array($response) before indexing into it, or the WP_REST_Response case will fatal:
add_filter('jwt_auth_valid_credential_response', function ($response, $user) {
if (is_array($response) && isset($response['data'])) {
$response['data']['my_field'] = my_project_value($user);
}
return $response;
}, 10, 2);
Outside the REST/hooks cycle (ex. a SHORTINIT script)
validate_token_string(), send_cors_headers(), get_iss(), get_alg() and decode_token() are static — they don't need get_instance(). This matters under SHORTINIT: get_instance() boots the full singleton, which schedules a weekly cron event in its constructor, and cron.php isn't loaded yet at that point (WordPress bails out of SHORTINIT before reaching it in wp-settings.php) — booting the singleton there fatals. get_iss() uses get_option('home') rather than get_bloginfo('url')/home_url() for the same reason (neither is loaded under SHORTINIT either).
validate_token_string() checks the signature and iss, but — unlike the token/validate route — does NOT check that the user still exists: get_user_by()/WP_User need user.php/class-wp-user.php, also not loaded under SHORTINIT. A caller that needs that guarantee checks it itself once its own bootstrap has loaded far enough.
try {
$payload = \Baxtian\WP_Jwt\Auth::validate_token_string($bearer_token);
} catch (\Exception $e) {
// invalid/expired token, or iss mismatch
}
\Baxtian\WP_Jwt\Auth::send_cors_headers(['Authorization', 'Content-Type', 'X-Blog-Path'], ['GET', 'OPTIONS']);