Skip to content
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
39 changes: 39 additions & 0 deletions src/Auth/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@ public function getAuthenticateUserId(): mixed
return $this->attributes[$this->primary_key];
}

/**
* The name of the column holding the remember-me token.
*
* Override in the model if your column is named differently. The application
* must add this column to its user table: `remember_token VARCHAR(100) NULL`.
*
* @return string
*/
public function getRememberTokenName(): string
{
return 'remember_token';
}

/**
* Read the current remember-me token.
*
* @return ?string
*/
public function getRememberToken(): ?string
{
$value = $this->attributes[$this->getRememberTokenName()] ?? null;

return is_null($value) ? null : (string) $value;
}

/**
* Store a new remember-me token and persist it.
*
* @param ?string $token
* @return void
*/
public function setRememberToken(?string $token): void
{
$column = $this->getRememberTokenName();

$this->attributes[$column] = $token;
$this->update([$column => $token]);
}

/**
* Define the additional values
*
Expand Down
8 changes: 5 additions & 3 deletions src/Auth/Guards/GuardContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ abstract public function guest(): bool;
abstract public function logout(): bool;

/**
* Logout
* Login
*
* @param Authentication $user
* @param bool $remember
* @return bool
*/
abstract public function login(Authentication $user): bool;
abstract public function login(Authentication $user, bool $remember = false): bool;

/**
* Get authenticated user
Expand All @@ -67,9 +68,10 @@ abstract public function user(): ?Authentication;
* Check if user is authenticated
*
* @param array $credentials
* @param bool $remember
* @return bool
*/
abstract public function attempts(array $credentials): bool;
abstract public function attempts(array $credentials, bool $remember = false): bool;

