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
22 changes: 21 additions & 1 deletion src/Illuminate/Auth/SessionGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Auth\Events\Logout;
use Illuminate\Auth\Events\OtherDeviceLogout;
use Illuminate\Auth\Events\Validated;
use Illuminate\Container\Container;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Auth\SupportsBasicAuth;
Expand Down Expand Up @@ -600,7 +601,7 @@ protected function ensureRememberTokenIsSet(AuthenticatableContract $user)
protected function queueRecallerCookie(AuthenticatableContract $user)
{
$this->getCookieJar()->queue($this->createRecaller(
$user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.$user->getAuthPassword()
$user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.$this->hashPasswordForCookie($user->getAuthPassword())
));
}

Expand All @@ -615,6 +616,25 @@ protected function createRecaller($value)
return $this->getCookieJar()->make($this->getRecallerName(), $value, $this->getRememberDuration());
}

/**
* Create a HMAC of the password hash for storage in cookies.
*
* @param string $passwordHash
* @return string
*/
public function hashPasswordForCookie($passwordHash)
{
$container = Container::getInstance();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty hacky. The data should be passed through the session guard, rather than it calling out to the container. The container isn't even a dependency of the auth package, so this would break people installing this package directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GrahamCampbell I can take a look at this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I wasn't sure how to reliably get hold of the app secret, thanks for dealing with it @taylorotwell.


if ($container && $container->bound('config')) {
$key = $container->make('config')->get('app.key');
} else {
$key = 'base-key-for-password-hash-mac';
}

return hash_hmac('sha256', $passwordHash, $key);
}

/**
* Log the user out of the application.
*
Expand Down
32 changes: 28 additions & 4 deletions src/Illuminate/Session/Middleware/AuthenticateSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ public function handle($request, Closure $next)
}

if ($this->guard()->viaRemember()) {
$passwordHash = explode('|', $request->cookies->get($this->guard()->getRecallerName()))[2] ?? null;
$passwordHashFromCookie = explode('|', $request->cookies->get($this->guard()->getRecallerName()))[2] ?? null;

if (! $passwordHash || ! hash_equals($request->user()->getAuthPassword(), $passwordHash)) {
if (! $passwordHashFromCookie || ! $this->validatePasswordHash($request->user()->getAuthPassword(), $passwordHashFromCookie)) {
$this->logout($request);
}
}
Expand All @@ -59,7 +59,9 @@ public function handle($request, Closure $next)
$this->storePasswordHashInSession($request);
}

if (! hash_equals($request->session()->get('password_hash_'.$this->auth->getDefaultDriver()), $request->user()->getAuthPassword())) {
$sessionPasswordHash = $request->session()->get('password_hash_'.$this->auth->getDefaultDriver());

if (! $this->validatePasswordHash($request->user()->getAuthPassword(), $sessionPasswordHash)) {
$this->logout($request);
}

Expand All @@ -83,7 +85,7 @@ protected function storePasswordHashInSession($request)
}

$request->session()->put([
'password_hash_'.$this->auth->getDefaultDriver() => $request->user()->getAuthPassword(),
'password_hash_'.$this->auth->getDefaultDriver() => $this->guard()->hashPasswordForCookie($request->user()->getAuthPassword()),
]);
}

Expand Down Expand Up @@ -129,6 +131,28 @@ protected function redirectTo(Request $request)
}
}

/**
* Validate the password hash against the stored value.
*
* This method first tries to validate using HMAC (new format),
* and falls back to raw password hash comparison (old format)
* for backward compatibility.
*
* @param string $passwordHash
* @param string $storedValue
* @return bool
*/
protected function validatePasswordHash($passwordHash, $storedValue)
{
// Try new HMAC format first
if (hash_equals($this->guard()->hashPasswordForCookie($passwordHash), $storedValue)) {
return true;
}

// Fall back to old raw password hash format for backward compatibility
return hash_equals($passwordHash, $storedValue);
}

