Last active
August 6, 2024 09:05
-
-
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
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 | |
// 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(), | |
]; | |
} | |
} |
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
{ | |
"require": { | |
"symfony/symfony": "~2.7.4", | |
"namshi/jose": "~6.0", | |
"knpuniversity/guard-bundle": "~0.3" | |
} | |
} |
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
# ... | |
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%] |
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 | |
// 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; | |
} | |
} |
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 | |
// src/AppBundle/Exception/InvalidJWTException.php | |
namespace AppBundle\Exception; | |
class InvalidJWTException extends \UnexpectedValueException | |
{ | |
} |
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 | |
// 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; | |
} | |
} |
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 | |
// 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 | |
} | |
} |
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
login: | |
path: /login | |
defaults: { _controller: AppBundle:User:login } |
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
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 |
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 | |
// 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]); | |
} | |
} |
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 | |
// 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(); | |
} | |
} | |
} |
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
@kbond
UsernamePasswordGuardAuthenticator::checkCredentials
expected a bool return.