/**
* Get the guard name
Expand Down
8 changes: 6 additions & 2 deletions src/Auth/Guards/JwtGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ public function __construct(array $provider, string $guard)
* Check if user is authenticated
*
* @param array $credentials
* @param bool $remember
* @return bool
* @throws AuthenticationException
* @throws Exception
*/
public function attempts(array $credentials): bool
public function attempts(array $credentials, bool $remember = false): bool
{
// $remember is ignored: JWT is stateless, remember-me is a session concept.
$user = $this->makeLogin($credentials);

$this->token = null;
Expand Down Expand Up @@ -143,11 +145,13 @@ private function getPolicier(): Policier
* Make direct login
*
* @param Authentication $user
* @param bool $remember
* @return bool
* @throws Exception
*/
public function login(Authentication $user): bool
public function login(Authentication $user, bool $remember = false): bool
{
// $remember is ignored: JWT is stateless, remember-me is a session concept.
$attributes = array_merge(
$user->customJwtAttributes(),
[
Expand Down
131 changes: 127 additions & 4 deletions src/Auth/Guards/SessionGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Bow\Auth\Exception\AuthenticationException;
use Bow\Auth\Traits\LoginUserTrait;
use Bow\Security\Hash;
use Bow\Session\Cookie;
use Bow\Session\Exception\SessionException;
use Bow\Session\Session;

Expand Down Expand Up @@ -46,10 +47,11 @@ public function __construct(array $provider, string $guard)
* Check if user is authenticated
*
* @param array $credentials
* @param bool $remember
* @return bool
* @throws AuthenticationException|SessionException
*/
public function attempts(array $credentials): bool
public function attempts(array $credentials, bool $remember = false): bool
{
$user = $this->makeLogin($credentials);

Expand All @@ -62,6 +64,11 @@ public function attempts(array $credentials): bool

if (Hash::check($password, $user->{$fields['password']})) {
$this->getSession()->put($this->session_key, $user);

if ($remember) {
$this->setRememberCookie($user);
}

return true;
}

Expand All @@ -76,7 +83,8 @@ public function attempts(array $credentials): bool
*/
public function check(): bool
{
return $this->getSession()->exists($this->session_key);
return $this->getSession()->exists($this->session_key)
|| $this->attemptRememberLogin();
}

/**
Expand Down Expand Up @@ -112,14 +120,19 @@ public function guest(): bool
/**
* Make direct login
*
* @param mixed $user
* @param Authentication $user
* @param bool $remember
* @return bool
* @throws AuthenticationException|SessionException
*/
public function login(Authentication $user): bool
public function login(Authentication $user, bool $remember = false): bool
{
$this->getSession()->add($this->session_key, $user);

if ($remember) {
$this->setRememberCookie($user);
}

return true;
}

Expand All @@ -131,6 +144,13 @@ public function login(Authentication $user): bool
*/
public function logout(): bool
{
$user = $this->getSession()->get($this->session_key);

if ($user instanceof Authentication) {
$user->setRememberToken($this->generateRememberToken());
}

$this->clearRememberCookie();
$this->getSession()->remove($this->session_key);

return true;
Expand Down Expand Up @@ -161,6 +181,109 @@ public function id(): mixed
*/
public function user(): ?Authentication
{
if (!$this->getSession()->exists($this->session_key)) {
$this->attemptRememberLogin();
}

return $this->getSession()->get($this->session_key);
}

/**
* Attempt to restore the session from a valid remember-me cookie.
*
* Never throws on malformed input: a bad cookie is simply cleared.
*
* @return bool
* @throws AuthenticationException|SessionException
*/
private function attemptRememberLogin(): bool
{
$cookie = Cookie::get($this->rememberCookieName());

if (!is_string($cookie) || !str_contains($cookie, '|')) {
if (!is_null($cookie)) {
$this->clearRememberCookie();
}
return false;
}

[$id, $token] = explode('|', $cookie, 2);

$user = $this->getUserById($id);

if (is_null($user)) {
$this->clearRememberCookie();
return false;
}

$stored = $user->getRememberToken();

if (is_null($stored) || !hash_equals($stored, $token)) {
$this->clearRememberCookie();
return false;
}

$this->getSession()->put($this->session_key, $user);

return true;
}

/**
* Generate a fresh remember token and persist it on the user, then
* write the encrypted remember cookie.
*
* @param Authentication $user
* @return void
*/
private function setRememberCookie(Authentication $user): void
{
$token = $this->generateRememberToken();
$user->setRememberToken($token);

Cookie::set(
$this->rememberCookieName(),
$user->getAuthenticateUserId() . '|' . $token,
$this->rememberLifetime()
);
}

/**
* Remove the remember cookie.
*
* @return void
*/
private function clearRememberCookie(): void
{
Cookie::remove($this->rememberCookieName());
}

/**
* Get the remember cookie name for this guard.
*
* @return string
*/
private function rememberCookieName(): string
{
return 'remember_' . $this->guard;
}

/**
* Generate a cryptographically strong remember token.
*
* @return string
*/
private function generateRememberToken(): string
{
return bin2hex(random_bytes(30));
}

/**
* Get the configured remember-me cookie lifetime in seconds.
*
* @return int
*/
private function rememberLifetime(): int
{
return (int) (config('auth.remember_lifetime') ?? 2592000);
}
}
61 changes: 61 additions & 0 deletions src/Auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,64 @@ $user = $auth->user();
```

Enjoy!

## Remember me

The session guard supports a persistent "remember me" cookie so users stay authenticated across browser sessions.

### Required migration

The framework cannot modify your application's database, so you must add a nullable token column to your user table:

```sql
ALTER TABLE users ADD COLUMN remember_token VARCHAR(100) NULL;
```

The default column name is `remember_token`. Override it on your user model if needed:

```php
public function getRememberTokenName(): string
{
return 'my_token_column';
}
```

### Usage

Pass `true` as the second argument to `attempts()` or `login()`:

```php
// Authenticate with credentials and remember the user
Auth::attempts(['username' => $username, 'password' => $password], true);

// Or, for an already-resolved user instance
Auth::login($user, true);
```

### Automatic session restore

When the session has expired, the next call to `Auth::check()` or `Auth::user()` transparently restores the session from the encrypted `remember_<guard>` cookie (e.g. `remember_web`). No changes are needed in `AuthMiddleware`.

### Logout

`Auth::logout()` regenerates the user's token (invalidating any outstanding remember cookie) and clears the cookie:

```php
Auth::logout();
```

Because all devices share the same `remember_token` column, logging out on one device invalidates remember-me for that user everywhere.

### Configuration

The cookie lifetime is read from `config('auth.remember_lifetime')` in seconds and defaults to 30 days. Add the key to your `config/auth.php` to override it:

```php
'remember_lifetime' => 2592000, // 30 days (default)
```

### Scope and security notes

- Remember-me applies to the **session guard only**; the JWT guard ignores the flag.
- The token is high-entropy (`bin2hex(random_bytes(30))`) and compared timing-safely with `hash_equals`; the cookie is encrypted.
- A single shared `remember_token` column means logging out invalidates remember-me for that user on all devices.
13 changes: 13 additions & 0 deletions src/Auth/Traits/LoginUserTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,17 @@ private function getUserBy(string $key, float|int|string $value): ?Authenticatio

return $model::where($key, $value)->first();
}

/**
* Get a user by primary key value.
*
* @param float|int|string $id
* @return ?Authentication
*/
private function getUserById(float|int|string $id): ?Authentication
{
$model = $this->provider['model'];

return $this->getUserBy((new $model())->getKey(), $id);
}
}
Loading
Loading