Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions inc/admin/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ) );
Expand Down Expand Up @@ -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'],
],
];

Expand All @@ -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'],
],
];

Expand Down Expand Up @@ -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'] );
Expand Down Expand Up @@ -457,6 +471,15 @@ function render_edit_page() {
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="oauth-token-ttl"><?php echo esc_html_x( 'Token TTL (seconds)', 'field name', 'oauth2' ); ?></label>
</th>
<td>
<input type="number" class="regular-text" name="token_ttl" id="oauth-token-ttl" value="<?php echo esc_attr( $data['token_ttl'] ); ?>" min="0" />
<p class="description"><?php esc_html_e( 'Time-to-live for client credentials tokens in seconds. Leave empty for tokens that do not expire.', 'oauth2' ); ?></p>
</td>
</tr>
</table>

<?php
Expand Down
14 changes: 14 additions & 0 deletions inc/authentication/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace WP\OAuth2\Authentication;

use WP_Error;
use WP_Http;
use WP_User;
use WP\OAuth2\Tokens;

Expand Down Expand Up @@ -169,6 +170,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;

Expand Down
26 changes: 26 additions & 0 deletions inc/class-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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 ) );
}
Expand Down
9 changes: 8 additions & 1 deletion inc/endpoints/class-token.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
38 changes: 34 additions & 4 deletions inc/tokens/class-access-token.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

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