Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.4] Secure password reset tokens against timing attacks and compromised DB #16850

Merged
merged 8 commits into from
Dec 19, 2016
Merged
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
29 changes: 24 additions & 5 deletions src/Illuminate/Auth/Passwords/DatabaseTokenRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class DatabaseTokenRepository implements TokenRepositoryInterface
Expand Down Expand Up @@ -37,6 +38,13 @@ class DatabaseTokenRepository implements TokenRepositoryInterface
*/
protected $expires;

/**
* The hasher implementation.
*
* @var \Illuminate\Contracts\Hashing\Hasher
*/
protected $hasher;

/**
* Create a new token repository instance.
*
Expand All @@ -46,12 +54,13 @@ class DatabaseTokenRepository implements TokenRepositoryInterface
* @param int $expires
* @return void
*/
public function __construct(ConnectionInterface $connection, $table, $hashKey, $expires = 60)
public function __construct(ConnectionInterface $connection, HasherContract $hasher, $table, $hashKey, $expires = 60)
{
$this->table = $table;
$this->hashKey = $hashKey;
$this->expires = $expires * 60;
$this->connection = $connection;
$this->hasher = $hasher;
}

/**
Expand Down Expand Up @@ -96,7 +105,7 @@ protected function deleteExisting(CanResetPasswordContract $user)
*/
protected function getPayload($email, $token)
{
return ['email' => $email, 'token' => $token, 'created_at' => new Carbon];
return ['email' => $email, 'token' => $this->hasher->make($token), 'created_at' => new Carbon];
}

/**
Expand All @@ -106,13 +115,13 @@ protected function getPayload($email, $token)
* @param string $token
* @return bool
*/
public function exists(CanResetPasswordContract $user, $token)
public function exists(CanResetPasswordContract $user, $userToken)
{
$email = $user->getEmailForPasswordReset();

$token = (array) $this->getTable()->where('email', $email)->where('token', $token)->first();
$token = (array) $this->getTable()->where('email', $email)->first();

return $token && ! $this->tokenExpired($token);
return $token && ! $this->tokenExpired($token) && $this->hasher->check($userToken, $token['token']);
}

/**
Expand Down Expand Up @@ -180,4 +189,14 @@ public function getConnection()
{
return $this->connection;
}

/**
* Get the hasher instance.
*
* @return \Illuminate\Contracts\Hashing\Hasher
*/
public function getHasher()
{
return $this->hasher;
}
}
1 change: 1 addition & 0 deletions src/Illuminate/Auth/Passwords/PasswordBrokerManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ protected function createTokenRepository(array $config)

return new DatabaseTokenRepository(
$this->app['db']->connection($connection),
$this->app['hash'],
$config['table'],
$key,
$config['expire']
Expand Down
29 changes: 23 additions & 6 deletions tests/Auth/AuthDatabaseTokenRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function tearDown()
public function testCreateInsertsNewRecordIntoTable()
{
$repo = $this->getRepo();
$repo->getHasher()->shouldReceive('make')->andReturn('hashed-token');
$repo->getConnection()->shouldReceive('table')->with('table')->andReturn($query = m::mock('StdClass'));
$query->shouldReceive('where')->with('email', 'email')->andReturn($query);
$query->shouldReceive('delete')->once();
Expand All @@ -30,7 +31,6 @@ public function testExistReturnsFalseIfNoRowFoundForUser()
$repo = $this->getRepo();
$repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock('StdClass'));
$query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query);
$query->shouldReceive('where')->once()->with('token', 'token')->andReturn($query);
$query->shouldReceive('first')->andReturn(null);
$user = m::mock('Illuminate\Contracts\Auth\CanResetPassword');
$user->shouldReceive('getEmailForPasswordReset')->andReturn('email');
Expand All @@ -41,11 +41,11 @@ public function testExistReturnsFalseIfNoRowFoundForUser()
public function testExistReturnsFalseIfRecordIsExpired()
{
$repo = $this->getRepo();
$repo->getHasher()->shouldReceive('check')->with('token', 'hashed-token')->andReturn(true);
$repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock('StdClass'));
$query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query);
$query->shouldReceive('where')->once()->with('token', 'token')->andReturn($query);
$date = date('Y-m-d H:i:s', time() - 300000);
$query->shouldReceive('first')->andReturn((object) ['created_at' => $date]);
$query->shouldReceive('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']);
$user = m::mock('Illuminate\Contracts\Auth\CanResetPassword');
$user->shouldReceive('getEmailForPasswordReset')->andReturn('email');

Expand All @@ -55,17 +55,31 @@ public function testExistReturnsFalseIfRecordIsExpired()
public function testExistReturnsTrueIfValidRecordExists()
{
$repo = $this->getRepo();
$repo->getHasher()->shouldReceive('check')->with('token', 'hashed-token')->andReturn(true);
$repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock('StdClass'));
$query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query);
$query->shouldReceive('where')->once()->with('token', 'token')->andReturn($query);
$date = date('Y-m-d H:i:s', time() - 600);
$query->shouldReceive('first')->andReturn((object) ['created_at' => $date]);
$query->shouldReceive('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']);
$user = m::mock('Illuminate\Contracts\Auth\CanResetPassword');
$user->shouldReceive('getEmailForPasswordReset')->andReturn('email');

$this->assertTrue($repo->exists($user, 'token'));
}

public function testExistReturnsFalseIfInvalidToken()
{
$repo = $this->getRepo();
$repo->getHasher()->shouldReceive('check')->with('wrong-token', 'hashed-token')->andReturn(false);
$repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock('StdClass'));
$query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query);
$date = date('Y-m-d H:i:s', time() - 600);
$query->shouldReceive('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']);
$user = m::mock('Illuminate\Contracts\Auth\CanResetPassword');
$user->shouldReceive('getEmailForPasswordReset')->andReturn('email');

$this->assertFalse($repo->exists($user, 'wrong-token'));
}

public function testDeleteMethodDeletesByToken()
{
$repo = $this->getRepo();
Expand All @@ -88,6 +102,9 @@ public function testDeleteExpiredMethodDeletesExpiredTokens()

protected function getRepo()
{
return new Illuminate\Auth\Passwords\DatabaseTokenRepository(m::mock('Illuminate\Database\Connection'), 'table', 'key');
return new Illuminate\Auth\Passwords\DatabaseTokenRepository(
m::mock('Illuminate\Database\Connection'),
m::mock('Illuminate\Contracts\Hashing\Hasher'),
'table', 'key');
}
}