When developing microservices, a standalone authentication API is quite often the way to go. By separating authentication from the rest of the application, load is reduced on other components and userdata is not widely spread across the platform, decreasing the chance of leakage.
We will refer to every component or application, that connects to the authentication API as a client. These might be frontend applications directly, but also other API’s. The authentication API needs to function independently of any connected clients, and always needs to stay backward compatible.
Before starting, it is important to keep in mind that the authentication API will handle authentication ONLY and not all the logic that comes with managing users. For instance, sending a password reset email is NOT a task for the authentication API, as clients may want to send the email with a different structure or layout (quite often with a reset password link referring to the client itself). Clients may choose to handle password resets completely different, e.g. with or without confirmation email, administrator approval, etc. Do not fall into the trap of letting one API do all the work, exponentially increasing complexity as the number of clients grow.
Goal of this first part of the authentication API series is to build an authentication API Server, based on Json Web Tokens, where clients can authenticate users.
The weapon of choice is Symfony 4, because of the ease of development and because there are some bundles that have perfect JWT integration out of the box. For the sake of simplicity, configuration is kept as default as possible. Here we go..
I assume you know your way around symfony, so I’ll skip some trivialities every now and then.. You might want to read the post about setting up symfony projects.
You can’t have authentication without a user entity, so create a new user entity and let the User class implement the default UserInterface so your IDE can autocomplete all required methods. UserInterface has to be acquired by composer require symfony/security-bundle. The minimum requirement for authentication is an identifier field, such as email or username. (Of course you can rename this field to what fits your context).
<?php
namespace App\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private $id;
/**
* @var string
*/
private $identifier;
/**
* @var array
*/
private $roles;
/**
* @var string
*/
private $password;
/**
* User constructor.
*/
public function __construct()
{
$this->roles = [];
}
public function getId()
{
return $this->id;
}
public function getIdentifier()
{
return $this->identifier;
}
public function setIdentifier($identifier)
{
$this->identifier = $identifier;
return $this;
}
public function getRoles()
{
if (!$this->roles) {
return [];
}
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
// ...
}
public function getUsername()
{
return $this->identifier;
}
public function eraseCredentials()
{
// ...
}
}
To persist the user in the database, configure config/doctrine/User.orm.yml:
App\Entity\User:
type: entity
table: users
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
identifier:
type: string
password:
type: string
# Add all fields
With all that configuration done, including creating and configuring your database, it’s time to create our users table with bin/console doc:sch:up -f.
For the authentication API, we are going to implement Json Web Tokens using JWT-authenticationBundle. So composer require lexik/jwt-authentication-bundle and configure this bundle. Set your identifier field (might be email or username as well) as base authenticating field.
It might be necessary to set permissions on private.pem by chmod 744 config/jwt/private.pem
config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
user_identity_field: identifier
After adding LexikJWTAuthentiationBundle, change the User Entity to implement both default UserInterface and the JWTUserInterface, which will require the createFromPayload method.
If you haven’t done so already during configuration of JWT, add routing:
config/routes.yaml
user_authentication:
path: /authenticate
We will setup security a bit differently from default JWT.
Starting with providers, we’ll need to fetch users from 2 sources: database (anonymous users that need to be authenticated) and jwt (users that were authenticated on a previous call). Note: An alternative option could be to use a chained provider. Notice we are going to use a very simple plaintext password encoder for now.
security.yaml
security:
providers:
jwt_user_provider:
lexik_jwt:
class: App\Entity\User
entity_user_provider:
entity:
class: App\Entity\User
property: identifier
encoders:
App\Entity\User: plaintext
Configure firewalls:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
authenticate:
pattern: ^/authenticate
stateless: true
anonymous: true
provider: entity_user_provider
form_login:
check_path: /authenticate
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
access_control:
- { path: ^/authenticate, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
And there you have it, a secured API. If we authenticate a user, he will get a Json web token, which can be used to gain access to other microservices.
To be able to “re-construct” the user based on the JWT, we’ll add more user data to the token, using an EventSubscriber. With this, all relevant data for the user will be stored in the token. Important notice: try not to OVERLOAD your token with irrelevant data.
For the EventSubscriber:
services.yml
App\Event\Subscriber\JwtEventSubscriber:
arguments:
- '@serializer'
tags:
- { name: kernel.event_subscriber }
And the JWTEventSubscriber class:
<?php
namespace App\Event\Subscriber;
// Insert missing use statements.
class JwtEventSubscriber implements EventSubscriberInterface
{
/**
* @var SerializerInterface
*/
private $serializer;
/**
* JwtEventSubscriber constructor.
*/
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
public static function getSubscribedEvents()
{
return [
Events::JWT_CREATED => 'onTokenCreated'
];
}
/**
* @param JWTCreatedEvent $event
*/
public function onTokenCreated(JWTCreatedEvent $event)
{
$data = $event->getData();
$user = $event->getUser();
$userData = $this->serializer->serialize( $user, 'json', ['groups' => ['public']] );
$data = array_merge($data, json_decode($userData, true));
$event->setData($data);
}
}
As we have already configured serialization, add the entity to config/serialization/User.yml
App\Entity\User:
attributes:
id:
groups: [ 'public' ]
identifier:
groups: [ 'public' ]
roles:
groups: [ 'public' ]
Time for a little test. Add a user in mysql by using an insert query. Then use curl or postman to post username and password to /authenticate. If all went well, you should receive a Json web token. If you take this token and open it on https://jwt.io, you will notice the serialized user in the decoded version of the token. Ready to be deserialized and used by your clients. If not, it’s time to debug using some dumps.
For security reasons, it would be best to store passwords with encryption. Check out the excellent symfony docs on how to do this.
At this point, a client can authenticate a user and will receive a JWT Token when done so. With this JWT token, clients can interact with other clients in a secure way.
In the next post we will setup the API to allow refreshing the tokens.