From d13a6c4a6e1597ef6a9b6cfcdeb33f0d7ca8b158 Mon Sep 17 00:00:00 2001 From: hecht Date: Wed, 14 Nov 2018 21:32:14 +0100 Subject: [PATCH] added authenticator so authentication is done via HTTP Header --- config/orm/User.orm.yml | 14 +++-- config/packages/security.yaml | 12 +++- config/serializer/Model.User.yml | 2 +- src/Controller/HeroController.php | 2 +- src/DataFixtures/UserFixtures.php | 38 +++++++---- src/Entity/User.php | 66 ++++++++++++++++--- src/Security/TokenAuthenticator.php | 98 +++++++++++++++++++++++++++++ tests/ClientRequestBuilder.php | 12 ++++ 8 files changed, 218 insertions(+), 26 deletions(-) create mode 100644 src/Security/TokenAuthenticator.php create mode 100644 tests/ClientRequestBuilder.php diff --git a/config/orm/User.orm.yml b/config/orm/User.orm.yml index 2f0d804..a912cd5 100644 --- a/config/orm/User.orm.yml +++ b/config/orm/User.orm.yml @@ -7,14 +7,18 @@ App\Entity\User: type: integer generator: { strategy: AUTO } fields: - name: + username: type: string length: 32 unique: true - password: - type: string - length: 64 - + password: # the hashed password + type: string + apiToken: + type: string + unique: true + nullable: true + roles: + type: json oneToMany: heroes: targetEntity: Hero diff --git a/config/packages/security.yaml b/config/packages/security.yaml index fb4c593..1cf5f8f 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,4 +1,8 @@ security: + encoders: + App\Entity\User: + algorithm: argon2i + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: in_memory: { memory: ~ } @@ -6,9 +10,15 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + api: + pattern: ^/api/ + guard: + authenticators: + - App\Security\TokenAuthenticator + stateless: true main: anonymous: true - + # activate different ways to authenticate # http_basic: true diff --git a/config/serializer/Model.User.yml b/config/serializer/Model.User.yml index 132a444..6bed0cf 100644 --- a/config/serializer/Model.User.yml +++ b/config/serializer/Model.User.yml @@ -2,7 +2,7 @@ App\Entity\User: attributes: id: groups: ['Default'] - name: + username: groups: ['Default'] user: groups: ['HeroWithUser'] diff --git a/src/Controller/HeroController.php b/src/Controller/HeroController.php index 6f88f65..51e0754 100644 --- a/src/Controller/HeroController.php +++ b/src/Controller/HeroController.php @@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response; */ class HeroController extends FOSRestController { - protected $HeroRepository; + protected $heroRepository; public function __construct(HeroRepository $heroRepository) { diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index f62d672..f985c76 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -1,31 +1,47 @@ passwordEncoder = $passwordEncoder; + } + public function load(ObjectManager $manager) { $userAdmin = new User(); - $userAdmin->setName('admin'); - $userAdmin->setPassword('123456789'); + $userAdmin->setUsername('admin'); + $this->setPassword($userAdmin, '123456789'); + $userAdmin->setApiToken('ItsHammerTime!'); $manager->persist($userAdmin); - $userDummy = new User(); - $userDummy->setName('dummy'); - $userDummy->setPassword('1234'); - $manager->persist($userDummy); + $userDude = new User(); + $userDude->setUsername('dude'); + $this->setPassword($userDude, '1234'); + $userDude->setApiToken('ItsDuderzeit!'); + $manager->persist($userDude); $manager->flush(); - + $this->addReference(self::ADMIN_USER_REFERENCE, $userAdmin); - $this->addReference(self::DUMMY_USER_REFERENCE, $userDummy); + $this->addReference(self::DUDE_USER_REFERENCE, $userDude); + } + + private function setPassword(User& $user, string $plainPassword): void + { + $user->setPassword($this->passwordEncoder->encodePassword($user, $plainPassword)); } } diff --git a/src/Entity/User.php b/src/Entity/User.php index f2747d9..08e220c 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -4,15 +4,20 @@ namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Symfony\Component\Security\Core\User\UserInterface; -class User +class User implements UserInterface { private $id; - private $name; + private $username; private $password; + private $apiToken; + + private $roles = []; + private $heroes; public function __construct() @@ -24,15 +29,18 @@ class User { return $this->id; } - - public function getName(): ?string + + /** + * @see UserInterface + */ + public function getUsername(): ?string { - return $this->name; + return $this->username; } - public function setName(string $name): self + public function setUsername(string $username): self { - $this->name = $name; + $this->username = $username; return $this; } @@ -49,6 +57,30 @@ class User return $this; } + public function getApiToken(): ?string + { + return $this->apiToken; + } + + public function setApiToken(?string $apiToken): self + { + $this->apiToken = $apiToken; + + return $this; + } + + public function getRoles(): ?array + { + return $this->roles; + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + /** * @return Collection|Hero[] */ @@ -79,4 +111,24 @@ class User return $this; } + + /** + * @see UserInterface + */ + public function getSalt() + { + // not needed when using the "bcrypt" algorithm in security.yml + } + + /** + * @see UserInterface + */ + public function eraseCredentials() + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + $this->apiToken = null; + } + + } diff --git a/src/Security/TokenAuthenticator.php b/src/Security/TokenAuthenticator.php new file mode 100644 index 0000000..ed8eac7 --- /dev/null +++ b/src/Security/TokenAuthenticator.php @@ -0,0 +1,98 @@ +em = $em; + } + + /** + * Called on every request to decide if this authenticator should be + * used for the request. Returning false will cause this authenticator + * to be skipped. + */ + public function supports(Request $request) + { + return $request->headers->has('X-AUTH-TOKEN'); + } + + /** + * Called on every request. Return whatever credentials you want to + * be passed to getUser() as $credentials. + */ + public function getCredentials(Request $request) + { + return array( + 'token' => $request->headers->get('X-AUTH-TOKEN'), + ); + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + $apiToken = $credentials['token']; + + if (null === $apiToken) { + return; + } + + // if a User object, checkCredentials() is called + return $this->em->getRepository(User::class) + ->findOneBy(['apiToken' => $apiToken]); + } + + public function checkCredentials($credentials, UserInterface $user) + { + // check credentials - e.g. make sure the password is valid + // no credential check is needed in this case + + // return true to cause authentication success + return true; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + // on success, let the request continue + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + $message = strtr($exception->getMessageKey(), $exception->getMessageData()); + // or to translate this message + // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) + + // This should translated by FOSRestBundle! + throw new AccessDeniedHttpException($message); + } + + /** + * Called when authentication is needed, but it's not sent + */ + public function start(Request $request, AuthenticationException $authException = null) + { + throw new UnauthorizedHttpException('', 'Authentication Required'); + } + + public function supportsRememberMe() + { + return false; + } +} \ No newline at end of file diff --git a/tests/ClientRequestBuilder.php b/tests/ClientRequestBuilder.php new file mode 100644 index 0000000..ff64cf4 --- /dev/null +++ b/tests/ClientRequestBuilder.php @@ -0,0 +1,12 @@ +