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() {
+
+ |
+
+ |
+
+
+
+ |
+
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/class-client.php b/inc/class-client.php
index bc9df85..8c41a23 100644
--- a/inc/class-client.php
+++ b/inc/class-client.php
@@ -20,6 +20,7 @@ class Client implements ClientInterface {
const TYPE_KEY = '_oauth2_client_type';
const REDIRECT_URI_KEY = '_oauth2_redirect_uri';
const CLIENT_CREDENTIALS_ENABLED_KEY = '_oauth2_client_credentials_enabled';
+ const TOKEN_TTL_KEY = '_oauth2_client_token_ttl';
const AUTH_CODE_KEY_PREFIX = '_oauth2_authcode_';
const AUTH_CODE_LENGTH = 12;
const CLIENT_ID_LENGTH = 12;
@@ -142,6 +143,21 @@ public function is_client_credentials_enabled() {
return (bool) get_post_meta( $this->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 2bdfc09..f20c1c4 100644
--- a/inc/endpoints/class-token.php
+++ b/inc/endpoints/class-token.php
@@ -178,10 +178,17 @@ private function handle_client_credentials( WP_REST_Request $request ) {
return $token;
}
- return [
+ $data = [
'access_token' => $token->get_key(),
'token_type' => 'bearer',
];
+
+ $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 c055ebb..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 META_PREFIX = '_oauth2_access_';
+ const KEY_LENGTH = 12;
const CLIENT_META_PREFIX = '_oauth2_client_token_';
- const KEY_LENGTH = 12;
/**
* @return string Meta prefix. Client tokens use a distinct prefix because
@@ -275,11 +275,16 @@ public static function create_for_client( ClientInterface $client, $meta = [] )
);
}
- $data = [
+ $ttl = $client->get_token_ttl();
+ $data = [
'client' => $client->get_id(),
'created' => time(),
'meta' => $meta,
];
+
+ if ( $ttl !== null ) {
+ $data['expires'] = time() + $ttl;
+ }
$key = wp_generate_password( static::KEY_LENGTH, false );
$meta_key = static::CLIENT_META_PREFIX . $key;
@@ -305,12 +310,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();
}
}