/**
* Specify the callback that should be used to generate the redirect path.
*
Expand Down
6 changes: 4 additions & 2 deletions tests/Auth/AuthGuardTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,8 @@ public function testLoginMethodQueuesCookieWhenRemembering()
$guard = new SessionGuard('default', $provider, $session, $request);
$guard->setCookieJar($cookie);
$foreverCookie = new Cookie($guard->getRecallerName(), 'foo');
$cookie->shouldReceive('make')->once()->with($guard->getRecallerName(), 'foo|recaller|bar', 576000)->andReturn($foreverCookie);
$expectedHash = hash_hmac('sha256', 'bar', 'base-key-for-password-hash-mac');
$cookie->shouldReceive('make')->once()->with($guard->getRecallerName(), 'foo|recaller|'.$expectedHash, 576000)->andReturn($foreverCookie);
$cookie->shouldReceive('queue')->once()->with($foreverCookie);
$guard->getSession()->shouldReceive('put')->once()->with($guard->getName(), 'foo');
$session->shouldReceive('regenerate')->once();
Expand All @@ -518,7 +519,8 @@ public function testLoginMethodQueuesCookieWhenRememberingAndAllowsOverride()
$guard->setRememberDuration(5000);
$guard->setCookieJar($cookie);
$foreverCookie = new Cookie($guard->getRecallerName(), 'foo');
$cookie->shouldReceive('make')->once()->with($guard->getRecallerName(), 'foo|recaller|bar', 5000)->andReturn($foreverCookie);
$expectedHash = hash_hmac('sha256', 'bar', 'base-key-for-password-hash-mac');
$cookie->shouldReceive('make')->once()->with($guard->getRecallerName(), 'foo|recaller|'.$expectedHash, 5000)->andReturn($foreverCookie);
$cookie->shouldReceive('queue')->once()->with($foreverCookie);
$guard->getSession()->shouldReceive('put')->once()->with($guard->getName(), 'foo');
$session->shouldReceive('regenerate')->once();
Expand Down
62 changes: 56 additions & 6 deletions tests/Session/Middleware/AuthenticateSessionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,13 @@ public function getAuthPassword()
$authFactory->shouldReceive('viaRemember')->andReturn(false);
$authFactory->shouldReceive('getDefaultDriver')->andReturn('web');
$authFactory->shouldReceive('user')->andReturn(null);
// expected MAC for current password when storing in session:
$authFactory->shouldReceive('hashPasswordForCookie')->with('my-pass-(*&^%$#!@')->andReturn('mac:my-pass-(*&^%$#!@');

$middleware = new AuthenticateSession($authFactory);
$response = $middleware->handle($request, fn () => 'next-4');

$this->assertEquals('my-pass-(*&^%$#!@', $session->get('password_hash_web'));
$this->assertEquals('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web'));
$this->assertEquals('next-4', $response);
}

Expand All @@ -112,7 +114,7 @@ public function getAuthPassword()
}
};

$request = new Request(cookies: ['recaller-name' => 'a|b|my-pass-dont-match']);
$request = new Request(cookies: ['recaller-name' => 'a|b|invalid-mac']);
$request->setUserResolver(fn () => $user);

$session = new Store('name', new ArraySessionHandler(1));
Expand All @@ -127,6 +129,8 @@ public function getAuthPassword()
$authFactory->shouldReceive('logoutCurrentDevice')->once()->andReturn(null);
$authFactory->shouldReceive('getDefaultDriver')->andReturn('web');
$authFactory->shouldReceive('user')->andReturn(null);
// expected MAC for current password (won't match cookie):
$authFactory->shouldReceive('hashPasswordForCookie')->with('my-pass-(*&^%$#!@')->andReturn('mac:my-pass-(*&^%$#!@');

$this->assertNotNull($session->get('a'));
$this->assertNotNull($session->get('b'));
Expand Down Expand Up @@ -159,7 +163,7 @@ public function getAuthPassword()
}
};

$request = new Request(cookies: ['recaller-name' => 'a|b|my-pass-dont-match']);
$request = new Request(cookies: ['recaller-name' => 'a|b|invalid-mac']);
$request->setUserResolver(fn () => $user);

$session = new Store('name', new ArraySessionHandler(1));
Expand All @@ -174,6 +178,8 @@ public function getAuthPassword()
$authFactory->shouldReceive('logoutCurrentDevice')->once();
$authFactory->shouldReceive('getDefaultDriver')->andReturn('web');
$authFactory->shouldReceive('user')->andReturn(null);
// expected MAC for current password (won't match cookie):
$authFactory->shouldReceive('hashPasswordForCookie')->with('my-pass-(*&^%$#!@')->andReturn('mac:my-pass-(*&^%$#!@');

$middleware = new AuthenticateSession($authFactory);
// act:
Expand Down Expand Up @@ -201,7 +207,7 @@ public function getAuthPassword()
}
};

$request = new Request(cookies: ['recaller-name' => 'a|b|my-pass-(*&^%$#!@']);
$request = new Request(cookies: ['recaller-name' => 'a|b|mac:my-pass-(*&^%$#!@']);
$request->setUserResolver(fn () => $user);

$session = new Store('name', new ArraySessionHandler(1));
Expand All @@ -217,6 +223,8 @@ public function getAuthPassword()
$authFactory->shouldReceive('logoutCurrentDevice')->once()->andReturn(null);
$authFactory->shouldReceive('getDefaultDriver')->andReturn('web');
$authFactory->shouldReceive('user')->andReturn(null);
// expected MAC for current password (matches cookie but not session):
$authFactory->shouldReceive('hashPasswordForCookie')->with('my-pass-(*&^%$#!@')->andReturn('mac:my-pass-(*&^%$#!@');

// act:
$middleware = new AuthenticateSession($authFactory);
Expand Down Expand Up @@ -250,7 +258,7 @@ public function getAuthPassword()
$session = new Store('name', new ArraySessionHandler(1));
$session->put('a', '1');
$session->put('b', '2');
$session->put('password_hash_web', 'my-pass-(*&^%$#!@');
$session->put('password_hash_web', 'mac:my-pass-(*&^%$#!@');
// set session on the request:
$request->setLaravelSession($session);

Expand All @@ -260,14 +268,56 @@ public function getAuthPassword()
$authFactory->shouldReceive('logoutCurrentDevice')->never();
$authFactory->shouldReceive('getDefaultDriver')->andReturn('web');
$authFactory->shouldReceive('user')->andReturn($user);
// expected MAC for current password:
$authFactory->shouldReceive('hashPasswordForCookie')->with('my-pass-(*&^%$#!@')->andReturn('mac:my-pass-(*&^%$#!@');

// act:
$middleware = new AuthenticateSession($authFactory);
$response = $middleware->handle($request, fn () => 'next-8');

$this->assertEquals('next-8', $response);
// ensure session is flushed:
$this->assertEquals('my-pass-(*&^%$#!@', $session->get('password_hash_web'));
$this->assertEquals('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web'));
$this->assertEquals('1', $session->get('a'));
$this->assertEquals('2', $session->get('b'));
}

public function test_handle_with_old_format_cookie_for_backward_compatibility()
{
$user = new class
{
public function getAuthPassword()
{
return 'my-pass-(*&^%$#!@';
}
};

// Cookie contains OLD format (raw password hash, not HMAC)
$request = new Request(cookies: ['recaller-name' => 'a|b|my-pass-(*&^%$#!@']);
$request->setUserResolver(fn () => $user);

$session = new Store('name', new ArraySessionHandler(1));
$session->put('a', '1');
$session->put('b', '2');
// Session also contains old format for this test
$session->put('password_hash_web', 'my-pass-(*&^%$#!@');
$request->setLaravelSession($session);

$authFactory = Mockery::mock(AuthFactory::class);
$authFactory->shouldReceive('viaRemember')->andReturn(true);
$authFactory->shouldReceive('getRecallerName')->once()->andReturn('recaller-name');
$authFactory->shouldReceive('getDefaultDriver')->andReturn('web');
$authFactory->shouldReceive('user')->andReturn($user);
// The HMAC won't match the old format, but fallback to raw hash should work
$authFactory->shouldReceive('hashPasswordForCookie')->with('my-pass-(*&^%$#!@')->andReturn('mac:my-pass-(*&^%$#!@');

$middleware = new AuthenticateSession($authFactory);
$response = $middleware->handle($request, fn () => 'next-9');

// Should succeed because of backward compatibility fallback
$this->assertEquals('next-9', $response);
// Session should be updated to new format (HMAC)
$this->assertEquals('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web'));
$this->assertEquals('1', $session->get('a'));
$this->assertEquals('2', $session->get('b'));
}
Expand Down
Loading