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