@@ -6,7 +6,7 @@ How to Authenticate Users with API Keys
66
77Nowadays, it's quite usual to authenticate the user via an API key (when developing
88a web service for instance). The API key is provided for every request and is
9- passed as a query string parameter or via a HTTP header.
9+ passed as a query string parameter or via an HTTP header.
1010
1111The API Key Authenticator
1212-------------------------
@@ -16,7 +16,11 @@ The API Key Authenticator
1616
1717Authenticating a user based on the Request information should be done via a
1818pre-authentication mechanism. The :class: `Symfony\\ Component\\ Security\\ Core\\ Authentication\\ SimplePreAuthenticatorInterface `
19- interface allows to implement such a scheme really easily::
19+ allows you to implement such a scheme really easily.
20+
21+ Your exact situation may differ, but in this example, a token is read
22+ from an ``apikey `` query parameter, the proper username is loaded from that
23+ value and then a User object is created::
2024
2125 // src/Acme/HelloBundle/Security/ApiKeyAuthenticator.php
2226 namespace Acme\HelloBundle\Security;
@@ -26,7 +30,6 @@ interface allows to implement such a scheme really easily::
2630 use Symfony\Component\Security\Core\Exception\AuthenticationException;
2731 use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
2832 use Symfony\Component\HttpFoundation\Request;
29- use Symfony\Component\Security\Core\User\User;
3033 use Symfony\Component\Security\Core\User\UserProviderInterface;
3134 use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
3235 use Symfony\Component\Security\Core\Exception\BadCredentialsException;
@@ -55,22 +58,20 @@ interface allows to implement such a scheme really easily::
5558
5659 public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
5760 {
58- $apikey = $token->getCredentials();
59- if (!$this->userProvider->getUsernameForApiKey($apikey)) {
61+ $apiKey = $token->getCredentials();
62+ $username = $this->userProvider->getUsernameForApiKey($apiKey)
63+
64+ if (!$username) {
6065 throw new AuthenticationException(
61- sprintf('API Key "%s" does not exist.', $apikey )
66+ sprintf('API Key "%s" does not exist.', $apiKey )
6267 );
6368 }
6469
65- $user = new User(
66- $this->userProvider->getUsernameForApiKey($apikey),
67- $apikey,
68- array('ROLE_USER')
69- );
70+ $user = $this->userProvider->loadUserByUsername($username);
7071
7172 return new PreAuthenticatedToken(
7273 $user,
73- $apikey ,
74+ $apiKey ,
7475 $providerKey,
7576 $user->getRoles()
7677 );
@@ -82,31 +83,143 @@ interface allows to implement such a scheme really easily::
8283 }
8384 }
8485
85- ``$userProvider `` can be any user provider implementing an interface similar to
86- this::
86+ Once you've :ref: `configured <cookbook-security-api-key-config >` everything,
87+ you'll be able to authenticate by adding an apikey parameter to the query
88+ string, like ``http://example.com/admin/foo?apikey=37b51d194a7513e45b56f6524f2d51f2 ``.
89+
90+ The authentication process has several steps, and your implementation will
91+ probably differ:
92+
93+ 1. createToken
94+ ~~~~~~~~~~~~~~
95+
96+ Early in the request cycle, Symfony calls ``createToken() ``. Your job here
97+ is to create a token object that contains all of the information from the
98+ request that you need to authenticate the user (e.g. the ``apikey `` query
99+ parameter). If that information is missing, throwing a
100+ :class: `Symfony\\ Component\\ Security\\ Core\\ Exception\\ BadCredentialsException `
101+ will cause authentication to fail.
102+
103+ 2. supportsToken
104+ ~~~~~~~~~~~~~~~~
105+
106+ .. include :: _supportsToken.rst.inc
107+
108+ 3. authenticateToken
109+ ~~~~~~~~~~~~~~~~~~~~
87110
88- // src/Acme/HelloBundle/Security/ApiKeyUserProviderInterface.php
111+ If ``supportsToken() `` returns ``true ``, Symfony will now call ``authenticateToken() ``.
112+ One key part is the ``$userProvider ``, which is an external class that helps
113+ you load information about the user. You'll learn more about this next.
114+
115+ In this specific example, the following things happen in ``authenticateToken() ``:
116+
117+ #. First, you use the ``$userProvider `` to somehow look up the ``$username `` that
118+ corresponds to the ``$apiKey ``;
119+ #. Second, you use the ``$userProvider `` again to load or create a ``User ``
120+ object for the ``$username ``;
121+ #. Finally, you create an *authenticated token * (i.e. a token with at least one
122+ role) that has the proper roles and the User object attached to it.
123+
124+ The goal is ultimately to use the ``$apiKey `` to find or create a ``User ``
125+ object. *How * you do this (e.g. query a database) and the exact class for
126+ your ``User `` object may vary. Those differences will be most obvious in your
127+ user provider.
128+
129+ The User Provider
130+ ~~~~~~~~~~~~~~~~~
131+
132+ The ``$userProvider `` can be any user provider (see :doc: `/cookbook/security/custom_provider `).
133+ In this example, the ``$apiKey `` is used to somehow find the username for
134+ the user. This work is done in a ``getUsernameForApiKey() `` method, which
135+ is created entirely custom for this use-case (i.e. this isn't a method that's
136+ used by Symfony's core user provider system).
137+
138+ The ``$userProvider `` might look something like this::
139+
140+ // src/Acme/HelloBundle/Security/ApiKeyUserProvider.php
89141 namespace Acme\HelloBundle\Security;
90142
91143 use Symfony\Component\Security\Core\User\UserProviderInterface;
144+ use Symfony\Component\Security\Core\User\User;
145+ use Symfony\Component\Security\Core\User\UserInterface;
146+ use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
92147
93- interface ApiKeyUserProviderInterface extends UserProviderInterface
148+ class ApiKeyUserProvider extends UserProviderInterface
94149 {
95- public function getUsernameForApiKey($apikey);
150+ public function getUsernameForApiKey($apiKey)
151+ {
152+ // Look up the username based on the token in the database, via
153+ // an API call, or do something entirely different
154+ $username = ...;
155+
156+ return $username;
157+ }
158+
159+ public function loadUserByUsername($username)
160+ {
161+ return new User(
162+ $username,
163+ null,
164+ // the roles for the user - you may choose to determine
165+ // these dynamically somehow based on the user
166+ array('ROLE_USER')
167+ );
168+ }
169+
170+ public function refreshUser(UserInterface $user)
171+ {
172+ // this is used for storing authentication in the session
173+ // but in this example, the token is sent in each request,
174+ // so authentication can be stateless. Throwing this exception
175+ // is proper to make things stateless
176+ throw new UnsupportedUserException();
177+ }
178+
179+ public function supportsClass($class)
180+ {
181+ return 'Symfony\Component\Security\Core\User\User' === $class;
182+ }
96183 }
97184
98185.. note ::
99186
100187 Read the dedicated article to learn
101188 :doc: `how to create a custom user provider </cookbook/security/custom_provider >`.
102189
103- To access a resource protected by such an authenticator, you need to add an apikey
104- parameter to the query string, like in ``http://example.com/admin/foo?apikey=37b51d194a7513e45b56f6524f2d51f2 ``.
190+ The logic inside ``getUsernameForApiKey() `` is up to you. You may somehow transform
191+ the API key (e.g. ``37b51d ``) into a username (e.g. ``jondoe ``) by looking
192+ up some information in a "token" database table.
193+
194+ The same is true for ``loadUserByUsername() ``. In this example, Symfony's core
195+ :class: `Symfony\\ Component\\ Security\\ Core\\ User\\ User ` class is simply created.
196+ This makes sense if you don't need to store any extra information on your
197+ User object (e.g. ``firstName ``). But if you do, you may instead have your *own *
198+ user class which you create and populate here by querying a database. This
199+ would allow you to have custom data on the ``User `` object.
200+
201+ Finally, just make sure that ``supportsClass() `` returns ``true `` for User
202+ objects with the same class as whatever user you return in ``loadUserByUsername() ``.
203+ If your authentication is stateless like in this example (i.e. you expect
204+ the user to send the API key with every request and so you don't save the
205+ login to the session), then you can simply throw the ``UnsupportedUserException ``
206+ exception in ``refreshUser() ``.
207+
208+ .. note ::
209+
210+ If you *do * want to store authentication data in the session so that
211+ the key doesn't need to be sent on every request, see :ref: `cookbook-security-api-key-session `.
212+
213+ .. _cookbook-security-api-key-config :
105214
106215Configuration
107216-------------
108217
109- Configure your ``ApiKeyAuthenticator `` as a service:
218+ Once you have your ``ApiKeyAuthentication `` all setup, you need to register
219+ it as a service and use it in your security configuration (e.g. ``security.yml ``).
220+ First, register it as a service. This assumes that you have already setup
221+ your custom user provider as a service called ``your_api_key_user_provider ``
222+ (see :doc: `/cookbook/security/custom_provider `).
110223
111224.. configuration-block ::
112225
@@ -118,7 +231,7 @@ Configure your ``ApiKeyAuthenticator`` as a service:
118231
119232 apikey_authenticator :
120233 class : Acme\HelloBundle\Security\ApiKeyAuthenticator
121- arguments : [@your_api_key_user_provider]
234+ arguments : [" @your_api_key_user_provider" ]
122235
123236 .. code-block :: xml
124237
@@ -152,20 +265,23 @@ Configure your ``ApiKeyAuthenticator`` as a service:
152265 array(new Reference('your_api_key_user_provider'))
153266 ));
154267
155- Then , activate it in your firewalls section using the `` simple-preauth `` key
156- like this :
268+ Now , activate it in the `` firewalls `` section of your security configuration
269+ using the `` simple_preauth `` key :
157270
158271.. configuration-block ::
159272
160273 .. code-block :: yaml
161274
275+ # app/config/security.yml
162276 security :
163- firewalls :
164- secured_area :
165- pattern : ^/admin
166- simple-preauth :
167- provider : ...
168- authenticator : apikey_authenticator
277+ # ...
278+
279+ firewalls :
280+ secured_area :
281+ pattern : ^/admin
282+ stateless : true
283+ simple_preauth :
284+ authenticator : apikey_authenticator
169285
170286 .. code-block :: xml
171287
@@ -181,7 +297,7 @@ like this:
181297
182298 <firewall name =" secured_area"
183299 pattern =" ^/admin"
184- provider = " ... "
300+ stateless = " true "
185301 >
186302 <simple-preauth authenticator =" apikey_authenticator" />
187303 </firewall >
@@ -198,11 +314,228 @@ like this:
198314 'firewalls' => array(
199315 'secured_area' => array(
200316 'pattern' => '^/admin',
201- 'provider' => 'authenticator',
202- 'simple-preauth' => array(
203- 'provider' => ...,
317+ 'stateless' => true,
318+ 'simple_preauth' => array(
319+ 'authenticator' => 'apikey_authenticator',
320+ ),
321+ ),
322+ ),
323+ ));
324+
325+ That's it! Now, your ``ApiKeyAuthentication `` should be called at the beginning
326+ of each request and your authentication process will take place.
327+
328+ The ``stateless `` configuration parameter prevents Symfony from trying to
329+ store the authentication information in the session, which isn't necessary
330+ since the client will send the ``apikey `` on each request. If you *do * need
331+ to store authentication in the session, keep reading!
332+
333+ .. _cookbook-security-api-key-session :
334+
335+ Storing Authentication in the Session
336+ -------------------------------------
337+
338+ So far, this entry has described a situation where some sort of authentication
339+ token is sent on every request. But in some situations (like an OAuth flow),
340+ the token may be sent on only *one * request. In this case, you will want to
341+ authenticate the user and store that authentication in the session so that
342+ the user is automatically logged in for every subsequent request.
343+
344+ To make this work, first remove the ``stateless `` key from your firewall
345+ configuration or set it to ``false ``:
346+
347+ .. configuration-block ::
348+
349+ .. code-block :: yaml
350+
351+ # app/config/security.yml
352+ security :
353+ # ...
354+
355+ firewalls :
356+ secured_area :
357+ pattern : ^/admin
358+ stateless : false
359+ simple_preauth :
360+ authenticator : apikey_authenticator
361+
362+ .. code-block :: xml
363+
364+ <!-- app/config/security.xml -->
365+ <?xml version =" 1.0" encoding =" UTF-8" ?>
366+ <srv : container xmlns =" http://symfony.com/schema/dic/security"
367+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
368+ xmlns : srv =" http://symfony.com/schema/dic/services"
369+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
370+ http://symfony.com/schema/dic/services/services-1.0.xsd" >
371+ <config >
372+ <!-- ... -->
373+
374+ <firewall name =" secured_area"
375+ pattern =" ^/admin"
376+ stateless =" false"
377+ >
378+ <simple-preauth authenticator =" apikey_authenticator" />
379+ </firewall >
380+ </config >
381+ </srv : container >
382+
383+ .. code-block :: php
384+
385+ // app/config/security.php
386+
387+ // ..
388+ $container->loadFromExtension('security', array(
389+ 'firewalls' => array(
390+ 'secured_area' => array(
391+ 'pattern' => '^/admin',
392+ 'stateless' => false,
393+ 'simple_preauth' => array(
204394 'authenticator' => 'apikey_authenticator',
205395 ),
206396 ),
207397 ),
208398 ));
399+
400+ Storing authentication information in the session works like this:
401+
402+ #. At the end of each request, Symfony serializes the token object (returned
403+ from ``authenticateToken() ``), which also serializes the ``User `` object
404+ (since it's set on a property on the token);
405+ #. On the next request the token is deserialized and the deserialized ``User ``
406+ object is passed to the ``refreshUser() `` function of the user provider.
407+
408+ The second step is the important one: Symfony calls ``refreshUser() `` and passes
409+ you the user object that was serialized in the session. If your users are
410+ stored in the database, then you may want to re-query for a fresh version
411+ of the user to make sure it's not out-of-date. But regardless of your requirements,
412+ ``refreshUser() `` should now return the User object::
413+
414+ // src/Acme/HelloBundle/Security/ApiKeyUserProvider.php
415+
416+ // ...
417+ class ApiKeyUserProvider extends UserProviderInterface
418+ {
419+ // ...
420+
421+ public function refreshUser(UserInterface $user)
422+ {
423+ // $user is the User that you set in the token inside authenticateToken()
424+ // after it has been deserialized from the session
425+
426+ // you might use $user to query the database for a fresh user
427+ // $id = $user->getId();
428+ // use $id to make a query
429+
430+ // if you are *not* reading from a database and are just creating
431+ // a User object (like in this example), you can just return it
432+ return $user;
433+ }
434+ }
435+
436+ .. note ::
437+
438+ You'll also want to make sure that your ``User `` object is being serialized
439+ correctly. If your ``User `` object has private properties, PHP can't serialize
440+ those. In this case, you may get back a User object that has a ``null ``
441+ value for each property. For an example, see :doc: `/cookbook/security/entity_provider `.
442+
443+ Only Authenticating for Certain URLs
444+ ------------------------------------
445+
446+ This entry has assumed that you want to look for the ``apikey `` authentication
447+ on *every * request. But in some situations (like an OAuth flow), you only
448+ really need to look for authentication information once the user has reached
449+ a certain URL (e.g. the redirect URL in OAuth).
450+
451+ Fortunately, handling this situation is easy: just check to see what the
452+ current URL is before creating the token in ``createToken() ``::
453+
454+ // src/Acme/HelloBundle/Security/ApiKeyAuthenticator.php
455+
456+ // ...
457+ use Symfony\Component\Security\Http\HttpUtils;
458+ use Symfony\Component\HttpFoundation\Request;
459+
460+ class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
461+ {
462+ protected $userProvider;
463+
464+ protected $httpUtils;
465+
466+ public function __construct(ApiKeyUserProviderInterface $userProvider, HttpUtils $httpUtils)
467+ {
468+ $this->userProvider = $userProvider;
469+ $this->httpUtils = $httpUtils;
470+ }
471+
472+ public function createToken(Request $request, $providerKey)
473+ {
474+ // set the only URL where we should look for auth information
475+ // and only return the token if we're at that URL
476+ $targetUrl = '/login/check';
477+ if (!$this->httpUtils->checkRequestPath($request, $targetUrl)) {
478+ return;
479+ }
480+
481+ // ...
482+ }
483+ }
484+
485+ This uses the handy :class: `Symfony\\ Component\\ Security\\ Http\\ HttpUtils `
486+ class to check if the current URL matches the URL you're looking for. In this
487+ case, the URL (``/login/check ``) has been hardcoded in the class, but you
488+ could also inject it as the third constructor argument.
489+
490+ Next, just update your service configuration to inject the ``security.http_utils ``
491+ service:
492+
493+ .. configuration-block ::
494+
495+ .. code-block :: yaml
496+
497+ # app/config/config.yml
498+ services :
499+ # ...
500+
501+ apikey_authenticator :
502+ class : Acme\HelloBundle\Security\ApiKeyAuthenticator
503+ arguments : ["@your_api_key_user_provider", "@security.http_utils"]
504+
505+ .. code-block :: xml
506+
507+ <!-- app/config/config.xml -->
508+ <?xml version =" 1.0" ?>
509+ <container xmlns =" http://symfony.com/schema/dic/services"
510+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
511+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
512+ http://symfony.com/schema/dic/services/services-1.0.xsd" >
513+ <services >
514+ <!-- ... -->
515+
516+ <service id =" apikey_authenticator"
517+ class =" Acme\HelloBundle\Security\ApiKeyAuthenticator"
518+ >
519+ <argument type =" service" id =" your_api_key_user_provider" />
520+ <argument type =" service" id =" security.http_utils" />
521+ </service >
522+ </services >
523+ </container >
524+
525+ .. code-block :: php
526+
527+ // app/config/config.php
528+ use Symfony\Component\DependencyInjection\Definition;
529+ use Symfony\Component\DependencyInjection\Reference;
530+
531+ // ...
532+
533+ $container->setDefinition('apikey_authenticator', new Definition(
534+ 'Acme\HelloBundle\Security\ApiKeyAuthenticator',
535+ array(
536+ new Reference('your_api_key_user_provider'),
537+ new Reference('security.http_utils')
538+ )
539+ ));
540+
541+ That's it! Have fun!
0 commit comments