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(); } }