Skip to content

Instantly share code, notes, and snippets.

@kbond
Last active August 6, 2024 09:05
Show Gist options
  • Save kbond/7904cee7c968e6fc6de3 to your computer and use it in GitHub Desktop.
Save kbond/7904cee7c968e6fc6de3 to your computer and use it in GitHub Desktop.
JWT Authentication With Symfony Guard. POST username/password to /login to receive token, /api* requests require a valid token
<?php
// app/AppKernel.php
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel;
class AppKernel extends Kernel
{
public function registerBundles()
{
return = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new KnpU\GuardBundle\KnpUGuardBundle(),
new AppBundle\AppBundle(),
];
}
}
{
"require": {
"symfony/symfony": "~2.7.4",
"namshi/jose": "~6.0",
"knpuniversity/guard-bundle": "~0.3"
}
}
# ...
services:
username_password_guard_authenticator:
class: AppBundle\Security\UsernamePasswordGuardAuthenticator
arguments: [@security.password_encoder]
public: false
jwt_guard_authenticator:
class: AppBundle\Security\JWTGuardAuthenticator
arguments: [@jwt_coder]
public: false
jwt_coder:
class: AppBundle\Service\JWTCoder
arguments: [%secret%]
<?php
// src/AppBundle/Security/GuardAuthenticator.php
namespace AppBundle\Security;
use KnpU\Guard\AbstractGuardAuthenticator;
use KnpU\Guard\Exception\CustomAuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* @author Kevin Bond <[email protected]>
*/
abstract class GuardAuthenticator extends AbstractGuardAuthenticator
{
/**
* NOTE: I chose to throw an HTTP Exception here to let the response be rendered elsewhere -
* separation of concerns and all... You could always return a JsonResponse here.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$message = 'Invalid Credentials';
if ($exception instanceof CustomAuthenticationException) {
$message = $exception->getMessageKey();
}
throw new HttpException(401, $message);
}
/**
* {@inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
// noop
}
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// noop
}
/**
* {@inheritdoc}
*/
public function supportsRememberMe()
{
return false;
}
}
<?php
// src/AppBundle/Exception/InvalidJWTException.php
namespace AppBundle\Exception;
class InvalidJWTException extends \UnexpectedValueException
{
}
<?php
// src/AppBundle/Service/JWTCoder.php
namespace AppBundle\Service;
use Namshi\JOSE\JWS;
use AppBundle\Exception\InvalidJWTException;
/**
* @author Kevin Bond <[email protected]>
*/
class JWTCoder
{
const ALG = 'HS256';
private $key;
public function __construct($key)
{
$this->key = $key;
}
/**
* @param array $payload
* @param int $ttl
*
* @return string
*/
public function encode(array $payload, $ttl = 86400)
{
$payload['iat'] = time();
$payload['exp'] = time() + $ttl;
$jws = new JWS([
'typ' => 'JWS',
'alg' => self::ALG,
]);
$jws->setPayload($payload);
$jws->sign($this->key);
return $jws->getTokenString();
}
/**
* @param string $token
*
* @return array
*
* @throws InvalidJWTException
*/
public function decode($token)
{
$jws = JWS::load($token);
if (!$jws->verify($this->key, self::ALG)) {
throw new InvalidJWTException('Invalid JWT');
}
if ($this->isExpired($payload = $jws->getPayload())) {
throw new InvalidJWTException('Expired JWT');
}
return $payload;
}
/**
* @param array $payload
*
* @return bool
*/
private function isExpired($payload)
{
if (isset($payload['exp']) && is_numeric($payload['exp'])) {
return (time() - $payload['exp']) > 0;
}
return false;
}
}
<?php
// src/AppBundle/Security/JWTGuardAuthenticator.php
namespace AppBundle\Security;
use KnpU\Guard\Exception\CustomAuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use AppBundle\Exception\InvalidJWTException;
use AppBundle\Service\JWTCoder;
/**
* @author Kevin Bond <[email protected]>
*/
final class JWTGuardAuthenticator extends GuardAuthenticator
{
private $jwtCoder;
public function __construct(JWTCoder $jwtCoder)
{
$this->jwtCoder = $jwtCoder;
}
/**
* {@inheritdoc}
*/
public function getCredentials(Request $request)
{
if (!$request->headers->has('Authorization')) {
throw CustomAuthenticationException::createWithSafeMessage('Missing Authorization Header');
}
$headerParts = explode(' ', $request->headers->get('Authorization'));
if (!(count($headerParts) === 2 && $headerParts[0] === 'Bearer')) {
throw CustomAuthenticationException::createWithSafeMessage('Malformed Authorization Header');
}
return $headerParts[1];
}
/**
* {@inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
try {
$payload = $this->jwtCoder->decode($credentials);
} catch (InvalidJWTException $e) {
throw CustomAuthenticationException::createWithSafeMessage($e->getMessage());
} catch (\Exception $e) {
throw CustomAuthenticationException::createWithSafeMessage('Malformed JWT');
}
if (!isset($payload['username'])) {
throw CustomAuthenticationException::createWithSafeMessage('Invalid JWT');
}
return $userProvider->loadUserByUsername($payload['username']);
}
/**
* {@inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
// noop
}
}
login:
path: /login
defaults: { _controller: AppBundle:User:login }
security:
encoders:
Symfony\Component\Security\Core\User\UserInterface: plaintext # change for real app
providers:
in_memory: # change for real app
memory:
users:
user: { password: userpass }
firewalls:
login:
pattern: ^/login$
stateless: true
knpu_guard:
authenticators:
- username_password_guard_authenticator
api:
pattern: ^/api
stateless: true
knpu_guard:
authenticators:
- jwt_guard_authenticator
<?php
// src/AppBundle/Controller/UserController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
class UserController extends Controller
{
/**
* NOTE: I don't return a response in the UsernamePasswordGuardAuthenticator
* because I wanted a controller to do the rendering. You could always
* return a JsonResponse in UsernamePasswordGuardAuthenticator::onAuthenticationSuccess().
* If you do that, this class/method is no longer required.
*/
public function loginAction()
{
$token = $this->get('jwt_coder')->encode([
'username' => $this->getUser()->getUsername()
]);
return new JsonResponse(['token' => $token]);
}
}
<?php
// src/AppBundle/Security/UsernamePasswordGuardAuthenticator.php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* @author Kevin Bond <[email protected]>
*/
final class UsernamePasswordGuardAuthenticator extends GuardAuthenticator
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
/**
* {@inheritdoc}
*/
public function getCredentials(Request $request)
{
if (!$request->isMethod('POST')) {
throw new MethodNotAllowedHttpException(['POST']);
}
return [
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
];
}
/**
* {@inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUsername($credentials['username']);
}
/**
* {@inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
$plainPassword = $credentials['password'];
if (!$this->passwordEncoder->isPasswordValid($user, $plainPassword)) {
throw new BadCredentialsException();
}
}
}
@hugohenrique
Copy link

@kbond UsernamePasswordGuardAuthenticator::checkCredentials expected a bool return.

public function checkCredentials($credentials, UserInterface $user)
{
    $isValid = $this->passwordEncoder->isPasswordValid($user, $credentials['password']);

    if (!$isValid) {
        throw new BadCredentialsException();
    }

    return true;
}

@pengend
Copy link

pengend commented Aug 19, 2016

Is there a vulnerability here with a modified payload ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment