How to Create a Custom Authentication System with Guard
Warning: You are browsing the documentation for Symfony 2.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
Whether you need to build a traditional login form, an API token authentication system or you need to integrate with some proprietary single-sign-on system, the Guard component can make it easy... and fun!
In this example, you'll build an API token authentication system and learn how to work with Guard.
Create a User and a User Provider
No matter how you authenticate, you need to create a User class that implements UserInterface
and configure a user provider. In this
example, users are stored in the database via Doctrine, and each user has an apiKey
property they use to access their account via the API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="`user`")
*/
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private $username;
/**
* @ORM\Column(type="string", unique=true)
*/
private $apiKey;
public function getUsername()
{
return $this->username;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function getPassword()
{
}
public function getSalt()
{
}
public function eraseCredentials()
{
}
// more getters/setters
}
Caution
In the example above, the table name is user
. This is a reserved SQL
keyword and must be quoted with backticks in Doctrine to avoid errors.
You might also change the table name (e.g. with app_users
) to solve
this issue.
Tip
This User doesn't have a password, but you can add a password
property if
you also want to allow this user to login with a password (e.g. via a login form).
Your User
class doesn't need to be stored in Doctrine: do whatever you need.
Next, make sure you've configured a "user provider" for the user:
1 2 3 4 5 6 7 8 9 10 11
# app/config/security.yml
security:
# ...
providers:
your_db_provider:
entity:
class: AppBundle:User
property: apiKey
# ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<!-- app/config/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
http://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<!-- ... -->
<provider name="your_db_provider">
<entity class="AppBundle:User" />
</provider>
<!-- ... -->
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// app/config/security.php
$container->loadFromExtension('security', array(
// ...
'providers' => array(
'your_db_provider' => array(
'entity' => array(
'class' => 'AppBundle:User',
),
),
),
// ...
));
That's it! Need more information about this step, see:
Step 1) Create the Authenticator Class
Suppose you have 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).
To create a custom authentication system, just create a class and make it implement GuardAuthenticatorInterface. Or, extend the simpler AbstractGuardAuthenticator. This requires you to implement seven methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
// src/AppBundle/Security/TokenAuthenticator.php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser(). Returning null will cause this authenticator
* to be skipped.
*/
public function getCredentials(Request $request)
{
if (!$token = $request->headers->get('X-AUTH-TOKEN')) {
// No token?
$token = null;
}
// What you return here will be passed to getUser() as $credentials
return array(
'token' => $token,
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$apiKey = $credentials['token'];
if (null === $apiKey) {
return;
}
// if a User object, checkCredentials() is called
return $userProvider->loadUserByUsername($apiKey);
}
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)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// 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: The Guard Authenticator Methods.
Step 2) Configure the Authenticator
To finish this, register the class as a service:
1 2 3 4
# app/config/services.yml
services:
app.token_authenticator:
class: AppBundle\Security\TokenAuthenticator
1 2 3 4
<!-- app/config/services.xml -->
<services>
<service id="app.token_authenticator" class="AppBundle\Security\TokenAuthenticator" />
</services>
1 2 3 4 5 6
// app/config/services.php
use AppBundle\Security\TokenAuthenticator;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$container->register('app.token_authenticator', TokenAuthenticator::class);
Finally, configure your firewalls
key in security.yml
to use this authenticator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# app/config/security.yml
security:
# ...
firewalls:
# ...
main:
anonymous: ~
logout: ~
guard:
authenticators:
- app.token_authenticator
# if you want, disable storing the user in the session
# stateless: true
# maybe other things, like form_login, remember_me, etc
# ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
<!-- app/config/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
http://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main"
pattern="^/"
anonymous="true"
>
<logout />
<guard>
<authenticator>app.token_authenticator</authenticator>
</guard>
<!-- ... -->
</firewall>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// app/config/security.php
// ..
$container->loadFromExtension('security', array(
'firewalls' => array(
'main' => array(
'pattern' => '^/',
'anonymous' => true,
'logout' => true,
'guard' => array(
'authenticators' => array(
'app.token_authenticator'
),
),
// ...
),
),
));
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:
1 2 3 4 5 6 7 8 9 10 11
# 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.
The Guard Authenticator Methods
Each authenticator needs the following methods:
- getCredentials(Request $request)
-
This will be called on every request and your job is to read the token (or
whatever your "authentication" information is) from the request and return it.
If you return
null
, the rest of the authentication process is skipped. Otherwise,getUser()
will be called and the return value is passed as the first argument. - getUser($credentials, UserProviderInterface $userProvider)
-
If
getCredentials()
returns a non-null value, then this method is called and its return value is passed here as the$credentials
argument. Your job is to return an object that implementsUserInterface
. If you do, thencheckCredentials()
will be called. If you returnnull
(or throw an AuthenticationException) 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, returntrue
. If you return anything else (or throw an AuthenticationException), authentication will fail. - onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
-
This is called after successful authentication and your job is to either
return a 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 returnnull
. - onAuthenticationFailure(Request $request, AuthenticationException $exception)
-
This is called if authentication fails. Your job
is to return the 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 (i.e. you returned
null
fromgetCredentials()
). Your job is to return a 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 GuardAuthenticatorInterface instead of extending the AbstractGuardAuthenticator class, you have to implement this method. It will be called after a successful authentication to create and return the token for the user, who was supplied as the first argument.
The picture below shows how Symfony calls Guard Authenticator methods:
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 easily return a custom message by throwing a
CustomUserMessageAuthenticationException.
You can throw this from getCredentials()
, getUser()
or checkCredentials()
to cause a failure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/AppBundle/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:
1 2
curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
# {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"}
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 getCredentials()
method only returns a non-null value when
you actually need to authenticate the user. Why? Because, when getCredentials()
returns a non-null value, 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:
1 2 3 4 5 6 7 8 9 10 11 12
public function getCredentials(Request $request)
{
// GOOD behavior: only authenticate on a specific route
if ($request->attributes->get('_route') !== 'login_route' || !$request->isMethod('POST')) {
return null;
}
// e.g. your login system authenticates by the user's IP address
// BAD behavior: authentication will now execute on every request
// even if the user is already authenticated (due to the session)
return array('ip' => $request->getClientIp());
}
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:
- If you do not need authentication to be stored in the session, set
stateless: true
under your firewall. - Update your authenticator to avoid authentication if the user is already authenticated:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// src/Security/MyIpAuthenticator.php
// ...
+ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class MyIpAuthenticator
{
+ private $tokenStorage;
+ public function __construct(TokenStorageInterface $tokenStorage)
+ {
+ $this->tokenStorage = $tokenStorage;
+ }
public function getCredentials(Request $request)
{
+ // if there is already an authenticated user (likely due to the session)
+ // then return null and skip authentication: there is no need.
+ $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
+ if (is_object($user)) {
+ return null;
+ }
return array('ip' => $request->getClientIp());
}
}
You'll also need to update your service configuration to pass the token storage:
1 2 3 4 5
# app/config/services.yml
services:
app.token_authenticator:
class: AppBundle\Security\TokenAuthenticator
arguments: ['@security.token_storage']
1 2 3 4 5 6
<!-- app/config/services.xml -->
<services>
<service id="app.token_authenticator" class="AppBundle\Security\TokenAuthenticator">
<argument type="service" id="security.token_storage" />
</service>
</services>
1 2 3 4 5 6 7
// app/config/services.php
use AppBundle\Security\TokenAuthenticator;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$container->register('app.token_authenticator', TokenAuthenticator::class)
->addArgument(new Reference('security.token_storage'));
Frequently Asked Questions
- Can I have Multiple Authenticators?
-
Yes! But when you do, you'll need choose just 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 example, suppose you have anapp.form_login_authenticator
that handles a traditional form login. When a user accesses a protected page anonymously, you want to use thestart()
method from the form authenticator and redirect them to the login page (instead of returning a JSON response):1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# app/config/security.yml security: # ... firewalls: # ... main: anonymous: ~ logout: ~ guard: entry_point: app.form_login_authenticator authenticators: - app.token_authenticator - app.form_login_authenticator # if you want, disable storing the user in the session # stateless: true # maybe other things, like form_login, remember_me, etc # ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
<!-- app/config/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 http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <!-- ... --> <firewall name="main" pattern="^/" anonymous="true" > <logout /> <guard entry-point="app.form_login_authenticator"> <authenticator>app.token_authenticator</authenticator> <authenticator>app.form_login_authenticator</authenticator> </guard> <!-- ... --> </firewall> </config> </srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// app/config/security.php // .. $container->loadFromExtension('security', array( 'firewalls' => array( 'main' => array( 'pattern' => '^/', 'anonymous' => true, 'logout' => true, 'guard' => array( 'entry_point' => 'app.form_login_authenticator', 'authenticators' => array( 'app.token_authenticator', 'app.form_login_authenticator', ), ), // ... ), ), ));
- 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 simply gives you a
User
object and some routes and controllers to help with login, registration, forgot password, etc. When you use FOSUserBundle, you typically useform_login
to actually authenticate the user. You can continue doing that (see previous question) or use theUser
object from FOSUserBundle and create your own authenticator(s) (just like in this article).