Skip to content
This repository has been archived by the owner on Jun 29, 2022. It is now read-only.

Commit

Permalink
Fix #218: Implement auto-login (#235)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
  • Loading branch information
2 people authored and devanych committed Feb 1, 2021
1 parent 37b31e2 commit 2334dfe
Show file tree
Hide file tree
Showing 13 changed files with 605 additions and 118 deletions.
38 changes: 0 additions & 38 deletions src/User/AuthenticationKeyInterface.php

This file was deleted.

64 changes: 64 additions & 0 deletions src/User/AutoLogin.php
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User;

use DateInterval;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Yii\Web\Cookie;

/**
* The service is used to send or remove auto-login cookie.
*
* @see AutoLoginIdentityInterface
* @see AutoLoginMiddleware
*/
final class AutoLogin
{
private string $cookieName = 'autoLogin';
private DateInterval $duration;

public function __construct(DateInterval $duration)
{
$this->duration = $duration;
}

public function withCookieName(string $name): self
{
$new = clone $this;
$new->cookieName = $name;
return $new;
}

/**
* Add auto-login cookie to response so the user is logged in automatically based on cookie even if session
* is expired.
*/
public function addCookie(AutoLoginIdentityInterface $identity, ResponseInterface $response): ResponseInterface
{
$data = json_encode([
$identity->getId(),
$identity->getAutoLoginKey()
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

return (new Cookie($this->cookieName, $data))
->withMaxAge($this->duration)
->addToResponse($response);
}

/**
* Expire auto-login cookie so user is not logged in automatically anymore.
*/
public function expireCookie(ResponseInterface $response): ResponseInterface
{
return (new Cookie($this->cookieName))
->expire()
->addToResponse($response);
}

public function getCookieName(): string
{
return $this->cookieName;
}
}
44 changes: 44 additions & 0 deletions src/User/AutoLoginIdentityInterface.php
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User;

use Yiisoft\Auth\IdentityInterface;

/**
* AutoLoginIdentityInterface should be implemented in order to automatically log user in based on a cookie.
*
* @see AutoLogin
* @see AutoLoginMiddleware
*/
interface AutoLoginIdentityInterface extends IdentityInterface
{
/**
* Returns a key that can be used to check the validity of a given identity ID.
*
* The key should be unique for each individual user, and should be persistent
* so that it can be used to check the validity of the user identity.
*
* The space of such keys should be big enough to defeat potential identity attacks.
*
* The returned key will be stored on the client side as part of a cookie and will be used to authenticate user even
* if PHP session has been expired.
*
* Make sure to invalidate earlier issued keys when you implement force user logout, password change and
* other scenarios, that require forceful access revocation for old sessions.
*
* @return string a key that is used to check the validity of a given identity ID.
* @see validateAutoLoginKey()
*/
public function getAutoLoginKey(): string;

/**
* Validates the given key.
*
* @param string $key the given key
* @return bool whether the given key is valid.
* @see getAutoLoginKey()
*/
public function validateAutoLoginKey(string $key): bool;
}
91 changes: 87 additions & 4 deletions src/User/AutoLoginMiddleware.php
@@ -1,16 +1,99 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User;

use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Log\LoggerInterface;
use Yiisoft\Auth\IdentityRepositoryInterface;

/**
* AutoLoginMiddleware automatically logs user in based on "remember me" cookie
* AutoLoginMiddleware automatically logs user in based on cookie.
*/
class AutoLoginMiddleware
final class AutoLoginMiddleware implements MiddlewareInterface
{
private User $user;
private IdentityRepositoryInterface $identityRepository;
private LoggerInterface $logger;
private AutoLogin $autoLogin;

public function __construct(User $user)
{
public function __construct(
User $user,
IdentityRepositoryInterface $identityRepository,
LoggerInterface $logger,
AutoLogin $autoLogin
) {
$this->user = $user;
$this->identityRepository = $identityRepository;
$this->logger = $logger;
$this->autoLogin = $autoLogin;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->authenticateUserByCookieFromRequest($request);
$guestBeforeHandle = $this->user->isGuest();
$response = $handler->handle($request);
$guestAfterHandle = $this->user->isGuest();

if ($guestBeforeHandle && !$guestAfterHandle) {
$this->autoLogin->addCookie($this->user->getIdentity(false), $response);
}

if (!$guestBeforeHandle && $guestAfterHandle) {
$this->autoLogin->expireCookie($response);
}

return $response;
}

/**
* Authenticate user by auto login cookie from request.
*
* @param ServerRequestInterface $request Request instance containing auto login cookie.
*/
private function authenticateUserByCookieFromRequest(ServerRequestInterface $request): void
{
$cookieName = $this->autoLogin->getCookieName();
$cookies = $request->getCookieParams();

if (!array_key_exists($cookieName, $cookies)) {
return;
}

try {
$data = json_decode($cookies[$cookieName], true, 512, JSON_THROW_ON_ERROR);
} catch (\Exception $e) {
$this->logger->warning('Unable to authenticate user by cookie. Invalid cookie.');
return;
}

if (!is_array($data) || count($data) !== 2) {
$this->logger->warning('Unable to authenticate user by cookie. Invalid cookie.');
return;
}

[$id, $key] = $data;
$identity = $this->identityRepository->findIdentity($id);

if ($identity === null) {
$this->logger->warning("Unable to authenticate user by cookie. Identity \"$id\" not found.");
return;
}

if (!$identity instanceof AutoLoginIdentityInterface) {
throw new \RuntimeException('Identity repository must return an instance of \Yiisoft\Yii\Web\User\AutoLoginIdentityInterface in order for auto-login to function.');
}

if (!$identity->validateAutoLoginKey($key)) {
$this->logger->warning('Unable to authenticate user by cookie. Invalid key.');
return;
}

$this->user->login($identity);
}
}
11 changes: 3 additions & 8 deletions src/User/Event/AfterLogin.php
@@ -1,27 +1,22 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User\Event;

use Yiisoft\Auth\IdentityInterface;

class AfterLogin
{
private IdentityInterface $identity;
private int $duration;

public function __construct(IdentityInterface $identity, int $duration)
public function __construct(IdentityInterface $identity)
{
$this->identity = $identity;
$this->duration = $duration;
}

public function getIdentity(): IdentityInterface
{
return $this->identity;
}

public function getDuration(): int
{
return $this->duration;
}
}
2 changes: 2 additions & 0 deletions src/User/Event/AfterLogout.php
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User\Event;

use Yiisoft\Auth\IdentityInterface;
Expand Down
11 changes: 3 additions & 8 deletions src/User/Event/BeforeLogin.php
@@ -1,19 +1,19 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User\Event;

use Yiisoft\Auth\IdentityInterface;

class BeforeLogin
{
private IdentityInterface $identity;
private int $duration;
private bool $isValid = true;

public function __construct(IdentityInterface $identity, int $duration)
public function __construct(IdentityInterface $identity)
{
$this->identity = $identity;
$this->duration = $duration;
}

public function invalidate(): void
Expand All @@ -30,9 +30,4 @@ public function getIdentity(): IdentityInterface
{
return $this->identity;
}

public function getDuration(): int
{
return $this->duration;
}
}
2 changes: 2 additions & 0 deletions src/User/Event/BeforeLogout.php
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User\Event;

use Yiisoft\Auth\IdentityInterface;
Expand Down
2 changes: 2 additions & 0 deletions src/User/GuestIdentity.php
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Web\User;

use Yiisoft\Auth\IdentityInterface;
Expand Down

0 comments on commit 2334dfe

Please sign in to comment.