mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-24 00:32:14 +01:00
513 lines
19 KiB
ReStructuredText
513 lines
19 KiB
ReStructuredText
.. index::
|
|
single: Security; Custom Authentication
|
|
|
|
Custom Authentication System with Guard (API Token Example)
|
|
===========================================================
|
|
|
|
Guard authentication can be used to:
|
|
|
|
* :doc:`Build a Login Form </security/form_login_setup>`
|
|
* Create an API token authentication system (see below)
|
|
* `Social Authentication`_ (or use `HWIOAuthBundle`_ for a robust non-Guard solution)
|
|
* Integrate with some proprietary single-sign-on system
|
|
|
|
and many more. In this example, we'll build an API token authentication
|
|
system, so we can learn more about Guard in detail.
|
|
|
|
Step 1) Prepare your User Class
|
|
-------------------------------
|
|
|
|
Suppose you want to build an API where your clients will send an ``X-AUTH-TOKEN`` header
|
|
on each request with their API token. Your job is to read this and find the associated
|
|
user (if any).
|
|
|
|
First, make sure you've followed the main :doc:`Security Guide </security>` to
|
|
create your ``User`` class. Then add an ``apiToken`` property directly to
|
|
your ``User`` class (the ``make:entity`` command is a good way to do this):
|
|
|
|
.. code-block:: diff
|
|
|
|
// src/Entity/User.php
|
|
// ...
|
|
|
|
class User implements UserInterface
|
|
{
|
|
// ...
|
|
|
|
+ /**
|
|
+ * @ORM\Column(type="string", unique=true, nullable=true)
|
|
+ */
|
|
+ private $apiToken;
|
|
|
|
// the getter and setter methods
|
|
}
|
|
|
|
Don't forget to generate and run the migration:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ php bin/console make:migration
|
|
$ php bin/console doctrine:migrations:migrate
|
|
|
|
Step 2) Create the Authenticator Class
|
|
--------------------------------------
|
|
|
|
To create a custom authentication system, create a class and make it implement
|
|
:class:`Symfony\\Component\\Security\\Guard\\AuthenticatorInterface`. Or, extend
|
|
the simpler :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator`.
|
|
|
|
This requires you to implement several methods::
|
|
|
|
// src/Security/TokenAuthenticator.php
|
|
namespace App\Security;
|
|
|
|
use App\Entity\User;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
|
use Symfony\Component\Security\Core\User\UserInterface;
|
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
|
|
|
|
class TokenAuthenticator extends AbstractGuardAuthenticator
|
|
{
|
|
private $em;
|
|
|
|
public function __construct(EntityManagerInterface $em)
|
|
{
|
|
$this->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 $request->headers->get('X-AUTH-TOKEN');
|
|
}
|
|
|
|
public function getUser($credentials, UserProviderInterface $userProvider)
|
|
{
|
|
if (null === $credentials) {
|
|
// The token header was empty, authentication fails with HTTP Status
|
|
// Code 401 "Unauthorized"
|
|
return null;
|
|
}
|
|
|
|
// if a User is returned, checkCredentials() is called
|
|
return $this->em->getRepository(User::class)
|
|
->findOneBy(['apiToken' => $credentials])
|
|
;
|
|
}
|
|
|
|
public function checkCredentials($credentials, UserInterface $user)
|
|
{
|
|
// Check credentials - e.g. make sure the password is valid.
|
|
// In case of an API token, no credential check is needed.
|
|
|
|
// 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)
|
|
{
|
|
$data = [
|
|
// you may want to customize or obfuscate the message first
|
|
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
|
|
|
|
// or to translate this message
|
|
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
|
|
];
|
|
|
|
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
|
|
}
|
|
|
|
/**
|
|
* Called when authentication is needed, but it's not sent
|
|
*/
|
|
public function start(Request $request, AuthenticationException $authException = null)
|
|
{
|
|
$data = [
|
|
// you might translate this message
|
|
'message' => 'Authentication Required'
|
|
];
|
|
|
|
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
|
|
}
|
|
|
|
public function supportsRememberMe()
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Nice work! Each method is explained below: :ref:`The Guard Authenticator Methods<guard-auth-methods>`.
|
|
|
|
Step 3) Configure the Authenticator
|
|
-----------------------------------
|
|
|
|
To finish this, make sure your authenticator is registered as a service. If you're
|
|
using the :ref:`default services.yaml configuration <service-container-services-load-example>`,
|
|
that happens automatically.
|
|
|
|
Finally, configure your ``firewalls`` key in ``security.yaml`` to use this authenticator:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/packages/security.yaml
|
|
security:
|
|
# ...
|
|
|
|
firewalls:
|
|
# ...
|
|
|
|
main:
|
|
anonymous: lazy
|
|
logout: ~
|
|
|
|
guard:
|
|
authenticators:
|
|
- App\Security\TokenAuthenticator
|
|
|
|
# if you want, disable storing the user in the session
|
|
# stateless: true
|
|
|
|
# ...
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/packages/security.xml -->
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<srv:container xmlns="http://symfony.com/schema/dic/security"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:srv="http://symfony.com/schema/dic/services"
|
|
xsi:schemaLocation="http://symfony.com/schema/dic/services
|
|
https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
<config>
|
|
<!-- ... -->
|
|
|
|
<!-- if you want, disable storing the user in the session
|
|
add 'stateless="true"' to the firewall -->
|
|
<firewall name="main" pattern="^/">
|
|
<anonymous lazy="true"/>
|
|
<logout/>
|
|
|
|
<guard>
|
|
<authenticator>App\Security\TokenAuthenticator</authenticator>
|
|
</guard>
|
|
|
|
<!-- ... -->
|
|
</firewall>
|
|
</config>
|
|
</srv:container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/packages/security.php
|
|
|
|
// ...
|
|
use App\Security\TokenAuthenticator;
|
|
|
|
$container->loadFromExtension('security', [
|
|
'firewalls' => [
|
|
'main' => [
|
|
'pattern' => '^/',
|
|
'anonymous' => 'lazy',
|
|
'logout' => true,
|
|
'guard' => [
|
|
'authenticators' => [
|
|
TokenAuthenticator::class,
|
|
],
|
|
],
|
|
// if you want, disable storing the user in the session
|
|
// 'stateless' => true,
|
|
// ...
|
|
],
|
|
],
|
|
]);
|
|
|
|
You did it! You now have a fully-working API token authentication system. If your
|
|
homepage required ``ROLE_USER``, then you could test it under different conditions:
|
|
|
|
.. code-block:: terminal
|
|
|
|
# test with no token
|
|
curl http://localhost:8000/
|
|
# {"message":"Authentication Required"}
|
|
|
|
# test with a bad token
|
|
curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
|
|
# {"message":"Username could not be found."}
|
|
|
|
# test with a working token
|
|
curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
|
|
# the homepage controller is executed: the page loads normally
|
|
|
|
Now, learn more about what each method does.
|
|
|
|
.. _guard-auth-methods:
|
|
|
|
The Guard Authenticator Methods
|
|
-------------------------------
|
|
|
|
Each authenticator needs the following methods:
|
|
|
|
**supports(Request $request)**
|
|
This is called on *every* request and your job is to decide if the
|
|
authenticator should be used for this request (return ``true``) or if it
|
|
should be skipped (return ``false``).
|
|
|
|
**getCredentials(Request $request)**
|
|
Your job is to read the token (or whatever your "authentication" information is)
|
|
from the request and return it. These credentials are passed to ``getUser()``.
|
|
|
|
**getUser($credentials, UserProviderInterface $userProvider)**
|
|
The ``$credentials`` argument is the value returned by ``getCredentials()``.
|
|
Your job is to return an object that implements ``UserInterface``. If you do,
|
|
then ``checkCredentials()`` will be called. If you return ``null`` (or throw
|
|
an :ref:`AuthenticationException <guard-customize-error>`) authentication
|
|
will fail.
|
|
|
|
**checkCredentials($credentials, UserInterface $user)**
|
|
If ``getUser()`` returns a User object, this method is called. Your job is to
|
|
verify if the credentials are correct. For a login form, this is where you would
|
|
check that the password is correct for the user. To pass authentication, return
|
|
``true``. If you return ``false``
|
|
(or throw an :ref:`AuthenticationException <guard-customize-error>`),
|
|
authentication will fail.
|
|
|
|
**onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)**
|
|
This is called after successful authentication and your job is to either
|
|
return a :class:`Symfony\\Component\\HttpFoundation\\Response` object
|
|
that will be sent to the client or ``null`` to continue the request
|
|
(e.g. allow the route/controller to be called like normal). Since this
|
|
is an API where each request authenticates itself, you want to return
|
|
``null``.
|
|
|
|
**onAuthenticationFailure(Request $request, AuthenticationException $exception)**
|
|
This is called if authentication fails. Your job
|
|
is to return the :class:`Symfony\\Component\\HttpFoundation\\Response`
|
|
object that should be sent to the client. The ``$exception`` will tell you
|
|
*what* went wrong during authentication.
|
|
|
|
**start(Request $request, AuthenticationException $authException = null)**
|
|
This is called if the client accesses a URI/resource that requires authentication,
|
|
but no authentication details were sent. Your job is to return a
|
|
:class:`Symfony\\Component\\HttpFoundation\\Response` object that helps
|
|
the user authenticate (e.g. a 401 response that says "token is missing!").
|
|
|
|
**supportsRememberMe()**
|
|
If you want to support "remember me" functionality, return ``true`` from this method.
|
|
You will still need to activate ``remember_me`` under your firewall for it to work.
|
|
Since this is a stateless API, you do not want to support "remember me"
|
|
functionality in this example.
|
|
|
|
**createAuthenticatedToken(UserInterface $user, string $providerKey)**
|
|
If you are implementing the :class:`Symfony\\Component\\Security\\Guard\\AuthenticatorInterface`
|
|
instead of extending the :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator`
|
|
class, you have to implement this method. It will be called
|
|
after a successful authentication to create and return the token (a
|
|
class implementing :class:`Symfony\\Component\\Security\\Guard\\Token\\GuardTokenInterface`)
|
|
for the user, who was supplied as the first argument.
|
|
|
|
The picture below shows how Symfony calls Guard Authenticator methods:
|
|
|
|
.. raw:: html
|
|
|
|
<object data="../_images/security/authentication-guard-methods.svg" type="image/svg+xml"></object>
|
|
|
|
.. _guard-customize-error:
|
|
|
|
Customizing Error Messages
|
|
--------------------------
|
|
|
|
When ``onAuthenticationFailure()`` is called, it is passed an ``AuthenticationException``
|
|
that describes *how* authentication failed via its ``$exception->getMessageKey()`` (and
|
|
``$exception->getMessageData()``) method. The message will be different based on *where*
|
|
authentication fails (i.e. ``getUser()`` versus ``checkCredentials()``).
|
|
|
|
But, you can also return a custom message by throwing a
|
|
:class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`.
|
|
You can throw this from ``getCredentials()``, ``getUser()`` or ``checkCredentials()``
|
|
to cause a failure::
|
|
|
|
// src/Security/TokenAuthenticator.php
|
|
// ...
|
|
|
|
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
|
|
|
class TokenAuthenticator extends AbstractGuardAuthenticator
|
|
{
|
|
// ...
|
|
|
|
public function getCredentials(Request $request)
|
|
{
|
|
// ...
|
|
|
|
if ($token == 'ILuvAPIs') {
|
|
throw new CustomUserMessageAuthenticationException(
|
|
'ILuvAPIs is not a real API key: it\'s just a silly phrase'
|
|
);
|
|
}
|
|
|
|
// ...
|
|
}
|
|
|
|
// ...
|
|
}
|
|
|
|
In this case, since "ILuvAPIs" is a ridiculous API key, you could include an easter
|
|
egg to return a custom message if someone tries this:
|
|
|
|
.. code-block:: terminal
|
|
|
|
curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
|
|
# {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"}
|
|
|
|
.. _guard-manual-auth:
|
|
|
|
Manually Authenticating a User
|
|
------------------------------
|
|
|
|
Sometimes you might want to manually authenticate a user - like after the user
|
|
completes registration. To do that, use your authenticator and a service called
|
|
``GuardAuthenticatorHandler``::
|
|
|
|
// src/Controller/RegistrationController.php
|
|
// ...
|
|
|
|
use App\Security\LoginFormAuthenticator;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
|
|
|
|
class RegistrationController extends AbstractController
|
|
{
|
|
public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request)
|
|
{
|
|
// ...
|
|
|
|
// after validating the user and saving them to the database
|
|
// authenticate the user and use onAuthenticationSuccess on the authenticator
|
|
return $guardHandler->authenticateUserAndHandleSuccess(
|
|
$user, // the User object you just created
|
|
$request,
|
|
$authenticator, // authenticator whose onAuthenticationSuccess you want to use
|
|
'main' // the name of your firewall in security.yaml
|
|
);
|
|
}
|
|
}
|
|
|
|
Avoid Authenticating the Browser on Every Request
|
|
-------------------------------------------------
|
|
|
|
If you create a Guard login system that's used by a browser and you're experiencing
|
|
problems with your session or CSRF tokens, the cause could be bad behavior by your
|
|
authenticator. When a Guard authenticator is meant to be used by a browser, you
|
|
should *not* authenticate the user on *every* request. In other words, you need to
|
|
make sure the ``supports()`` method *only* returns ``true`` when
|
|
you actually *need* to authenticate the user. Why? Because, when ``supports()``
|
|
returns true (and authentication is ultimately successful), for security purposes,
|
|
the user's session is "migrated" to a new session id.
|
|
|
|
This is an edge-case, and unless you're having session or CSRF token issues, you
|
|
can ignore this. Here is an example of good and bad behavior::
|
|
|
|
public function supports(Request $request)
|
|
{
|
|
// GOOD behavior: only authenticate (i.e. return true) on a specific route
|
|
return 'login_route' === $request->attributes->get('_route') && $request->isMethod('POST');
|
|
|
|
// e.g. your login system authenticates by the user's IP address
|
|
// BAD behavior: So, you decide to *always* return true so that
|
|
// you can check the user's IP address on every request
|
|
return true;
|
|
}
|
|
|
|
The problem occurs when your browser-based authenticator tries to authenticate
|
|
the user on *every* request - like in the IP address-based example above. There
|
|
are two possible fixes:
|
|
|
|
1. If you do *not* need authentication to be stored in the session, set
|
|
``stateless: true`` under your firewall.
|
|
2. Update your authenticator to avoid authentication if the user is already
|
|
authenticated:
|
|
|
|
.. code-block:: diff
|
|
|
|
// src/Security/MyIpAuthenticator.php
|
|
// ...
|
|
|
|
+ use Symfony\Component\Security\Core\Security;
|
|
|
|
class MyIpAuthenticator
|
|
{
|
|
+ private $security;
|
|
|
|
+ public function __construct(Security $security)
|
|
+ {
|
|
+ $this->security = $security;
|
|
+ }
|
|
|
|
public function supports(Request $request)
|
|
{
|
|
+ // if there is already an authenticated user (likely due to the session)
|
|
+ // then return false and skip authentication: there is no need.
|
|
+ if ($this->security->getUser()) {
|
|
+ return false;
|
|
+ }
|
|
|
|
+ // the user is not logged in, so the authenticator should continue
|
|
+ return true;
|
|
}
|
|
}
|
|
|
|
If you use autowiring, the ``Security`` service will automatically be passed to
|
|
your authenticator.
|
|
|
|
Frequently Asked Questions
|
|
--------------------------
|
|
|
|
**Can I have Multiple Authenticators?**
|
|
Yes! But when you do, you'll need to choose only *one* authenticator to be your
|
|
"entry_point". This means you'll need to choose *which* authenticator's ``start()``
|
|
method should be called when an anonymous user tries to access a protected resource.
|
|
For more details, see :doc:`/security/multiple_guard_authenticators`.
|
|
|
|
**Can I use this with form_login?**
|
|
Yes! ``form_login`` is *one* way to authenticate a user, so you could use
|
|
it *and* then add one or more authenticators. Using a guard authenticator doesn't
|
|
collide with other ways to authenticate.
|
|
|
|
**Can I use this with FOSUserBundle?**
|
|
Yes! Actually, FOSUserBundle doesn't handle security: it only gives you a
|
|
``User`` object and some routes and controllers to help with login, registration,
|
|
forgot password, etc. When you use FOSUserBundle, you typically use ``form_login``
|
|
to actually authenticate the user. You can continue doing that (see previous
|
|
question) or use the ``User`` object from FOSUserBundle and create your own
|
|
authenticator(s) (like in this article).
|
|
|
|
.. _`Social Authentication`: https://github.com/knpuniversity/oauth2-client-bundle#authenticating-with-guard
|
|
.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle
|