Skip to content

Commit

Permalink
Add email token expiry (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasparsd authored May 6, 2020
1 parent f8d2fda commit 873f782
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 4 deletions.
78 changes: 74 additions & 4 deletions providers/class.two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* The user meta token key.
*
* @type string
* @var string
*/
const TOKEN_META_KEY = '_two_factor_email_token';

/**
* Store the timestamp when the token was generated.
*
* @var string
*/
const TOKEN_META_KEY_TIMESTAMP = '_two_factor_email_token_timestamp';

/**
* Name of the input field used for code resend.
*
Expand Down Expand Up @@ -65,7 +72,10 @@ public function get_label() {
*/
public function generate_token( $user_id ) {
$token = $this->get_code();

update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() );
update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );

return $token;
}

Expand All @@ -80,9 +90,65 @@ public function user_has_token( $user_id ) {

if ( ! empty( $hashed_token ) ) {
return true;
} else {
}

return false;
}

/**
* Has the user token validity timestamp expired.
*
* @param integer $user_id User ID.
*
* @return boolean
*/
public function user_token_has_expired( $user_id ) {
$token_lifetime = $this->user_token_lifetime( $user_id );
$token_ttl = $this->user_token_ttl( $user_id );

// Invalid token lifetime is considered an expired token.
if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) {
return false;
}

return true;
}

/**
* Get the lifetime of a user token in seconds.
*
* @param integer $user_id User ID.
*
* @return integer|null Return `null` if the lifetime can't be measured.
*/
public function user_token_lifetime( $user_id ) {
$timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) );

if ( ! empty( $timestamp ) ) {
return time() - $timestamp;
}

return null;
}

/**
* Return the token time-to-live for a user.
*
* @param integer $user_id User ID.
*
* @return integer
*/
public function user_token_ttl( $user_id ) {
$token_ttl = 15 * MINUTE_IN_SECONDS;

/**
* Number of seconds the token is considered valid
* after the generation.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
*/
return (int) apply_filters( 'two_factor_token_ttl', $token_ttl, $user_id );
}

/**
Expand Down Expand Up @@ -119,7 +185,11 @@ public function validate_token( $user_id, $token ) {
return false;
}

// Ensure that the token can't be re-used.
if ( $this->user_token_has_expired( $user_id ) ) {
return false;
}

// Ensure the token can be used only once.
$this->delete_token( $user_id );

return true;
Expand Down Expand Up @@ -184,7 +254,7 @@ public function authentication_page( $user ) {
return;
}

if ( ! $this->user_has_token( $user->ID ) ) {
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
$this->generate_and_email_token( $user );
}

Expand Down
84 changes: 84 additions & 0 deletions tests/providers/class.two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,88 @@ function test_pre_process_authentication() {
$this->assertNotEquals( $token_original, $token_new, 'Failed to generate a new code as requested.' );
}

/**
* Ensure that a default TTL is set.
*
* @covers Two_Factor_Email::user_token_ttl
*/
public function test_user_token_has_ttl() {
$this->assertEquals(
15 * 60,
$this->provider->user_token_ttl( 123 ),
'Default TTL is 15 minutes'
);
}

/**
* Ensure the token generation time is stored.
*
* @covers Two_Factor_Email::user_token_lifetime
*/
public function test_tokens_have_generation_time() {
$user_id = $this->factory->user->create();

$this->assertFalse(
$this->provider->user_has_token( $user_id ),
'User does not have a valid token before requesting it'
);

$this->assertNull(
$this->provider->user_token_lifetime( $user_id ),
'Token lifetime is not present until a token is generated'
);

$this->provider->generate_token( $user_id );

$this->assertTrue(
$this->provider->user_has_token( $user_id ),
'User has a token after requesting it'
);

$this->assertTrue(
is_int( $this->provider->user_token_lifetime( $user_id ) ),
'Lifetime is a valid integer if present'
);

$this->assertFalse(
$this->provider->user_token_has_expired( $user_id ),
'Fresh token do not expire'
);
}

/**
* Ensure the token generation time is stored.
*
* @covers Two_Factor_Email::user_token_has_expired
* @covers Two_Factor_Email::validate_token
*/
public function test_tokens_can_expire() {
$user_id = $this->factory->user->create();
$token = $this->provider->generate_token( $user_id );

$this->assertFalse(
$this->provider->user_token_has_expired( $user_id ),
'Fresh token have not expired'
);

$this->assertTrue(
$this->provider->validate_token( $user_id, $token ),
'Fresh tokens are also valid'
);

// Update the generation time to one second before the TTL.
$expired_token_timestamp = time() - $this->provider->user_token_ttl( $user_id ) - 1;
update_user_meta( $user_id, Two_Factor_Email::TOKEN_META_KEY_TIMESTAMP, $expired_token_timestamp );

$this->assertTrue(
$this->provider->user_token_has_expired( $user_id ),
'Tokens expire after their TTL'
);

$this->assertFalse(
$this->provider->validate_token( $user_id, $token ),
'Expired tokens are invalid'
);
}

}

0 comments on commit 873f782

Please sign in to comment.