From a5dfb1c66c70c14bc7ca3ffed1790fa5111aedd2 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sat, 23 Apr 2022 18:52:55 +0200 Subject: [PATCH] [Docs] Tweaks and improvements --- Resources/doc/1-configuration-reference.rst | 289 +++++----- Resources/doc/2-data-customization.rst | 538 +++++++++--------- Resources/doc/3-functional-testing.rst | 112 ++-- Resources/doc/4-cors-requests.rst | 18 +- Resources/doc/5-encoder-service.rst | 144 ++--- .../doc/6-extending-jwt-authenticator.rst | 165 +++--- Resources/doc/7-manual-token-creation.rst | 42 +- Resources/doc/8-jwt-user-provider.rst | 118 ++-- .../doc/9-access-authenticated-jwt-token.rst | 26 +- Resources/doc/index.rst | 176 +++--- 10 files changed, 812 insertions(+), 816 deletions(-) diff --git a/Resources/doc/1-configuration-reference.rst b/Resources/doc/1-configuration-reference.rst index 3ede4c38..3b4105ba 100644 --- a/Resources/doc/1-configuration-reference.rst +++ b/Resources/doc/1-configuration-reference.rst @@ -8,88 +8,88 @@ Minimal configuration ~~~~~~~~~~~~~~~~~~~~~ Using RSA/ECDSA -^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~ .. code-block:: yaml - # config/packages/lexik_jwt_authentication.yaml - #... - lexik_jwt_authentication: - secret_key: '%kernel.project_dir%/config/jwt/private.pem' # path to the secret key OR raw secret key, required for creating tokens - public_key: '%kernel.project_dir%/config/jwt/public.pem' # path to the public key OR raw public key, required for verifying tokens - pass_phrase: 'yourpassphrase' # required for creating tokens - # Additional public keys are used to verify signature of incoming tokens, if the key provided in "public_key" configuration node doesn't verify the token - additional_public_keys: - - '%kernel.project_dir%/config/jwt/public1.pem' - - '%kernel.project_dir%/config/jwt/public2.pem' - - '%kernel.project_dir%/config/jwt/public3.pem' + # config/packages/lexik_jwt_authentication.yaml + #... + lexik_jwt_authentication: + secret_key: '%kernel.project_dir%/config/jwt/private.pem' # path to the secret key OR raw secret key, required for creating tokens + public_key: '%kernel.project_dir%/config/jwt/public.pem' # path to the public key OR raw public key, required for verifying tokens + pass_phrase: 'yourpassphrase' # required for creating tokens + # Additional public keys are used to verify signature of incoming tokens, if the key provided in "public_key" configuration node doesn't verify the token + additional_public_keys: + - '%kernel.project_dir%/config/jwt/public1.pem' + - '%kernel.project_dir%/config/jwt/public2.pem' + - '%kernel.project_dir%/config/jwt/public3.pem' Using HMAC -^^^^^^^^^^ +~~~~~~~~~~ .. code-block:: yaml - # config/packages/lexik_jwt_authentication.yaml - #... - lexik_jwt_authentication: - secret_key: yoursecret + # config/packages/lexik_jwt_authentication.yaml + #... + lexik_jwt_authentication: + secret_key: yoursecret Full default configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml - # config/packages/lexik_jwt_authentication.yaml - # ... - lexik_jwt_authentication: - secret_key: ~ - public_key: ~ - pass_phrase: ~ - token_ttl: 3600 # token TTL in seconds, defaults to 1 hour - user_identity_field: username # key under which the user identity will be stored in the token payload - clock_skew: 0 - - # token encoding/decoding settings - encoder: - # token encoder/decoder service - default implementation based on the lcobucci/jwt library - service: lexik_jwt_authentication.encoder.lcobucci - - # encryption algorithm used by the encoder service - signature_algorithm: RS256 - - # token extraction settings - token_extractors: - # look for a token as Authorization Header - authorization_header: - enabled: true - prefix: Bearer - name: Authorization - - # check token in a cookie - cookie: - enabled: false - name: BEARER - - # check token in query string parameter - query_parameter: - enabled: false - name: bearer - - # check token in a cookie - split_cookie: - enabled: false - cookies: - - jwt_hp - - jwt_s - - # remove the token from the response body when using cookies - remove_token_from_body_when_cookies_used: true + # config/packages/lexik_jwt_authentication.yaml + # ... + lexik_jwt_authentication: + secret_key: ~ + public_key: ~ + pass_phrase: ~ + token_ttl: 3600 # token TTL in seconds, defaults to 1 hour + user_identity_field: username # key under which the user identity will be stored in the token payload + clock_skew: 0 + + # token encoding/decoding settings + encoder: + # token encoder/decoder service - default implementation based on the lcobucci/jwt library + service: lexik_jwt_authentication.encoder.lcobucci + + # encryption algorithm used by the encoder service + signature_algorithm: RS256 + + # token extraction settings + token_extractors: + # look for a token as Authorization Header + authorization_header: + enabled: true + prefix: Bearer + name: Authorization + + # check token in a cookie + cookie: + enabled: false + name: BEARER + + # check token in query string parameter + query_parameter: + enabled: false + name: bearer + + # check token in a cookie + split_cookie: + enabled: false + cookies: + - jwt_hp + - jwt_s + + # remove the token from the response body when using cookies + remove_token_from_body_when_cookies_used: true Encoder configuration -^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~ service -''''''' +....... Defaults to ``lexik_jwt_authentication.encoder.lcobucci`` which is based on the `Lcobucci/JWT `__ library. @@ -101,11 +101,11 @@ which is based on the great `web-token/jwt-framework `__ library. -To create your own encoder service, see the `JWT encoder service -customization chapter <5-encoder-service>`__. +To create your own encoder service, see the +:doc:`JWT encoder service customization chapter `. signature_algorithm -''''''''''''''''''' +................... One of the algorithms supported by the default encoder for the configured `crypto engine <#crypto_engine>`__. @@ -115,72 +115,72 @@ configured `crypto engine <#crypto_engine>`__. - ES256, ES384, ES512 (ECDSA) Automatically generating cookies -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You are now able to automatically generate secure and httpOnly cookies when the cookie token extractor is enabled `#753 `__. -:: - - token_extractors: - cookie: - enabled: true - name: BEARER - # ... - set_cookies: - BEARER: ~ - - # Full config with defaults: - # BEARER: - # lifetime: null (defaults to token ttl) - # samesite: lax - # path: / - # domain: null (null means automatically set by symfony) - # secure: true (default to true) - # httpOnly: true +.. code-block:: yaml + + token_extractors: + cookie: + enabled: true + name: BEARER + # ... + set_cookies: + BEARER: ~ + + # Full config with defaults: + # BEARER: + # lifetime: null (defaults to token ttl) + # samesite: lax + # path: / + # domain: null (null means automatically set by symfony) + # secure: true (default to true) + # httpOnly: true Automatically generating split cookies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You are also able to automatically generate split cookies. Benefits of -this approach are in `this -post `__. +this approach are in +`this post `__. Set the signature cookie (jwt_s) lifetime to 0 to create session cookies. -Keep in mind, that SameSite attribute is **not supported** in `some -browsers `__ +Keep in mind, that SameSite attribute is **not supported** in +`some browsers `__ .. code-block:: yaml - token_extractors: - split_cookie: - enabled: true - cookies: - - jwt_hp - - jwt_s - - set_cookies: - jwt_hp: - lifetime: null - samesite: strict - path: / - domain: null - httpOnly: false - split: - - header - - payload - - jwt_s: - lifetime: 0 - samesite: strict - path: / - domain: null - httpOnly: true - split: - - signature + token_extractors: + split_cookie: + enabled: true + cookies: + - jwt_hp + - jwt_s + + set_cookies: + jwt_hp: + lifetime: null + samesite: strict + path: / + domain: null + httpOnly: false + split: + - header + - payload + + jwt_s: + lifetime: 0 + samesite: strict + path: / + domain: null + httpOnly: true + split: + - signature Keep the token in the body when using cookies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -188,14 +188,13 @@ Keep the token in the body when using cookies When using cookies the response defaults to an empty body and result code 204. It is possible to modify this behaviour. -Keep in mind, this invalidates a requirement from the `previously -mentioned -post `__, +Keep in mind, this invalidates a requirement from the +`previously mentioned post `__, namely "JavaScript/front-end should never have access to the full JWT". -:: +.. code-block:: yaml - remove_token_from_body_when_cookies_used: false + remove_token_from_body_when_cookies_used: false Security configuration ---------------------- @@ -204,41 +203,41 @@ For Symfony 5.3 and higher, use the ``jwt`` authenticator: .. code-block:: yaml - # config/packages/security.yaml - security: - enable_authenticator_manager: true - firewalls: - api: - # ... - jwt: ~ # enables the jwt authenticator + # config/packages/security.yaml + security: + enable_authenticator_manager: true + firewalls: + api: + # ... + jwt: ~ # enables the jwt authenticator - # Full config with defaults: - # jwt: - # provider: null (you can put provider here or just ignore this config) - # authenticator: lexik_jwt_authentication.security.jwt_authenticator (default jwt authenticator) - # ... + # Full config with defaults: + # jwt: + # provider: null (you can put provider here or just ignore this config) + # authenticator: lexik_jwt_authentication.security.jwt_authenticator (default jwt authenticator) + # ... For Symfony versions prior to 5.3, use the Guard authenticator: .. code-block:: yaml - firewalls: - # ... - api: - # ... - guard: - authenticators: - - 'lexik_jwt_authentication.jwt_token_authenticator' + firewalls: + # ... + api: + # ... + guard: + authenticators: + - 'lexik_jwt_authentication.jwt_token_authenticator' Authenticator -''''''''''''' +............. For more details about using custom authenticator in your application, -see :doc:`"Extending JWT Authenticator" `. +see :doc:`Extending JWT Authenticator `. Database-less User Provider -''''''''''''''''''''''''''' +........................... For a database-less authentication (i.e. trusting into the JWT data -instead of reloading the user from the database), see :doc:`"A database less -user provider" `. +instead of reloading the user from the database), see +:doc:`"A database less user provider" `. diff --git a/Resources/doc/2-data-customization.rst b/Resources/doc/2-data-customization.rst index f9fd421a..f0ed4088 100644 --- a/Resources/doc/2-data-customization.rst +++ b/Resources/doc/2-data-customization.rst @@ -4,38 +4,28 @@ Data customization and validation **Careful**: Before you add your own custom data, know that the **JWT payload is not encrypted, it is only base64 encoded**. The token signature ensures its integrity (meaning it cannot be modified), but -anyone can read its content (try it using a simple tool like -https://jwt.io/). +anyone can read its content (try it using a simple tool like https://jwt.io/). Table of contents ----------------- -- :ref:`Adding custom data or headers to the JWT - payload ` -- :ref:`Validating data in the JWT - payload ` -- :ref:`Customize your security - token ` -- :ref:`Getting the JWT token string after - encoding ` -- :ref:`Customizing the response on invalid - credentials ` -- :ref:`Customizing the response on invalid - token <#eventsjwt_invalid---customizing-the-invalid-token-response>` -- :ref:`Customizing the response on token not - found ` -- :ref:`Customizing the response on expired - token ` +- :ref:`Adding custom data or headers to the JWT payload ` +- :ref:`Validating data in the JWT payload ` +- :ref:`Customize your security token ` +- :ref:`Getting the JWT token string after encoding ` +- :ref:`Customizing the response on invalid credentials ` +- :ref:`Customizing the response on invalid token <#eventsjwt_invalid---customizing-the-invalid-token-response>` +- :ref:`Customizing the response on token not found ` +- :ref:`Customizing the response on expired token ` Adding custom data or headers to the JWT ---------------------------------------- -.. eventsjwt_created: +.. _eventsjwt_created: Using Events::JWT_CREATED -^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~ By default the JWT payload will contain the username and the token TTL, but you can add your own data. @@ -44,97 +34,96 @@ You can also modify the header to fit on your application context. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.jwt_created_listener: - class: App\EventListener\JWTCreatedListener - arguments: [ '@request_stack' ] - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated } + # config/services.yaml + services: + acme_api.event.jwt_created_listener: + class: App\EventListener\JWTCreatedListener + arguments: [ '@request_stack' ] + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated } -Example: Add client ip to the encoded payload -''''''''''''''''''''''''''''''''''''''''''''' +Example: Add client IP address to the encoded payload +..................................................... .. code-block:: php - // src/App/EventListener/JWTCreatedListener.php + // src/App/EventListener/JWTCreatedListener.php - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; - use Symfony\Component\HttpFoundation\RequestStack; + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; + use Symfony\Component\HttpFoundation\RequestStack; - /** - * @var RequestStack - */ - private $requestStack; + /** + * @var RequestStack + */ + private $requestStack; - /** - * @param RequestStack $requestStack - */ - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; - } + /** + * @param RequestStack $requestStack + */ + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } - /** - * @param JWTCreatedEvent $event - * - * @return void - */ - public function onJWTCreated(JWTCreatedEvent $event) - { - $request = $this->requestStack->getCurrentRequest(); + /** + * @param JWTCreatedEvent $event + * + * @return void + */ + public function onJWTCreated(JWTCreatedEvent $event) + { + $request = $this->requestStack->getCurrentRequest(); - $payload = $event->getData(); - $payload['ip'] = $request->getClientIp(); + $payload = $event->getData(); + $payload['ip'] = $request->getClientIp(); - $event->setData($payload); - - $header = $event->getHeader(); - $header['cty'] = 'JWT'; + $event->setData($payload); - $event->setHeader($header); - } + $header = $event->getHeader(); + $header['cty'] = 'JWT'; + + $event->setHeader($header); + } Example: Override token expiration date calculation to be more flexible -''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +....................................................................... .. code-block:: php - // src/App/EventListener/JWTCreatedListener.php + // src/App/EventListener/JWTCreatedListener.php - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; - /** - * @param JWTCreatedEvent $event - * - * @return void - */ - public function onJWTCreated(JWTCreatedEvent $event) - { - $expiration = new \DateTime('+1 day'); - $expiration->setTime(2, 0, 0); + /** + * @param JWTCreatedEvent $event + * + * @return void + */ + public function onJWTCreated(JWTCreatedEvent $event) + { + $expiration = new \DateTime('+1 day'); + $expiration->setTime(2, 0, 0); - $payload = $event->getData(); - $payload['exp'] = $expiration->getTimestamp(); + $payload = $event->getData(); + $payload['exp'] = $expiration->getTimestamp(); - $event->setData($payload); - } + $event->setData($payload); + } Using a custom payload at JWT creation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you :doc:`create JWT tokens -programmatically `, you can add custom -data to the JWT using the method +If you :doc:`create JWT tokens programmatically `, +you can add custom data to the JWT using the method ``createFromPayload(UserInterface $user, array $payload)`` .. code-block:: php - $payload = ['foo' => 'bar']; + $payload = ['foo' => 'bar']; - $jwt = $this->container->get('lexik_jwt_authentication.jwt_manager')->createFromPayload($user, $payload); + $jwt = $this->container->get('lexik_jwt_authentication.jwt_manager')->createFromPayload($user, $payload); -.. eventsjwt_decoded: +.. _eventsjwt_decoded: Events::JWT_DECODED - Validating data in the JWT payload -------------------------------------------------------- @@ -144,64 +133,63 @@ own additional validation. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.jwt_decoded_listener: - class: App\EventListener\JWTDecodedListener - arguments: [ '@request_stack' ] - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_decoded, method: onJWTDecoded } + # config/services.yaml + services: + acme_api.event.jwt_decoded_listener: + class: App\EventListener\JWTDecodedListener + arguments: [ '@request_stack' ] + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_decoded, method: onJWTDecoded } Example: Check client ip the decoded payload (from example 1) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTDecodedListener.php + // src/App/EventListener/JWTDecodedListener.php + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent; - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent; + /** + * @param JWTDecodedEvent $event + * + * @return void + */ + public function onJWTDecoded(JWTDecodedEvent $event) + { + $request = $this->requestStack->getCurrentRequest(); - /** - * @param JWTDecodedEvent $event - * - * @return void - */ - public function onJWTDecoded(JWTDecodedEvent $event) - { - $request = $this->requestStack->getCurrentRequest(); - - $payload = $event->getPayload(); + $payload = $event->getPayload(); - if (!isset($payload['ip']) || $payload['ip'] !== $request->getClientIp()) { - $event->markAsInvalid(); - } - } + if (!isset($payload['ip']) || $payload['ip'] !== $request->getClientIp()) { + $event->markAsInvalid(); + } + } -Example: Add additional data to payload - to get it in your `custom UserProvider <8-jwt-user-provider>`__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Example: Add additional data to payload - to get it in your :doc:`custom UserProvider ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTDecodedListener.php + // src/App/EventListener/JWTDecodedListener.php - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent; + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent; - /** - * @param JWTDecodedEvent $event - * - * @return void - */ - public function onJWTDecoded(JWTDecodedEvent $event) - { - $payload = $event->getPayload(); - $user = $this->userRepository->findOneByUsername($payload['username']); + /** + * @param JWTDecodedEvent $event + * + * @return void + */ + public function onJWTDecoded(JWTDecodedEvent $event) + { + $payload = $event->getPayload(); + $user = $this->userRepository->findOneByUsername($payload['username']); - $payload['custom_user_data'] = $user->getCustomUserInformations(); + $payload['custom_user_data'] = $user->getCustomUserInformations(); - $event->setPayload($payload); // Don't forget to regive the payload for next event / step - } + $event->setPayload($payload); // Don't forget to regive the payload for next event / step + } -.. eventsjwt_authenticated: +.. _eventsjwt_authenticated: Events::JWT_AUTHENTICATED - Customizing your security token ----------------------------------------------------------- @@ -211,36 +199,35 @@ allow JWT properties to be used by your application. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.jwt_authenticated_listener: - class: App\EventListener\JWTAuthenticatedListener - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_authenticated, method: onJWTAuthenticated } + # config/services.yaml + services: + acme_api.event.jwt_authenticated_listener: + class: App\EventListener\JWTAuthenticatedListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_authenticated, method: onJWTAuthenticated } Example: Keep a UUID that was set into the JWT in the authenticated token -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTAuthenticatedListener.php - - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent; + // src/App/EventListener/JWTAuthenticatedListener.php + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent; - /** - * @param JWTAuthenticatedEvent $event - * - * @return void - */ - public function onJWTAuthenticated(JWTAuthenticatedEvent $event) - { - $token = $event->getToken(); - $payload = $event->getPayload(); + /** + * @param JWTAuthenticatedEvent $event + * + * @return void + */ + public function onJWTAuthenticated(JWTAuthenticatedEvent $event) + { + $token = $event->getToken(); + $payload = $event->getPayload(); - $token->setAttribute('uuid', $payload['uuid']); - } + $token->setAttribute('uuid', $payload['uuid']); + } -.. eventsauthentication_success: +.. _eventsauthentication_success: Events::AUTHENTICATION_SUCCESS - Adding public data to the JWT response ----------------------------------------------------------------------- @@ -250,42 +237,41 @@ JWT but you can add your own public data to it. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.authentication_success_listener: - class: App\EventListener\AuthenticationSuccessListener - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: onAuthenticationSuccessResponse } + # config/services.yaml + services: + acme_api.event.authentication_success_listener: + class: App\EventListener\AuthenticationSuccessListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: onAuthenticationSuccessResponse } Example: Add user roles to the response body -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/AuthenticationSuccessListener.php + // src/App/EventListener/AuthenticationSuccessListener.php + use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent; - use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent; + /** + * @param AuthenticationSuccessEvent $event + */ + public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event) + { + $data = $event->getData(); + $user = $event->getUser(); - /** - * @param AuthenticationSuccessEvent $event - */ - public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event) - { - $data = $event->getData(); - $user = $event->getUser(); + if (!$user instanceof UserInterface) { + return; + } - if (!$user instanceof UserInterface) { - return; - } + $data['data'] = array( + 'roles' => $user->getRoles(), + ); - $data['data'] = array( - 'roles' => $user->getRoles(), - ); + $event->setData($data); + } - $event->setData($data); - } - -.. eventsjwt_encoded: +.. _eventsjwt_encoded: Events::JWT_ENCODED - Getting the JWT token string after encoding ----------------------------------------------------------------- @@ -293,23 +279,22 @@ Events::JWT_ENCODED - Getting the JWT token string after encoding You may need to get JWT after its creation. Example: Obtain JWT string -^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTEncodedListener.php - - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent; + // src/App/EventListener/JWTEncodedListener.php + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent; - /** - * @param JWTEncodedEvent $event - */ - public function onJwtEncoded(JWTEncodedEvent $event) - { - $token = $event->getJWTString(); - } + /** + * @param JWTEncodedEvent $event + */ + public function onJwtEncoded(JWTEncodedEvent $event) + { + $token = $event->getJWTString(); + } -.. eventsauthentication_failure: +.. _eventsauthentication_failure: Events::AUTHENTICATION_FAILURE - Customizing the failure response body ---------------------------------------------------------------------- @@ -320,40 +305,39 @@ custom response. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.authentication_failure_listener: - class: App\EventListener\AuthenticationFailureListener - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_failure, method: onAuthenticationFailureResponse } + # config/services.yaml + services: + acme_api.event.authentication_failure_listener: + class: App\EventListener\AuthenticationFailureListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_failure, method: onAuthenticationFailureResponse } Example: Set a custom response on authentication failure .. code-block:: php - // src/App/EventListener/AuthenticationFailureListener.php + // src/App/EventListener/AuthenticationFailureListener.php + use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent; + use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; + use Symfony\Component\HttpFoundation\JsonResponse; - use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent; - use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; - use Symfony\Component\HttpFoundation\JsonResponse; + /** + * @param AuthenticationFailureEvent $event + */ + public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event) + { + $data = [ + 'name' => 'John Doe', + 'foo' => 'bar', + ]; - /** - * @param AuthenticationFailureEvent $event - */ - public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event) - { - $data = [ - 'name' => 'John Doe', - 'foo' => 'bar', - ]; + $response = new JWTAuthenticationFailureResponse('Bad credentials, please verify that your username/password are correctly set', JsonResponse::HTTP_UNAUTHORIZED); + $response->setData($data); - $response = new JWTAuthenticationFailureResponse('Bad credentials, please verify that your username/password are correctly set', JsonResponse::HTTP_UNAUTHORIZED); - $response->setData($data); + $event->setResponse($response); + } - $event->setResponse($response); - } - -.. eventsjwt_invalid: +.. _eventsjwt_invalid: Events::JWT_INVALID - Customizing the invalid token response ------------------------------------------------------------ @@ -364,79 +348,79 @@ you can set a custom response. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.jwt_invalid_listener: - class: App\EventListener\JWTInvalidListener - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_invalid, method: onJWTInvalid } + # config/services.yaml + services: + acme_api.event.jwt_invalid_listener: + class: App\EventListener\JWTInvalidListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_invalid, method: onJWTInvalid } Example: Set a custom response message and status code on invalid token -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTInvalidListener.php - - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent; - use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; + // src/App/EventListener/JWTInvalidListener.php + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent; + use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; - /** - * @param JWTInvalidEvent $event - */ - public function onJWTInvalid(JWTInvalidEvent $event) - { - $response = new JWTAuthenticationFailureResponse('Your token is invalid, please login again to get a new one', 403); + /** + * @param JWTInvalidEvent $event + */ + public function onJWTInvalid(JWTInvalidEvent $event) + { + $response = new JWTAuthenticationFailureResponse('Your token is invalid, please login again to get a new one', 403); - $event->setResponse($response); - } + $event->setResponse($response); + } -.. eventsjwt_not_found: +.. _eventsjwt_not_found: Events::JWT_NOT_FOUND - Customizing the response on token not found ------------------------------------------------------------------- -| By default, if no token is found in a request, the authentication - listener will either call the entry point that returns a unauthorized - (401) json response, or (if the firewall allows anonymous requests), - just let the request continue. -| Thanks to this event, you can set a custom response. +By default, if no token is found in a request, the authentication +listener will either call the entry point that returns a unauthorized +(401) json response, or (if the firewall allows anonymous requests), +just let the request continue. + +Thanks to this event, you can set a custom response. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.jwt_notfound_listener: - class: App\EventListener\JWTNotFoundListener - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_not_found, method: onJWTNotFound } + # config/services.yaml + services: + acme_api.event.jwt_notfound_listener: + class: App\EventListener\JWTNotFoundListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_not_found, method: onJWTNotFound } Example: Set a custom response message on token not found -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTNotFoundListener.php + // src/App/EventListener/JWTNotFoundListener.php - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent; - use Symfony\Component\HttpFoundation\JsonResponse; + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent; + use Symfony\Component\HttpFoundation\JsonResponse; - /** - * @param JWTNotFoundEvent $event - */ - public function onJWTNotFound(JWTNotFoundEvent $event) - { - $data = [ - 'status' => '403 Forbidden', - 'message' => 'Missing token', - ]; + /** + * @param JWTNotFoundEvent $event + */ + public function onJWTNotFound(JWTNotFoundEvent $event) + { + $data = [ + 'status' => '403 Forbidden', + 'message' => 'Missing token', + ]; - $response = new JsonResponse($data, 403); + $response = new JsonResponse($data, 403); - $event->setResponse($response); - } + $event->setResponse($response); + } -.. eventsjwt_expired: +.. _eventsjwt_expired: Events::JWT_EXPIRED - Customizing the response message on expired token ----------------------------------------------------------------------- @@ -448,33 +432,33 @@ custom response or simply change the response message. .. code-block:: yaml - # config/services.yaml - services: - acme_api.event.jwt_expired_listener: - class: App\EventListener\JWTExpiredListener - tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired } + # config/services.yaml + services: + acme_api.event.jwt_expired_listener: + class: App\EventListener\JWTExpiredListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired } Example: Customize the response in case of expired token -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - // src/App/EventListener/JWTExpiredListener.php + // src/App/EventListener/JWTExpiredListener.php - use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent; - use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; + use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent; + use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; - /** - * @param JWTExpiredEvent $event - */ - public function onJWTExpired(JWTExpiredEvent $event) - { - /** @var JWTAuthenticationFailureResponse */ - $response = $event->getResponse(); + /** + * @param JWTExpiredEvent $event + */ + public function onJWTExpired(JWTExpiredEvent $event) + { + /** @var JWTAuthenticationFailureResponse */ + $response = $event->getResponse(); - $response->setMessage('Your token is expired, please renew it.'); - } + $response->setMessage('Your token is expired, please renew it.'); + } **Protip:** You might want to use the same method for customizing the response on both ``JWT_INVALID``, ``JWT_NOT_FOUND`` and/or diff --git a/Resources/doc/3-functional-testing.rst b/Resources/doc/3-functional-testing.rst index 088491be..b06fc177 100644 --- a/Resources/doc/3-functional-testing.rst +++ b/Resources/doc/3-functional-testing.rst @@ -6,82 +6,82 @@ Configuration Generate some test specific keys, for example: -.. code-block:: bash +.. code-block:: terminal - $ openssl genrsa -out config/jwt/private-test.pem -aes256 4096 - $ openssl rsa -pubout -in config/jwt/private-test.pem -out config/jwt/public-test.pem + $ openssl genrsa -out config/jwt/private-test.pem -aes256 4096 + $ openssl rsa -pubout -in config/jwt/private-test.pem -out config/jwt/public-test.pem Override the bundle configuration in your ``config_test.yml`` : .. code-block:: yaml - # config/test/lexik_jwt_authentication.yaml - lexik_jwt_authentication: - secret_key: '%kernel.project_dir%/config/jwt/private-test.pem' - public_key: '%kernel.project_dir%/config/jwt/public-test.pem' + # config/test/lexik_jwt_authentication.yaml + lexik_jwt_authentication: + secret_key: '%kernel.project_dir%/config/jwt/private-test.pem' + public_key: '%kernel.project_dir%/config/jwt/public-test.pem' **Protip:** You might want to commit those keys if you intend to run -your test on a ci server. +your test on a CI server. Usage ----- -Create an authenticated client : +Create an authenticated client: .. code-block:: php - /** - * Create a client with a default Authorization header. - * - * @param string $username - * @param string $password - * - * @return \Symfony\Bundle\FrameworkBundle\Client - */ - protected function createAuthenticatedClient($username = 'user', $password = 'password') - { - $client = static::createClient(); - $client->request( - 'POST', - '/api/login_check', - [], - [], - ['CONTENT_TYPE' => 'application/json'], - json_encode([ - '_username' => $username, - '_password' => $password, - ]) - ); - - $data = json_decode($client->getResponse()->getContent(), true); - - $client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $data['token'])); - - return $client; - } - - /** - * test getPagesAction - */ - public function testGetPages() - { - $client = $this->createAuthenticatedClient(); - $client->request('GET', '/api/pages'); - // ... - } + /** + * Create a client with a default Authorization header. + * + * @param string $username + * @param string $password + * + * @return \Symfony\Bundle\FrameworkBundle\Client + */ + protected function createAuthenticatedClient($username = 'user', $password = 'password') + { + $client = static::createClient(); + $client->request( + 'POST', + '/api/login_check', + [], + [], + ['CONTENT_TYPE' => 'application/json'], + json_encode([ + '_username' => $username, + '_password' => $password, + ]) + ); + + $data = json_decode($client->getResponse()->getContent(), true); + + $client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $data['token'])); + + return $client; + } + + /** + * test getPagesAction + */ + public function testGetPages() + { + $client = $this->createAuthenticatedClient(); + $client->request('GET', '/api/pages'); + // ... + } Or manually generate a JWT token for end-to-end testing: .. code-block:: php - use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; + use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; - protected static function createAuthenticatedClient(array $claims) - { - $client = self::createClient(); - $encoder = $client->getContainer()->get(JWTEncoderInterface::class); + protected static function createAuthenticatedClient(array $claims) + { + $client = self::createClient(); + $encoder = $client->getContainer()->get(JWTEncoderInterface::class); - $client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $encoder->encode($claims))); + $client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $encoder->encode($claims))); - return $client; - } + return $client; + } diff --git a/Resources/doc/4-cors-requests.rst b/Resources/doc/4-cors-requests.rst index bcbb0021..176e1e6a 100644 --- a/Resources/doc/4-cors-requests.rst +++ b/Resources/doc/4-cors-requests.rst @@ -10,18 +10,18 @@ without having to modify your server configuration. See the documentation for installation and usage instructions. Example usage with the LexikJWTAuthenticationBundle -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml - nelmio_cors: - ... - paths: - '^/api/': - allow_origin: ['*'] - allow_headers: ['*'] - allow_methods: ['POST', 'PUT', 'GET', 'DELETE'] - max_age: 3600 + nelmio_cors: + ... + paths: + '~/api/': + allow_origin: ['*'] + allow_headers: ['*'] + allow_methods: ['POST', 'PUT', 'GET', 'DELETE'] + max_age: 3600 The important thing to note here is that both the ``login`` and ``api`` firewalls paths (in our example ``/api/login_check`` and ``/api``) must diff --git a/Resources/doc/5-encoder-service.rst b/Resources/doc/5-encoder-service.rst index c713988f..7eeec8a0 100644 --- a/Resources/doc/5-encoder-service.rst +++ b/Resources/doc/5-encoder-service.rst @@ -2,13 +2,11 @@ JWT encoder service customization ================================= This bundle comes with two built-in token encoders, one based on the -```namshi/jose`` `__ library (default) -and the later based on the -```lcobucci/jwt`` `__ library. If both -don't suit your needs, you can replace it with your own encoder service. -Here's an example implementing a -```nixilla/php-jwt`` `__ library -based encoder. +`namshi/jose `__ library (default) and the later +based on the `lcobucci/jwt `__ library. If both +don't suit your needs, you can replace it with your own encoder service. Here's +an example implementing a `nixilla/php-jwt `__ +library based encoder. Creating your own encoder ------------------------- @@ -18,80 +16,82 @@ Create the encoder class .. code-block:: php - // src/App/Encoder/NixillaJWTEncoder.php - namespace App\Encoder; - - use JWT\Authentication\JWT; - use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; - use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException; - use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; - - /** - * NixillaJWTEncoder - * - * @author Nicolas Cabot - */ - class NixillaJWTEncoder implements JWTEncoderInterface - { - private $key; - - public function __construct(string $key = 'super_secret_key') - { - $this->key = $key; - } - - /** - * {@inheritdoc} - */ - public function encode(array $data) - { - try { - return JWT::encode($data, $this->key); - } - catch (\Exception $e) { - throw new JWTEncodeFailureException(JWTEncodeFailureException::INVALID_CONFIG, 'An error occurred while trying to encode the JWT token.', $e); - } - } - - /** - * {@inheritdoc} - */ - public function decode($token) - { - try { - return (array) JWT::decode($token, $this->key); - } catch (\Exception $e) { - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', $e); - } - } - } + // src/App/Encoder/NixillaJWTEncoder.php + namespace App\Encoder; + + use JWT\Authentication\JWT; + use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; + use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException; + use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; + + /** + * NixillaJWTEncoder + * + * @author Nicolas Cabot + */ + class NixillaJWTEncoder implements JWTEncoderInterface + { + private $key; + + public function __construct(string $key = 'super_secret_key') + { + $this->key = $key; + } + + /** + * {@inheritdoc} + */ + public function encode(array $data) + { + try { + return JWT::encode($data, $this->key); + } + catch (\Exception $e) { + throw new JWTEncodeFailureException(JWTEncodeFailureException::INVALID_CONFIG, 'An error occurred while trying to encode the JWT token.', $e); + } + } + + /** + * {@inheritdoc} + */ + public function decode($token) + { + try { + return (array) JWT::decode($token, $this->key); + } catch (\Exception $e) { + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', $e); + } + } + } Declare it as a service ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml - # config/services.yaml - services: - acme_api.encoder.nixilla_jwt_encoder: - class: App\Encoder\NixillaJWTEncoder + # config/services.yaml + services: + acme_api.encoder.nixilla_jwt_encoder: + class: App\Encoder\NixillaJWTEncoder Use it as encoder service ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml - # config/packages/lexik_jwt_authentication.yaml - lexik_jwt_authentication: - # ... - encoder: - service: acme_api.encoder.nixilla_jwt_encoder - -| **Note** -| You can use the ``lexik_jwt_authentication.encoder.crypto_engine`` and - ``lexik_jwt_authentication.encoder.signature_algorithm`` parameters - that represent the corresponding configuration options by injecting - them as argument of the encoder's service, then use them through the - library on which the encoder is based on. -| See the :doc:`configuration reference <1-configuration-reference>` for - more informations. + # config/packages/lexik_jwt_authentication.yaml + lexik_jwt_authentication: + # ... + encoder: + service: acme_api.encoder.nixilla_jwt_encoder + +.. note:: + + You can use the ``lexik_jwt_authentication.encoder.crypto_engine`` and + ``lexik_jwt_authentication.encoder.signature_algorithm`` parameters + that represent the corresponding configuration options by injecting + them as argument of the encoder's service, then use them through the + library on which the encoder is based on. + + See the :doc:`configuration reference <1-configuration-reference>` for + more information. diff --git a/Resources/doc/6-extending-jwt-authenticator.rst b/Resources/doc/6-extending-jwt-authenticator.rst index c32e82eb..9b1366d2 100644 --- a/Resources/doc/6-extending-jwt-authenticator.rst +++ b/Resources/doc/6-extending-jwt-authenticator.rst @@ -1,100 +1,101 @@ Extending Authenticator ======================= -The ``JWTTokenAuthenticator`` (Symfony < 5.3) or ``JWTAuthenticator`` -(Symfony >= 5.3) class is responsible of authenticating JWT tokens. It -is used through the -``lexik_jwt_authentication.security.guard.jwt_token_authenticator`` -(Symfony < 5.3) or -``lexik_jwt_authentication.security.jwt_authenticator`` (Symfony >= 5.3) +The ``JWTTokenAuthenticator`` (Symfony < 5.3) or ``JWTAuthenticator`` (Symfony >= 5.3) +class is responsible of authenticating JWT tokens. It is used through the +``lexik_jwt_authentication.security.guard.jwt_token_authenticator`` (Symfony < 5.3) +or ``lexik_jwt_authentication.security.jwt_authenticator`` (Symfony >= 5.3) abstract service which can be customized in the most flexible but still structured way to do it: *creating your own authenticators by extending the service*, so you can manage various security contexts in the same application. -.. creating-your-own-authenticator: +.. _creating-your-own-authenticator: Creating your own Authenticator ------------------------------- -For Symfony versions prior to 5.3: +For Symfony versions prior to 5.3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - namespace App\Security\Guard; + namespace App\Security\Guard; - use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator as BaseAuthenticator; + use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator as BaseAuthenticator; - class JWTTokenAuthenticator extends BaseAuthenticator - { - // Your own logic - } + class JWTTokenAuthenticator extends BaseAuthenticator + { + // Your own logic + } .. code-block:: yaml - # config/services.yaml - services: - app.jwt_token_authenticator: - class: App\Security\Guard\JWTTokenAuthenticator - parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator + # config/services.yaml + services: + app.jwt_token_authenticator: + class: App\Security\Guard\JWTTokenAuthenticator + parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator .. code-block:: yaml - # config/packages/security.yaml - security: - # ... - firewalls: - # ... - api: - pattern: ^/api - stateless: true - guard: - authenticators: - - app.jwt_token_authenticator - -For Symfony 5.3 and higher: + # config/packages/security.yaml + security: + # ... + firewalls: + # ... + api: + pattern: ~/api + stateless: true + guard: + authenticators: + - app.jwt_token_authenticator + +For Symfony 5.3 and higher +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php - namespace App\Security; + namespace App\Security; - use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator; + use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator; - class CustomAuthenticator extends JWTAuthenticator - { - // Your own logic - } + class CustomAuthenticator extends JWTAuthenticator + { + // Your own logic + } .. code-block:: yaml - # config/services.yaml - services: - app.custom_authenticator: - class: App\Security\CustomAuthenticator - parent: lexik_jwt_authentication.security.jwt_authenticator + # config/services.yaml + services: + app.custom_authenticator: + class: App\Security\CustomAuthenticator + parent: lexik_jwt_authentication.security.jwt_authenticator .. code-block:: yaml - # config/packages/security.yaml - security: - # ... - firewalls: - # ... - api: - pattern: ^/api - stateless: true - jwt: - authenticator: app.custom_authenticator + # config/packages/security.yaml + security: + # ... + firewalls: + # ... + api: + pattern: ~/api + stateless: true + jwt: + authenticator: app.custom_authenticator + +.. note:: -**Note:** The code examples of this section require to have this step -done, it may not be repeated. + The code examples of this section require to have this step + done, it may not be repeated. Using different Token Extractors per Authenticator -------------------------------------------------- Token extractors are set up in the main configuration of this bundle -(see :doc:`configuration -reference `). +(see :doc:`configuration reference `). If your application contains multiple firewalls with different security contexts, you may want to configure the different token extractors which should be used on each firewall respectively. This can be done by having @@ -105,28 +106,28 @@ You can overwrite the ``getTokenExtractor()`` in custom authenticator: .. code-block:: php - /** - * @return TokenExtractor\TokenExtractorInterface - */ - protected function getTokenExtractor() - { - // Return a custom extractor, no matter of what are configured - return new TokenExtractor\AuthorizationHeaderTokenExtractor('Token', 'Authorization'); - - // Or retrieve the chain token extractor for mapping/unmapping extractors for this authenticator - $chainExtractor = parent::getTokenExtractor(); - - // Clear the token extractor map from all configured extractors - $chainExtractor->clearMap(); - - // Or only remove a specific extractor - $chainTokenExtractor->removeExtractor(function (TokenExtractor\TokenExtractorInterface $extractor) { - return $extractor instanceof TokenExtractor\CookieTokenExtractor; - }); - - // Add a new query parameter extractor to the configured ones - $chainExtractor->addExtractor(new TokenExtractor\QueryParameterTokenExtractor('jwt')); - - // Return the chain token extractor with the new map - return $chainTokenExtractor; - } + /** + * @return TokenExtractor\TokenExtractorInterface + */ + protected function getTokenExtractor() + { + // Return a custom extractor, no matter of what are configured + return new TokenExtractor\AuthorizationHeaderTokenExtractor('Token', 'Authorization'); + + // Or retrieve the chain token extractor for mapping/unmapping extractors for this authenticator + $chainExtractor = parent::getTokenExtractor(); + + // Clear the token extractor map from all configured extractors + $chainExtractor->clearMap(); + + // Or only remove a specific extractor + $chainTokenExtractor->removeExtractor(function (TokenExtractor\TokenExtractorInterface $extractor) { + return $extractor instanceof TokenExtractor\CookieTokenExtractor; + }); + + // Add a new query parameter extractor to the configured ones + $chainExtractor->addExtractor(new TokenExtractor\QueryParameterTokenExtractor('jwt')); + + // Return the chain token extractor with the new map + return $chainTokenExtractor; + } diff --git a/Resources/doc/7-manual-token-creation.rst b/Resources/doc/7-manual-token-creation.rst index 45dd2d55..a1742c6b 100644 --- a/Resources/doc/7-manual-token-creation.rst +++ b/Resources/doc/7-manual-token-creation.rst @@ -8,22 +8,22 @@ directly: .. code-block:: php - namespace App\Controller; + namespace App\Controller; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\JsonResponse; - use Symfony\Component\Security\Core\User\UserInterface; - use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\Security\Core\User\UserInterface; + use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; - class ApiController extends Controller - { - public function getTokenUser(UserInterface $user, JWTTokenManagerInterface $JWTManager) - { - // ... + class ApiController extends Controller + { + public function getTokenUser(UserInterface $user, JWTTokenManagerInterface $JWTManager) + { + // ... - return new JsonResponse(['token' => $JWTManager->create($user)]); - } - } + return new JsonResponse(['token' => $JWTManager->create($user)]); + } + } This dispatches the ``Events::JWT_CREATED``, ``Events::JWT_ENCODED`` events and returns a JWT token, but the @@ -35,18 +35,18 @@ your login form: .. code-block:: php - public function fooAction(UserInterface $user) - { - $authenticationSuccessHandler = $this->container->get('lexik_jwt_authentication.handler.authentication_success'); - - return $authenticationSuccessHandler->handleAuthenticationSuccess($user); - } + public function fooAction(UserInterface $user) + { + $authenticationSuccessHandler = $this->container->get('lexik_jwt_authentication.handler.authentication_success'); + + return $authenticationSuccessHandler->handleAuthenticationSuccess($user); + } You can also pass an existing JWT to the ``handleAuthenticationSuccess`` method: .. code-block:: php - $jwt = $this->container->get('lexik_jwt_authentication.jwt_manager')->create($user); + $jwt = $this->container->get('lexik_jwt_authentication.jwt_manager')->create($user); - return $authenticationSuccessHandler->handleAuthenticationSuccess($user, $jwt); + return $authenticationSuccessHandler->handleAuthenticationSuccess($user, $jwt); diff --git a/Resources/doc/8-jwt-user-provider.rst b/Resources/doc/8-jwt-user-provider.rst index 6ad43c72..132bb0d8 100644 --- a/Resources/doc/8-jwt-user-provider.rst +++ b/Resources/doc/8-jwt-user-provider.rst @@ -7,11 +7,10 @@ From `jwt.io `__: about the user, avoiding the need to query the database more than once. https://jwt.io/introduction -| A JWT is *self-contained*, meaning that we can trust into its payload - for processing the authentication. In a nutshell, there should be no - need for loading the user from the database when authenticating a JWT - Token, -| the database should be hit only once for delivering the token. +A JWT is *self-contained*, meaning that we can trust into its payload +for processing the authentication. In a nutshell, there should be no +need for loading the user from the database when authenticating a JWT Token, +the database should be hit only once for delivering the token. That's why we decided to provide a user provider which is able to create User instances from the JWT payload. @@ -23,32 +22,38 @@ To work, the provider just needs a few lines of configuration: .. code-block:: yaml - # config/packages/security.yaml - security: - providers: - jwt: - lexik_jwt: ~ + # config/packages/security.yaml + security: + providers: + jwt: + lexik_jwt: ~ Then, use it on your JWT protected firewall: +Symfony versions prior to 5.3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + .. code-block:: yaml - # Symfony versions prior to 5.3 - security: - firewalls: - api: - provider: jwt - guard: - # ... + # config/packages/security.yaml + security: + firewalls: + api: + provider: jwt + guard: + # ... + +Symfony 5.3 and higher +~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml - # Symfony 5.3 and higher - security: - firewalls: - api: - provider: jwt - jwt: ~ + # config/packages/security.yaml + security: + firewalls: + api: + provider: jwt + jwt: ~ What does it change? -------------------- @@ -71,46 +76,47 @@ username and the JWT token payload as arguments and returns an instance of the class. Sample implementation -''''''''''''''''''''' +--------------------- .. code-block:: php - namespace App\Security; - - final class User implements JWTUserInterface - { - // Your own logic - - public function __construct($username, array $roles, $email) - { - $this->username = $username; - $this->roles = $roles; - $this->email = $email; - } - - public static function createFromPayload($username, array $payload) - { - return new self( - $username, - $payload['roles'], // Added by default - $payload['email'] // Custom - ); - } - } - -*Note*: You can extend the default ``JWTUser`` class if that fits your -needs. + namespace App\Security; + + final class User implements JWTUserInterface + { + // Your own logic + + public function __construct($username, array $roles, $email) + { + $this->username = $username; + $this->roles = $roles; + $this->email = $email; + } + + public static function createFromPayload($username, array $payload) + { + return new self( + $username, + $payload['roles'], // Added by default + $payload['email'] // Custom + ); + } + } + +.. note:: + + You can extend the default ``JWTUser`` class if that fits your needs. Configuration -''''''''''''' +------------- .. code-block:: yaml - # config/packages/security.yaml - providers: - # ... - jwt: - lexik_jwt: - class: App\Security\User + # config/packages/security.yaml + providers: + # ... + jwt: + lexik_jwt: + class: App\Security\User And voilĂ ! diff --git a/Resources/doc/9-access-authenticated-jwt-token.rst b/Resources/doc/9-access-authenticated-jwt-token.rst index 352263a3..96097c7b 100644 --- a/Resources/doc/9-access-authenticated-jwt-token.rst +++ b/Resources/doc/9-access-authenticated-jwt-token.rst @@ -6,23 +6,21 @@ Service for some purposes, you can: #. Inject *TokenStorageInterface* and *JWTTokenManagerInterface*: -.. code-block:: php + .. code-block:: php - use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; + use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; - public function __construct(TokenStorageInterface $tokenStorageInterface, JWTTokenManagerInterface $jwtManager) - { - $this->jwtManager = $jwtManager; - $this->tokenStorageInterface = $tokenStorageInterface; - } + public function __construct(TokenStorageInterface $tokenStorageInterface, JWTTokenManagerInterface $jwtManager) + { + $this->jwtManager = $jwtManager; + $this->tokenStorageInterface = $tokenStorageInterface; + } -#. Call ``decode()`` in jwtManager, and ``getToken()`` in - tokenStorageInterface. +#. Call ``decode()`` in jwtManager, and ``getToken()`` in TokenStorageInterface. -.. code-block:: php + .. code-block:: php - $decodedJwtToken = $this->jwtManager->decode($this->tokenStorageInterface->getToken()); + $decodedJwtToken = $this->jwtManager->decode($this->tokenStorageInterface->getToken()); -This returns the decoded information of the JWT token sent in the -current request. +This returns the decoded information of the JWT token sent in the current request. diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index c173125c..13b286e2 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -16,12 +16,12 @@ Add `lexik/jwt-authentication-bundle `__ to your ``composer.json`` file: -.. code-block:: bash +.. code-block:: terminal - php composer.phar require "lexik/jwt-authentication-bundle" + $ php composer.phar require "lexik/jwt-authentication-bundle" -Register the bundle: -^^^^^^^^^^^^^^^^^^^^ +Register the bundle +~~~~~~~~~~~~~~~~~~~ Register bundle into ``config/bundles.php`` (Flex did it automatically): @@ -32,12 +32,12 @@ Register bundle into ``config/bundles.php`` (Flex did it automatically): Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; -Generate the SSL keys: -^^^^^^^^^^^^^^^^^^^^^^ +Generate the SSL keys +~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: bash +.. code-block:: terminal - $ php bin/console lexik:jwt:generate-keypair + $ php bin/console lexik:jwt:generate-keypair Your keys will land in ``config/jwt/private.pem`` and ``config/jwt/public.pem`` (unless you configured a different path). @@ -70,69 +70,80 @@ Configure the SSL keys path and passphrase in your ``.env``: pass_phrase: '%env(JWT_PASSPHRASE)%' # required for token creation token_ttl: 3600 # in seconds, default is 3600 -Configure your ``config/packages/security.yaml`` : +Configure application security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Make sure the firewall ``login`` is place before ``api``, and if -``main`` exists, put it after ``api``, otherwise you will encounter -``/api/login_check`` route not found.** +.. caution:: + + Make sure the firewall ``login`` is place before ``api``, and if + ``main`` exists, put it after ``api``, otherwise you will encounter + ``/api/login_check`` route not found. + +Symfony versions prior to 5.3 +............................. .. code-block:: yaml - # Symfony versions prior to 5.3 - security: - # ... - - firewalls: - login: - pattern: ^/api/login - stateless: true - json_login: - check_path: /api/login_check # or api_login_check as defined in config/routes.yaml - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - - api: - pattern: ^/api - stateless: true - guard: - authenticators: - - lexik_jwt_authentication.jwt_token_authenticator - - access_control: - - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } + # config/packages/security.yaml + security: + # ... + + firewalls: + login: + pattern: ~/api/login + stateless: true + json_login: + check_path: /api/login_check # or api_login_check as defined in config/routes.yaml + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + + api: + pattern: ~/api + stateless: true + guard: + authenticators: + - lexik_jwt_authentication.jwt_token_authenticator + + access_control: + - { path: ~/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ~/api, roles: IS_AUTHENTICATED_FULLY } + +Symfony 5.3 and higher +...................... .. code-block:: yaml - # Symfony 5.3 and higher - security: - enable_authenticator_manager: true - # ... - - firewalls: - login: - pattern: ^/api/login - stateless: true - json_login: - check_path: /api/login_check - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - - api: - pattern: ^/api - stateless: true - jwt: ~ - - access_control: - - { path: ^/api/login, roles: PUBLIC_ACCESS } - - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } - -Configure your routing into ``config/routes.yaml`` : + # config/packages/security.yaml + security: + enable_authenticator_manager: true + # ... + + firewalls: + login: + pattern: ~/api/login + stateless: true + json_login: + check_path: /api/login_check + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + + api: + pattern: ~/api + stateless: true + jwt: ~ + + access_control: + - { path: ~/api/login, roles: PUBLIC_ACCESS } + - { path: ~/api, roles: IS_AUTHENTICATED_FULLY } + +Configure application routing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml - api_login_check: - path: /api/login_check + # config/routes.yaml + api_login_check: + path: /api/login_check Usage ----- @@ -143,31 +154,30 @@ Usage ~~~~~~~~~~~~~~~~~~~ The first step is to authenticate the user using its credentials. - You can test getting the token with a simple curl command like this (adapt host and port): -Linux or macOS +Linux or macOS: -.. code-block:: bash +.. code-block:: terminal - curl -X POST -H "Content-Type: application/json" https://localhost/api/login_check -d '{"username":"johndoe","password":"test"}' + $ curl -X POST -H "Content-Type: application/json" https://localhost/api/login_check -d '{"username":"johndoe","password":"test"}' -Windows +Windows: .. code-block:: bash - curl -X POST -H "Content-Type: application/json" https://localhost/api/login_check --data {\"username\":\"johndoe\",\"password\":\"test\"} + C:\> curl -X POST -H "Content-Type: application/json" https://localhost/api/login_check --data {\"username\":\"johndoe\",\"password\":\"test\"} If it works, you will receive something like this: .. code-block:: json - { - "token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzQ3Mjc1MzYsInVzZXJuYW1lIjoia29ybGVvbiIsImlhdCI6IjE0MzQ2NDExMzYifQ.nh0L_wuJy6ZKIQWh6OrW5hdLkviTs1_bau2GqYdDCB0Yqy_RplkFghsuqMpsFls8zKEErdX5TYCOR7muX0aQvQxGQ4mpBkvMDhJ4-pE4ct2obeMTr_s4X8nC00rBYPofrOONUOR4utbzvbd4d2xT_tj4TdR_0tsr91Y7VskCRFnoXAnNT-qQb7ci7HIBTbutb9zVStOFejrb4aLbr7Fl4byeIEYgp2Gd7gY" - } + { + "token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzQ3Mjc1MzYsInVzZXJuYW1lIjoia29ybGVvbiIsImlhdCI6IjE0MzQ2NDExMzYifQ.nh0L_wuJy6ZKIQWh6OrW5hdLkviTs1_bau2GqYdDCB0Yqy_RplkFghsuqMpsFls8zKEErdX5TYCOR7muX0aQvQxGQ4mpBkvMDhJ4-pE4ct2obeMTr_s4X8nC00rBYPofrOONUOR4utbzvbd4d2xT_tj4TdR_0tsr91Y7VskCRFnoXAnNT-qQb7ci7HIBTbutb9zVStOFejrb4aLbr7Fl4byeIEYgp2Gd7gY" + } -Store it (client side), the JWT is reusable until its ttl has expired +Store it (client side), the JWT is reusable until its TTL has expired (3600 seconds by default). .. _2-use-the-token: @@ -185,7 +195,8 @@ See the :doc:`configuration reference ` document to enable query string parameter mode or change the header value prefix. Examples -^^^^^^^^ +~~~~~~~~ + See :doc:`Functionally testing a JWT protected api ` document or the sandbox application `Symfony4 `__) @@ -195,7 +206,7 @@ Notes ----- About token expiration -^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~ Each request after token expiration will result in a 401 response. Redo the authentication process to obtain a new token. @@ -205,20 +216,19 @@ case you can check `JWTRefreshTokenBundle `__. Working with CORS requests -^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is more of a Symfony2 related topic, but see `Working with CORS -requests <4-cors-requests>`__ document to get a quick explanation on -handling CORS requests. +This is more of a Symfony2 related topic, but see :doc:`Working with CORS requests ` +document to get a quick explanation on handling CORS requests. Impersonation -^^^^^^^^^^^^^ +~~~~~~~~~~~~~ For impersonating users using JWT, see https://symfony.com/doc/current/security/impersonating_user.html Important note for Apache users -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As stated in `this link `__ @@ -232,7 +242,7 @@ you should), please add those rules to your VirtualHost configuration : .. code-block:: apache - SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 Further documentation --------------------- @@ -241,12 +251,10 @@ The following documents are available: - :doc:`Configuration reference ` - :doc:`Data customization and validation ` -- :doc:`Functionally testing a JWT protected - api ` +- :doc:`Functionally testing a JWT protected api ` - :doc:`Working with CORS requests ` - :doc:`JWT encoder service customization ` - :doc:`Extending Authenticator ` - :doc:`Creating JWT tokens programmatically ` - :doc:`A database-less user provider ` -- :doc:`Accessing the authenticated JWT - token ` +- :doc:`Accessing the authenticated JWT token `