From 83cebf57ec946092e92560798ac5c2511d5a4b02 Mon Sep 17 00:00:00 2001
From: Abhishek Kaushik
Date: Sun, 12 Apr 2026 13:21:11 +0530
Subject: [PATCH 1/3] Add access token expiry and TTL support
---
inc/authentication/namespace.php | 13 +++++++++++
inc/endpoints/class-token.php | 3 +++
inc/tokens/class-access-token.php | 38 +++++++++++++++++++++++++++----
3 files changed, 49 insertions(+), 5 deletions(-)
diff --git a/inc/authentication/namespace.php b/inc/authentication/namespace.php
index 63c0617..169403e 100644
--- a/inc/authentication/namespace.php
+++ b/inc/authentication/namespace.php
@@ -169,6 +169,19 @@ function attempt_authentication( $user = null ) {
return $user;
}
+ // Reject expired tokens before any further lookups.
+ if ( $token->is_expired() ) {
+ $is_querying_token = false;
+ $oauth2_error = new WP_Error(
+ 'oauth2.authentication.token_expired',
+ __( 'Access token has expired.', 'oauth2' ),
+ [
+ 'status' => \WP_Http::UNAUTHORIZED,
+ ]
+ );
+ return $user;
+ }
+
$client = $token->get_client();
$is_querying_token = false;
diff --git a/inc/endpoints/class-token.php b/inc/endpoints/class-token.php
index 2bdfc09..6a1520d 100644
--- a/inc/endpoints/class-token.php
+++ b/inc/endpoints/class-token.php
@@ -178,9 +178,12 @@ private function handle_client_credentials( WP_REST_Request $request ) {
return $token;
}
+ $ttl = apply_filters( 'oauth2.client_token_ttl', OAuth2\Tokens\Access_Token::DEFAULT_CLIENT_TOKEN_TTL );
+
return [
'access_token' => $token->get_key(),
'token_type' => 'bearer',
+ 'expires_in' => $ttl,
];
}
diff --git a/inc/tokens/class-access-token.php b/inc/tokens/class-access-token.php
index c055ebb..5704c4b 100644
--- a/inc/tokens/class-access-token.php
+++ b/inc/tokens/class-access-token.php
@@ -15,9 +15,10 @@
use WP_User_Query;
class Access_Token extends Token {
- const META_PREFIX = '_oauth2_access_';
- const CLIENT_META_PREFIX = '_oauth2_client_token_';
- const KEY_LENGTH = 12;
+ const META_PREFIX = '_oauth2_access_';
+ const CLIENT_META_PREFIX = '_oauth2_client_token_';
+ const KEY_LENGTH = 12;
+ const DEFAULT_CLIENT_TOKEN_TTL = 3600; // 1 hour in seconds
/**
* @return string Meta prefix. Client tokens use a distinct prefix because
@@ -275,9 +276,11 @@ public static function create_for_client( ClientInterface $client, $meta = [] )
);
}
- $data = [
+ $ttl = apply_filters( 'oauth2.client_token_ttl', static::DEFAULT_CLIENT_TOKEN_TTL );
+ $data = [
'client' => $client->get_id(),
'created' => time(),
+ 'expires' => time() + $ttl,
'meta' => $meta,
];
$key = wp_generate_password( static::KEY_LENGTH, false );
@@ -305,12 +308,37 @@ public function is_client_token() {
return $this->user === null;
}
+ /**
+ * Check if the token has expired.
+ *
+ * Tokens without an `expires` timestamp never expire (backwards compat
+ * for user tokens issued before expiry support was added).
+ *
+ * @return bool True if the token has expired, false otherwise.
+ */
+ public function is_expired() {
+ if ( ! isset( $this->value['expires'] ) ) {
+ return false;
+ }
+
+ return time() >= $this->value['expires'];
+ }
+
+ /**
+ * Get expiration timestamp.
+ *
+ * @return int|null Expiration timestamp, or null if no expiration.
+ */
+ public function get_expiration_time() {
+ return $this->value['expires'] ?? null;
+ }
+
/**
* Check if the token is valid.
*
* @return bool True if the token is valid, false otherwise.
*/
public function is_valid() {
- return true;
+ return ! $this->is_expired();
}
}
From e3769aca5aba31a1d3b66f3bff4efd275e3c6ab3 Mon Sep 17 00:00:00 2001
From: Abhishek Kaushik
Date: Mon, 13 Apr 2026 21:23:33 +0530
Subject: [PATCH 2/3] Add support for configurable client token TTL
---
inc/admin/namespace.php | 23 +++++++++++++++++++++++
inc/class-client.php | 26 ++++++++++++++++++++++++++
inc/endpoints/class-token.php | 12 ++++++++----
inc/tokens/class-access-token.php | 8 +++++---
4 files changed, 62 insertions(+), 7 deletions(-)
diff --git a/inc/admin/namespace.php b/inc/admin/namespace.php
index d1375b6..be0f7b4 100644
--- a/inc/admin/namespace.php
+++ b/inc/admin/namespace.php
@@ -175,6 +175,16 @@ function validate_parameters( $params ) {
$valid['client_credentials_enabled'] = ! empty( $params['client_credentials_enabled'] );
+ if ( isset( $params['token_ttl'] ) && $params['token_ttl'] !== '' ) {
+ $ttl = (int) $params['token_ttl'];
+ if ( $ttl < 0 ) {
+ return new WP_Error( 'rest_oauth2_invalid_ttl', esc_html__( 'Token TTL must be a positive number or empty for no expiry.', 'oauth2' ) );
+ }
+ $valid['token_ttl'] = $ttl;
+ } else {
+ $valid['token_ttl'] = '';
+ }
+
// Callback is required unless this client only uses client_credentials.
if ( empty( $params['callback'] ) && ! $valid['client_credentials_enabled'] ) {
return new WP_Error( 'rest_oauth2_missing_callback', esc_html__( 'Client callback is required and must be a valid URL.', 'oauth2' ) );
@@ -219,6 +229,7 @@ function handle_edit_submit( Client $consumer = null ) {
'type' => $params['type'],
'callback' => $params['callback'],
'client_credentials_enabled' => $params['client_credentials_enabled'],
+ 'token_ttl' => $params['token_ttl'],
],
];
@@ -233,6 +244,7 @@ function handle_edit_submit( Client $consumer = null ) {
'type' => $params['type'],
'callback' => $params['callback'],
'client_credentials_enabled' => $params['client_credentials_enabled'],
+ 'token_ttl' => $params['token_ttl'],
],
];
@@ -326,12 +338,14 @@ function render_edit_page() {
$data[ $key ] = empty( $form_data[ $key ] ) ? '' : $form_data[ $key ];
}
$data['client_credentials_enabled'] = ! empty( $form_data['client_credentials_enabled'] );
+ $data['token_ttl'] = isset( $form_data['token_ttl'] ) ? $form_data['token_ttl'] : '';
} else {
$data['name'] = $consumer->get_name();
$data['description'] = $consumer->get_description( true );
$data['type'] = $consumer->get_type();
$data['callback'] = $consumer->get_redirect_uris();
$data['client_credentials_enabled'] = $consumer->is_client_credentials_enabled();
+ $data['token_ttl'] = $consumer->get_token_ttl();
if ( is_array( $data['callback'] ) ) {
$data['callback'] = implode( ',', $data['callback'] );
@@ -457,6 +471,15 @@ function render_edit_page() {
+
+ |
+
+ |
+
+
+
+ |
+
get_post_id(), static::CLIENT_CREDENTIALS_ENABLED_KEY, true );
}
+ /**
+ * Get the token TTL for client credentials tokens.
+ *
+ * @return int|null TTL in seconds, or null if tokens should not expire.
+ */
+ public function get_token_ttl() {
+ $ttl = get_post_meta( $this->get_post_id(), static::TOKEN_TTL_KEY, true );
+
+ if ( $ttl === '' || $ttl === false ) {
+ return null;
+ }
+
+ return (int) $ttl;
+ }
+
/**
* Get registered URI for the client.
*
@@ -363,6 +379,10 @@ public static function create( $data ) {
static::CLIENT_CREDENTIALS_ENABLED_KEY => ! empty( $data['meta']['client_credentials_enabled'] ) ? '1' : '',
];
+ if ( isset( $data['meta']['token_ttl'] ) && $data['meta']['token_ttl'] !== '' ) {
+ $meta[ static::TOKEN_TTL_KEY ] = (int) $data['meta']['token_ttl'];
+ }
+
foreach ( $meta as $key => $value ) {
$result = update_post_meta( $post_id, wp_slash( $key ), wp_slash( $value ) );
if ( ! $result ) {
@@ -400,6 +420,12 @@ public function update( $data ) {
static::CLIENT_CREDENTIALS_ENABLED_KEY => ! empty( $data['meta']['client_credentials_enabled'] ) ? '1' : '',
];
+ if ( isset( $data['meta']['token_ttl'] ) && $data['meta']['token_ttl'] !== '' ) {
+ $meta[ static::TOKEN_TTL_KEY ] = (int) $data['meta']['token_ttl'];
+ } else {
+ $meta[ static::TOKEN_TTL_KEY ] = '';
+ }
+
foreach ( $meta as $key => $value ) {
update_post_meta( $post_id, wp_slash( $key ), wp_slash( $value ) );
}
diff --git a/inc/endpoints/class-token.php b/inc/endpoints/class-token.php
index 6a1520d..f20c1c4 100644
--- a/inc/endpoints/class-token.php
+++ b/inc/endpoints/class-token.php
@@ -178,13 +178,17 @@ private function handle_client_credentials( WP_REST_Request $request ) {
return $token;
}
- $ttl = apply_filters( 'oauth2.client_token_ttl', OAuth2\Tokens\Access_Token::DEFAULT_CLIENT_TOKEN_TTL );
-
- return [
+ $data = [
'access_token' => $token->get_key(),
'token_type' => 'bearer',
- 'expires_in' => $ttl,
];
+
+ $expires = $token->get_expiration_time();
+ if ( $expires !== null ) {
+ $data['expires_in'] = $expires - time();
+ }
+
+ return $data;
}
/**
diff --git a/inc/tokens/class-access-token.php b/inc/tokens/class-access-token.php
index 5704c4b..c4b15d2 100644
--- a/inc/tokens/class-access-token.php
+++ b/inc/tokens/class-access-token.php
@@ -18,7 +18,6 @@ class Access_Token extends Token {
const META_PREFIX = '_oauth2_access_';
const CLIENT_META_PREFIX = '_oauth2_client_token_';
const KEY_LENGTH = 12;
- const DEFAULT_CLIENT_TOKEN_TTL = 3600; // 1 hour in seconds
/**
* @return string Meta prefix. Client tokens use a distinct prefix because
@@ -276,13 +275,16 @@ public static function create_for_client( ClientInterface $client, $meta = [] )
);
}
- $ttl = apply_filters( 'oauth2.client_token_ttl', static::DEFAULT_CLIENT_TOKEN_TTL );
+ $ttl = $client->get_token_ttl();
$data = [
'client' => $client->get_id(),
'created' => time(),
- 'expires' => time() + $ttl,
'meta' => $meta,
];
+
+ if ( $ttl !== null ) {
+ $data['expires'] = time() + $ttl;
+ }
$key = wp_generate_password( static::KEY_LENGTH, false );
$meta_key = static::CLIENT_META_PREFIX . $key;
From 4a17266ae517cedda53a39fd681f8916b27e0205 Mon Sep 17 00:00:00 2001
From: Abhishek Kaushik
Date: Wed, 22 Apr 2026 14:55:35 +0200
Subject: [PATCH 3/3] Code review changes
---
inc/authentication/namespace.php | 3 ++-
inc/tokens/class-access-token.php | 6 +++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/inc/authentication/namespace.php b/inc/authentication/namespace.php
index 169403e..c246384 100644
--- a/inc/authentication/namespace.php
+++ b/inc/authentication/namespace.php
@@ -8,6 +8,7 @@
namespace WP\OAuth2\Authentication;
use WP_Error;
+use WP_Http;
use WP_User;
use WP\OAuth2\Tokens;
@@ -176,7 +177,7 @@ function attempt_authentication( $user = null ) {
'oauth2.authentication.token_expired',
__( 'Access token has expired.', 'oauth2' ),
[
- 'status' => \WP_Http::UNAUTHORIZED,
+ 'status' => WP_Http::UNAUTHORIZED,
]
);
return $user;
diff --git a/inc/tokens/class-access-token.php b/inc/tokens/class-access-token.php
index c4b15d2..8298bd4 100644
--- a/inc/tokens/class-access-token.php
+++ b/inc/tokens/class-access-token.php
@@ -15,9 +15,9 @@
use WP_User_Query;
class Access_Token extends Token {
- const META_PREFIX = '_oauth2_access_';
- const CLIENT_META_PREFIX = '_oauth2_client_token_';
- const KEY_LENGTH = 12;
+ const META_PREFIX = '_oauth2_access_';
+ const KEY_LENGTH = 12;
+ const CLIENT_META_PREFIX = '_oauth2_client_token_';
/**
* @return string Meta prefix. Client tokens use a distinct prefix because