Created
November 11, 2011 21:12
-
-
Save jmikola/1359290 to your computer and use it in GitHub Desktop.
Custom Symfony2 Voter for recent-activity and has-password security checks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8" ?> | |
<container xmlns="http://symfony.com/schema/dic/services" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> | |
<parameters> | |
<parameter key="security.last_activity_listener.class">Acme\DemoBundle\Listener\SecurityLastActivityListener</parameter> | |
<parameter key="security.authorization.voter.class">Acme\DemoBundle\Security\Authorization\Voter</parameter> | |
<parameter key="security.authorization.last_activity_timeout">900</parameter> | |
<parameter key="security.authorization.last_activity_attribute">_last_activity_at</parameter> | |
</parameters> | |
<services> | |
<service id="security.authorization.voter" class="%security.authorization.voter.class%"> | |
<tag name="security.voter" /> | |
<argument>%security.authorization.last_activity_attribute%</argument> | |
<argument>%security.authorization.last_activity_timeout%</argument> | |
</service> | |
<service id="security.last_activity_listener" class="%security.last_activity_listener.class%"> | |
<tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" /> | |
<tag name="kernel.event_listener" event="kernel.response" method="onCoreResponse" priority="10" /> | |
<argument type="service" id="security.context" /> | |
<argument type="service" id="security.authorization.voter" /> | |
<argument>%security.authorization.last_activity_attribute%</argument> | |
<argument type="service" id="logger" on-invalid="null" /> | |
</service> | |
</services> | |
</container> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Acme\DemoBundle\Listener; | |
use Acme\DemoBundle\Security\Authorization\Voter; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\HttpKernel\HttpKernelInterface; | |
use Symfony\Component\HttpKernel\Log\LoggerInterface; | |
use Symfony\Component\Security\Core\SecurityContextInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; | |
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; | |
use Symfony\Component\HttpKernel\Event\FilterResponseEvent; | |
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; | |
/** | |
* Initializes and updates the last activity timestamp in the current security | |
* token if it has not expired. In conjunction with AuthenticationTrustResolver, | |
* this listener allows for two-tier authentication. | |
*/ | |
class SecurityLastActivityListener | |
{ | |
/** | |
* @var Symfony\Component\Security\Core\SecurityContextInterface | |
*/ | |
private $context; | |
/** | |
* @var Acme\DemoBundle\Security\Authorization\Voter | |
*/ | |
private $voter; | |
/** | |
* @var integer | |
*/ | |
private $lastActivityAttribute; | |
/** | |
* @var Symfony\Component\HttpKernel\Log\LoggerInterface | |
*/ | |
private $logger; | |
/** | |
* Constructor. | |
* | |
* @param Symfony\Component\Security\Core\SecurityContextInterface $context | |
* @param Acme\DemoBundle\Security\Authorization\Voter $voter | |
* @param Symfony\Component\HttpKernel\Log\LoggerInterface $logger | |
*/ | |
public function __construct(SecurityContextInterface $context, Voter $voter, $lastActivityAttribute, LoggerInterface $logger = null) | |
{ | |
$this->context = $context; | |
$this->voter = $voter; | |
$this->lastActivityAttribute = $lastActivityAttribute; | |
$this->logger = $logger; | |
} | |
/** | |
* Listens to the "security.interactive_login" event and initializes the | |
* last activity timestamp when the user first logs in. | |
* | |
* @param Event $event | |
* @throws \InvalidArgumentException If the event's "token" parameter is invalid | |
*/ | |
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) | |
{ | |
$token = $event->getAuthenticationToken(); | |
$token->setAttribute($this->lastActivityAttribute, new \DateTime()); | |
if (null !== $this->logger) { | |
$this->logger->debug('Initializing last activity timestamp in SecurityContext token'); | |
} | |
} | |
/** | |
* Listens to the "core.response" event and updates the last activity | |
* timestamp if it is within the configured timeout interval. | |
* | |
* If this method does not update the timestamp, the same timeout interval | |
* will cause AuthenticationTrustResolver to consider the token's activity | |
* expired (so "fully authenticated" is considered "remembered") during | |
* subsequent requests. | |
* | |
* @param Event $event | |
* @return Response | |
*/ | |
public function onCoreResponse(FilterResponseEvent $event) | |
{ | |
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { | |
return; | |
} | |
if (!($token = $this->context->getToken()) instanceof TokenInterface) { | |
return; | |
} | |
if (VoterInterface::ACCESS_GRANTED === $this->voter->vote($token, $this, array(Voter::USER_RECENTLY_AUTHENTICATED))) { | |
$token->setAttribute($this->lastActivityAttribute, new \DateTime()); | |
if (null !== $this->logger) { | |
$this->logger->debug('Updating last activity timestamp in SecurityContext token'); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Acme\DemoBundle\Security\Authorization; | |
use Acme\DemoBundle\Model\User; | |
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; | |
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
class Voter implements VoterInterface | |
{ | |
const USER_HAS_PASSWORD = 'USER_HAS_PASSWORD'; | |
const USER_RECENTLY_AUTHENTICATED = 'USER_RECENTLY_AUTHENTICATED'; | |
private $lastActivityAttribute; | |
private $lastActivityTimeout; | |
public function __construct($lastActivityAttribute, $lastActivityTimeout) | |
{ | |
$this->lastActivityAttribute = $lastActivityAttribute; | |
$this->lastActivityTimeout = $lastActivityTimeout; | |
} | |
public function supportsAttribute($attribute) | |
{ | |
return in_array( | |
$attribute, | |
array( | |
self::USER_RECENTLY_AUTHENTICATED, | |
self::USER_HAS_PASSWORD | |
) | |
); | |
} | |
public function supportsClass($class) | |
{ | |
return true; | |
} | |
public function vote(TokenInterface $token, $object, array $attributes) | |
{ | |
$result = VoterInterface::ACCESS_ABSTAIN; | |
if (!$token->getUser() instanceof User) { | |
return VoterInterface::ACCESS_DENIED; | |
} | |
foreach ($attributes as $attribute) { | |
if (!$this->supportsAttribute($attribute)) { | |
continue; | |
} | |
$result = VoterInterface::ACCESS_DENIED; | |
if (self::USER_RECENTLY_AUTHENTICATED === $attribute && | |
false === $this->isLastActivityExpired($token)) { | |
$result = VoterInterface::ACCESS_GRANTED; | |
} | |
if (self::USER_HAS_PASSWORD === $attribute && | |
$this->isUserWithPassword($token)) { | |
$result = VoterInterface::ACCESS_GRANTED; | |
} | |
} | |
return $result; | |
} | |
/** | |
* Determines if the last activity timestamp in the token is expired. If no | |
* timestamp exists in the token, null will be returned since expiration | |
* cannot be determined. | |
* | |
* @param TokenInterface $token | |
* @return boolean or null if no timestamp exists in the token | |
* @throws \LogicException if the last activity timestamp in the token is invalid | |
*/ | |
private function isLastActivityExpired(TokenInterface $token) | |
{ | |
if ($token->hasAttribute($this->lastActivityAttribute)) { | |
$lastActivityAt = $token->getAttribute($this->lastActivityAttribute); | |
if (!$lastActivityAt instanceof \DateTime) { | |
throw new \LogicException(sprintf('The token attribute "%s" is not a DateTime object', $this->lastActivityAttribute)); | |
} | |
return $lastActivityAt->getTimestamp() + $this->lastActivityTimeout <= time(); | |
} | |
return true; | |
} | |
/** | |
* Checks whether the token contains a user with an empty password. | |
* | |
* @param TokenInterface $token | |
* @return boolean Whether the user's password is empty, or null if the token has no user | |
*/ | |
private function isUserWithPassword(TokenInterface $token) | |
{ | |
if ($token instanceof UsernamePasswordToken && ($user = $token->getUser()) instanceof UserInterface) { | |
return strlen($user->getPassword()) > 0; | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
nice