Stateless CSRF protection -- "Synchronizer Token Pattern"
Hi there,
I'm implementing astateless CSRF protection. This is a pattern listed by Owasp and implemented by Angular using the X-XSRF-TOKEN.
To do so, I've been using three events,
-
onJWTCreated: I generate a random value and add it to the payload of the token. I'm currently using Symfony security token generator but let's say it's the string
oyo42for the sake of the example. -
onAuthenticationSuccessResponse: I'm using this to add the randomly generated value to the response; currently to the data but I could add a
Set-Cookieheader. -
onJWTDecoded: when the request method is POST (primary focus), PUT or DELETE, I'm making some verifications. The front-end is sending me the JWT as a cookie and the header
X-XSRF-TOKENcontaining theoyo42.
The problem is the onAuthenticationSuccessResponse does not have access to the payload of the token, only the JWT as a string and the user. I inspected the code, but I did not see any way of making it available,
My current workaround is to store the payload in my listener;
class JWTListener
{
/**
* @var array
*/
private $payloads = [];
// [...]
/**
* @param JWTCreatedEvent $event
*
* @return void
*/
public function onJWTCreated(JWTCreatedEvent $event)
{
$payload = $event->getData();
// CSRF protection.
$payload['csrf_token'] = 'oyo42';
$this->setPayload($this->getPayloadKey($event->getUser()), $payload);
// Set the data to the event;
$event->setData($payload);
return;
}
/**
* @param AuthenticationSuccessEvent $event
*/
public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event)
{
$data = $event->getData();
$data['csrf_token'] = $this->getPayload($this->getPayloadKey($event->getUser()))['csrf_token'];
$event->setData($data);
}
/**
* @param JWTDecodedEvent $event
*
* @return void
*/
public function onJWTDecoded(JWTDecodedEvent $event)
{
$request = $this->requestStack->getCurrentRequest();
$payload = $event->getPayload();
if (!in_array($request->getMethod(), ['POST', 'PUT', 'DELETE'])) {
return;
}
$csrfTokenHeaderNames = [
'CSRF-Token',
'X-XSRF-TOKEN', // Angular support.
];
$csrfToken = null;
foreach ($csrfTokenHeaderNames as $headerName) {
if ($request->headers->has($headerName)) {
$csrfToken = $request->headers->get($headerName);
}
}
if (!$csrfToken || ($csrfToken && (string)$csrfToken !== (string)$payload['csrf_token'])) {
$event->markAsInvalid();
}
return;
}
protected function setPayload($payloadKey, $payload)
{
return $this->payloads[$payloadKey] = $payload;
}
protected function getPayload($payloadKey)
{
return $this->payloads[$payloadKey];
}
protected function getPayloadKey($user)
{
return sha1($user->getId() . '@' . $user->getActiveChannel()->getId());
}
}
Working but not elegant at all. Forgive the one-liners too in the snippet above, its a demo/proof-of-concept.
Idea 1:
I feel that the create() method is doing two things; creating the token and encoding it; if there were two methods: create() returning an object and the encode() returning a string like today.
Do you feel it could be something interesting to implement in this bundle ?
Idea 2:
Another way I did not experimented would be this article, splitting the token in two cookies ({header}.{payload} and {signature}) and have a listener before the guard authenticator to concatenate the two which seems quite clever to me!
Thanks a lot, :bowing_man:
Hello !
You can decode the token in the onAuthenticationSuccessResponse listener by getting the JWTEncoder service and using its decode() method as you can see below:
class JWTListener
{
private $decoder;
public function __construct( JWTEncoderInterface $decoder )
{
$this->decoder = $decoder;
}
public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event)
{
$data = $event->getData();
$payload = $this->decoder->decode( $data[ 'token' ] );
}
}
Hope it helped ;)