Securing a Symfony API with JWTs
On this page
This tutorial shows you how you can secure endpoints with JSON Web Tokens in a Symfony powered API.
Note
The example uses the Curity Identity Server, but you can run the code against any standards-based authorization server.
Prerequisites
Curity Identity Server
For this tutorial you will need some configuration information about your Curity Identity Server. You'll need the Issuer
value that the server uses and the JWKS endpoint (the endpoint which exposes the public keys used to verify the tokens).
The JWKS value can be found at the metadata endpoint exposed by the server. The metadata endpoint can be found at
{issuerUri}/.well-known/openid-configuration
.
In this tutorial the following values will be used for the issuer and JWKS URIs:
Parameter Name | Value in tutorial |
---|---|
OpenID Provider Issuer URI | https://idsvr.example.com/oauth/anonymous |
JWKS URI | https://idsvr.example.com/oauth/anonymous/jwks |
You must also know the client_id that will be used to issue the Access Tokens, as it is needed to properly verify the audience claim.
If you need to configure a new client, follow the tutorial here: Setup a Client
In this tutorial the value demo-client
will be used for the client_id.
The client uses Pairwise Pseudonymous Identifier by default
By default in the Curity Identity Servers clients have the Pairwise Pseudonymous Identifiers (or PPID) option enabled. PPIDs
are a way of increasing privacy of your users. The tokens issued to a client do not use the user's ID, but instead a pseudonymous,
opaque ID. This means that you'll see a random string in the sub
claim of your tokens. If, for some reason, you need
the original user ID in this claim, turn this feature off. We highly recommend keeping this feature on though.
This tutorial builds on the configuration setup in the Getting started guide. Make sure to complete the steps outlined there before you continue.
PHP and composer installed
Make sure that you have PHP and composer installed in your system as they'll be used in the tutorial. You can also install
the symfony
CLI to have an easier access to some commands.
Create a Symfony API
Initialize a Symfony app
Create the new app by following these steps:
- Setup a new Symfony application, by calling the following command:.
symfony new secured-app
- Install dependencies.
Enter the newly created directory and run the following command to install the required dependencies.
composer require spomky-labs/lexik-jose-bridge:">=3.0.2" web-token/jwt-signature-algorithm-rsa php-http/httplug-bundle php-http/curl-client nyholm/psr7 php-http/cache-plugin annotations
Select y
when prompted to use composer recipes.
So what exactly are these?
lexik-jose-bridge
is a bundle which joins together two awesome bundles that will make most of the work for us. Note that the bridge bundle is required in at least version 3.0.2. In this tutorial we're using functionality that was introduced in this version so this is the minimum one that you should include. The essential bundles included by this one are:LexikJWTAuthenticationBundle
which adds possibilities for securing a Symfony app with JWTsweb-token/jwt-bundle
which uses thejwt-framework
to handle all things related to JWTs (like key management, signature validation, claims validation, etc.)
web-token/jwt-signature-algorithm-rsa
adds support for the RSA family of signature algorithms. Should you need other algorithms you have to change this dependency to a proper one. Have a look here to check the [list of supported algorithms] (https://web-token.spomky-labs.com/the-components/signed-tokens-jws/signature-algorithms) and their respective packages.php-http/httplug-bundle
,php-http/curl-client
,nyholm/psr7
are bundles and libraries needed to download the signature keys from the Curity Identity Server. You can exchange thecurl-client
library for any HTTP client that is supported byhttplug
. You can change thenyholm/psr7
library for any other PSR7 implementation.php-http/cache-plugin
adds caching mechanism so that the keys are not downloaded from Curity at each request.annotations
enables support for configuring routes with annotations.
Let's move on to creating an endpoint.
-
In the
src/Controller
directory add anApiController.php
file. -
Add the following to the
ApiController.php
file:
<?phpnamespace App\Controller;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\Routing\Annotation\Route;use Symfony\Component\Security\Core\User\UserInterface;class ApiController extends AbstractController{/*** @Route("/api", methods={"GET"})*/public function handleGetApi(UserInterface $user){$data = ['message' => 'Hello from ' . $user->getUsername()];return $this->json($data, 200);}}
The endpoint returns a simple JSON structure with the username of the user, which will be taken straight from the incoming JWT.
- Let's start the app:
symfony server:start
You can now open the browser and go to http://localhost:8000/api
to call your API. For now you'll see an error as we're
trying to inject a UserInterface
to the method, but there are no firewalls configured that will provide the controller
with a user object.
Secure Endpoints with JWTs
Symfony gives you some cool features for securing endpoints in your application. The LexikJWTAuthenticationBundle
adds
further functionality so that using JWTs for this task is much easier. In this part you'll learn how to configure the
bundles so that the endpoints are protected and the incoming JWTs are properly validated.
New to JSON Web Tokens?
Have a look at this article if JWTs are a new thing for you.
Note on Opaque tokens
It is a common practice for an Authorization Server to issue opaque access tokens instead of JWTs. As a matter of fact, this is also the default behaviour of the Curity Identity Server. Using opaque tokens in the outside world is more secure as no data can be read from the token (as is the case with JWTs). However, it's more convenient for an API to use JWTs as access tokens - you'll usually want your service to have access to all the data carried in a JWT. That's why at Curity we recommend using the Phantom token or the Split token approach, where an API gateway is responsible for exchanging the opaque token for a JWT, which is then sent together with the request to the services handling the API request.
In this tutorial we assume that you either use JWTs as access tokens for your API or use one of the mentioned approaches, so that the microservice always deals with a JWT, never an opaque token.
The configuration
- Configure the HTTP client for downloading the public keys. In the
config/packages
directory open thehttplug.yaml
file. Replace the contents in the file with the following:
httplug:plugins:retry:retry: 1cache: # We use the cache plugincache_pool: 'cache.app' # We use the PSR-6 Cache service of the applicationconfig:default_ttl: 1800 # TTL set to 30 mindiscovery:client: 'auto'clients:app:http_methods_client: trueplugins:- 'httplug.plugin.content_length'- 'httplug.plugin.redirect'- 'httplug.plugin.cache'
- In the same directory, in the
lexik_jwt_authentication.yaml
file add the following setting:
lexik_jwt_authentication:# ...user_identity_field: sub
We want the bundle to use the sub
claim from the token as the identity of the user.
- Still in the same directory, open the
spomky_labs_lexik_jose_bridge_bundle.yaml
file, and replace the contents with the following:
lexik_jose:ttl: 3600key_index: 0server_name: 'https://idsvr.example.com/oauth/anonymous'audience: 'demo-client'key_set_remote:type: 'jku'url: 'https://idsvr.example.com/oauth/anonymous/jwks'signature_algorithm: "RS256"jose:jku_factory:enabled: trueclient: 'httplug.client.app' # The Httplug clientrequest_factory: 'Psr\Http\Message\RequestFactoryInterface'
The ttl
and key_index
settings are mandatory, but are not used for token verification, so you can put any value there.
The server_name
must have the value of your Authorization Server's issuer. This value will be verified in the incoming
tokens, as well as the aud
claim, which has to have the value of the audience
setting. The key_set_remote
tells the
library where the keys should be downloaded from. The signature_algorithm
should be the value of the alg
claim that
will be used in the incoming tokens.
Here we also add configuration for the jwt-framework
bundle, that tells it which http client should be used to download
the public keys.
- Finally open the
security.yaml
file from the same folder and replace the contents with the following:
security:providers:jwt:lexik_jwt: ~firewalls:api:provider: jwtpattern: ^/apistateless: trueguard:authenticators:- lexik_jwt_authentication.jwt_token_authenticatoraccess_control:- {path: ^/api, roles: IS_AUTHENTICATED_FULLY }
Here we tell Symfony that the path /api
should be secured with a JWT authenticator from the LexikJWTAuthenticationBundle
.
We also use a user provider from this bundle which loads users using data found inside of Access Tokens.
Have a look at the bundle's documentation
to check how this provider can be adjusted.
- Test the solution
Make a curl request to the secured endpoint:
curl -i http://localhost:8000/api
You should see a 401 response, as you haven't sent a token.
To obtain a valid JWT you can use the online tool OAuth Tools which is a powerful tool to explore OAuth and OpenID Connect. You can easily add configuration of your Curity Identity Server and use any flow to generate a valid access token. If you're not sure how to create a JWT token using OAuth flows have a look at the Code Flow tutorial.
Once you have the token, make a request like this:
curl -i http://localhost:8000/api -H "Authorization: Bearer eyJ0e...aOCg"
This time you should see a 200 response and some JSON data.
Adding more security to the endpoint
The bundle used here validates the signature of the JWT as well as some basic claims: issuer, audience, issued at and expiration. If you want to validate other claims you can use one of the two available options:
Requiring presence of claims
To require that certain claims are present in the token, add the following setting to the spomky_labs_lexik_jose_bridge_bundle.yaml
file:
lexik_jose:# ...mandatory_claims:- 'jti'- 'myClaim'
A 401 response will be returned if the incoming token does not contain the listed claims.
Validating claims with a custom validator
To add a custom claim validator follow these steps:
- Create a
Security
directory in thesrc
directory, and add aMyClaimChecker.php
file inside. - Paste the following code into the file:
<?phpnamespace App\Security;use Jose\Component\Checker\ClaimChecker;use Jose\Component\Checker\InvalidClaimException;class MyClaimChecker implements ClaimChecker{public function checkClaim($value): void{if ($value !== 'someConcreteValue') {throw new InvalidClaimException("This is a wrong value for a claim.", "myClaim", $value);}}public function supportedClaim(): string{return "myClaim";}}
This class has to implement the ClaimChecker
interface and should throw an InvalidClaimException
if the value of the
claim is not valid. This class will be registered as a standard Symfony service, so you can inject any other service you would
need to verify the claim.
- Next, register the service. In the
config/services.yaml
add the following service definition:
services:# ...App\Security\MyClaimChecker:tags:- {name: 'jose.checker.claim', alias: 'my_claim_checker' }
- Add the following to the
spomky_labs_lexik_jose_bridge_bundle.yaml
file:
lexik_jose:# ...claim_checked:- 'my_claim_checker'
That's it. Now the custom claim value will be checked as well.
Note that this custom claim checker will raise an exception only if the value of the claim is present and different from
the one in the validator. If you want to also ensure that the claim is present at all you should also add the claim's name
to the mandatory_claims
configuration option of the lexik-jose-bridge bundle.
Conclusion
Securing endpoints with JWTs in Symfony is easy and can be achieved with adding a few lines of configuration to your project. Additional verification of claims can be achieved just with a proper implementation of an interface. This together gives you a powerful tool to secure your APIs with JSON Web Tokens.
You can check out the complete code for this tutorial here: Example for JWT Validation in Symfony on GitHub
Here are links to the documentations of the different projects used in this tutorial:
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial