-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security] Implement support for CSRF tokens in logout URL's #3007
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
Changes from all commits
c48c775
b1f545b
aaaa040
66722b3
4837407
a96105e
49a8654
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\SecurityBundle\Templating\Helper; | ||
|
||
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||
use Symfony\Component\Templating\Helper\Helper; | ||
|
||
/** | ||
* LogoutUrlHelper provides generator functions for the logout URL. | ||
* | ||
* @author Jeremy Mikola <jmikola@gmail.com> | ||
*/ | ||
class LogoutUrlHelper extends Helper | ||
{ | ||
private $listeners; | ||
private $request; | ||
private $router; | ||
|
||
/** | ||
* Constructor. | ||
* | ||
* @param Request $request A request instance | ||
* @param UrlGeneratorInterface $router A Router instance | ||
*/ | ||
public function __construct(Request $request, UrlGeneratorInterface $router) | ||
{ | ||
$this->request = $request; | ||
$this->router = $router; | ||
$this->listeners = array(); | ||
} | ||
|
||
/** | ||
* Registers a firewall's LogoutListener, allowing its URL to be generated. | ||
* | ||
* @param string $key The firewall key | ||
* @param string $logoutPath The path that starts the logout process | ||
* @param string $intention The intention for CSRF token generation | ||
* @param string $csrfParameter The CSRF token parameter name | ||
* @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance | ||
*/ | ||
public function registerListener($key, $logoutPath, $intention, $csrfParameter, CsrfProviderInterface $csrfProvider = null) | ||
{ | ||
$this->listeners[$key] = array($logoutPath, $intention, $csrfParameter, $csrfProvider); | ||
} | ||
|
||
/** | ||
* Generate the relative logout URL for the firewall. | ||
* | ||
* @param string $key The firewall key | ||
* @return string The relative logout URL | ||
*/ | ||
public function getLogoutPath($key) | ||
{ | ||
return $this->generateLogoutUrl($key, false); | ||
} | ||
|
||
/** | ||
* Generate the absolute logout URL for the firewall. | ||
* | ||
* @param string $key The firewall key | ||
* @return string The absolute logout URL | ||
*/ | ||
public function getLogoutUrl($key) | ||
{ | ||
return $this->generateLogoutUrl($key, true); | ||
} | ||
|
||
/** | ||
* Generate the logout URL for the firewall. | ||
* | ||
* @param string $key The firewall key | ||
* @param Boolean $absolute Whether to generate an absolute URL | ||
* @return string The logout URL | ||
* @throws InvalidArgumentException if no LogoutListener is registered for the key | ||
*/ | ||
private function generateLogoutUrl($key, $absolute) | ||
{ | ||
if (!array_key_exists($key, $this->listeners)) { | ||
throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key)); | ||
} | ||
|
||
list($logoutPath, $intention, $csrfParameter, $csrfProvider) = $this->listeners[$key]; | ||
|
||
$parameters = null !== $csrfProvider ? array($csrfParameter => $csrfProvider->generateCsrfToken($intention)) : array(); | ||
|
||
if ('/' === $logoutPath[0]) { | ||
$url = ($absolute ? $this->request->getUriForPath($logoutPath) : $this->request->getBasePath() . $logoutPath); | ||
|
||
if (!empty($parameters)) { | ||
$url .= '?' . http_build_query($parameters); | ||
} | ||
} else { | ||
$url = $this->router->generate($logoutPath, $parameters, $absolute); | ||
} | ||
|
||
return $url; | ||
} | ||
|
||
/** | ||
* Returns the canonical name of this helper. | ||
* | ||
* @return string The canonical name | ||
*/ | ||
public function getName() | ||
{ | ||
return 'logout_url'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony framework. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerAware; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Security\Core\SecurityContextInterface; | ||
use Symfony\Component\Security\Core\Exception\AccessDeniedException; | ||
|
||
class LoginController extends ContainerAware | ||
{ | ||
public function loginAction() | ||
{ | ||
$form = $this->container->get('form.factory')->create('user_login'); | ||
|
||
return $this->container->get('templating')->renderResponse('CsrfFormLoginBundle:Login:login.html.twig', array( | ||
'form' => $form->createView(), | ||
)); | ||
} | ||
|
||
public function afterLoginAction() | ||
{ | ||
return $this->container->get('templating')->renderResponse('CsrfFormLoginBundle:Login:after_login.html.twig'); | ||
} | ||
|
||
public function loginCheckAction() | ||
{ | ||
return new Response('', 400); | ||
} | ||
|
||
public function secureAction() | ||
{ | ||
throw new \Exception('Wrapper', 0, new \Exception('Another Wrapper', 0, new AccessDeniedException())); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony framework. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\Bundle; | ||
|
||
class CsrfFormLoginBundle extends Bundle | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony framework. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Form; | ||
|
||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\Form\FormBuilder; | ||
use Symfony\Component\Form\FormError; | ||
use Symfony\Component\Form\FormEvents; | ||
use Symfony\Component\Form\Event\FilterDataEvent; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\Security\Core\SecurityContextInterface; | ||
|
||
/** | ||
* Form type for use with the Security component's form-based authentication | ||
* listener. | ||
* | ||
* @author Henrik Bjornskov <henrik@bjrnskov.dk> | ||
* @author Jeremy Mikola <jmikola@gmail.com> | ||
*/ | ||
class UserLoginFormType extends AbstractType | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting approach. Is this maybe something we should ship as part of the security component/bundle so that people can re-use the existing markup tools for forms? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely. It would not be difficult to capitalize on this form type's service definition and inject the field names, intention, etc. The only question is how to allow the base form name to be configured, as the existing form login options would not allow for that. One idea would be instructing the user to configure all fields (username, password, target path and, optionally, CSRF) as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jmikola the Form component now supports empty root names so it can work with the default config of form_login. And AFAIK, you can configure the username field to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. I wasn't following the Form component changes when working on this in December. We'd likely leave the base name empty and allow the fields themselves to be configured. The annoying part about creating extra services for listener configurations is that they need to be namespaced with the firewall key, since there could be multiple form_login listeners in the application. But I don't see a way around that :) |
||
{ | ||
private $reqeust; | ||
|
||
/** | ||
* @param Request $request A request instance | ||
*/ | ||
public function __construct(Request $request) | ||
{ | ||
$this->request = $request; | ||
} | ||
|
||
/** | ||
* @see Symfony\Component\Form\AbstractType::buildForm() | ||
*/ | ||
public function buildForm(FormBuilder $builder, array $options) | ||
{ | ||
$builder | ||
->add('username', 'text') | ||
->add('password', 'password') | ||
->add('_target_path', 'hidden') | ||
; | ||
|
||
$request = $this->request; | ||
|
||
/* Note: since the Security component's form login listener intercepts | ||
* the POST request, this form will never really be bound to the | ||
* request; however, we can match the expected behavior by checking the | ||
* session for an authentication error and last username. | ||
*/ | ||
$builder->addEventListener(FormEvents::SET_DATA, function (FilterDataEvent $event) use ($request) { | ||
if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) { | ||
$error = $request->attributes->get(SecurityContextInterface::AUTHENTICATION_ERROR); | ||
} else { | ||
$error = $request->getSession()->get(SecurityContextInterface::AUTHENTICATION_ERROR); | ||
} | ||
|
||
if ($error) { | ||
$event->getForm()->addError(new FormError($error->getMessage())); | ||
} | ||
|
||
$event->setData(array_replace((array) $event->getData(), array( | ||
'username' => $request->getSession()->get(SecurityContextInterface::LAST_USERNAME), | ||
))); | ||
}); | ||
} | ||
|
||
/** | ||
* @see Symfony\Component\Form\AbstractType::getDefaultOptions() | ||
*/ | ||
public function getDefaultOptions(array $options) | ||
{ | ||
/* Note: the form's intention must correspond to that for the form login | ||
* listener in order for the CSRF token to validate successfully. | ||
*/ | ||
return array( | ||
'intention' => 'authenticate', | ||
); | ||
} | ||
|
||
/** | ||
* @see Symfony\Component\Form\FormTypeInterface::getName() | ||
*/ | ||
public function getName() | ||
{ | ||
return 'user_login'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding this to the existing security helper? It might also be nice to have for login (also not directly related to this PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered it, but since the main focus is for URL generation it didn't seem to jive with the role check method. The existing SecurityHelper class is concerned only with reading from the context, and it didn't seem proper to tack on request and router services for a feature that will li ED4F kely be used by only a minority of users.
I also wanted to demonstrate a model for having the Twig extension compose the templating helper.