Tuto micro-services, sécurité et utilisateurs avec Symfony, nginx, et Keycloak

Yann Dardot
Reparcar
Published in
6 min readJun 22, 2021

Quand vous allez créer votre application en micro-service vous allez rencontrer plusieurs problèmes l’un d’eux est de savoir comment sécuriser vos services et gérer l’authentification des utilisateurs sur tous vos services.

Avec reparcar.fr nous avons rencontré ces problèmes, nos APIs sont développées avec Symfony 5, et api platform, mais ces solutions sont valables pour n’importe quels micro-services ou technos.

La meilleure sécurité: l’isolation

Pour sécuriser vos micro-services la meilleure solution est de faire en sorte que personne ne puisse les appeler de l’extérieur et d’avoir uniquement un seul point d’entrée cela permet d’ajouter une sécurité uniquement au point d’entrée, on appelle cela une Gateway.

Ici, nous ajoutons un préfixe sur nos URL avec le nom du micro-service par exemple pour un micro-service de livres on ajoute le préfixe “book” ce qui donne gateway.reparcar.fr/book/books pour avoir la liste des livres, la gateway va ensuite appeler notre micro-service de livres avec comme URL book.reparcar.fr/books.

Voilà dans la théorie, dans la pratique on va utiliser nginx pour configurer cette gateway, pour cela rien de plus simple, nginx nous permet d’utiliser des proxy_pass qui permet de rediriger des utilisateurs avec une règle.

Dans un premier temps créons le site gateway sur Nginx:

server {
listen 80;
server_name gateway.reparcar.fr;
}

Ensuite configurons le proxy pour qu’il pointe vers notre micro-service de livre:

server {
listen 80;
server_name gateway.reparcar.fr;
location /book {
rewrite /book/(.*) /$1 break;
proxy_pass http://book.reparcar.fr;
}
}

Avec Nginx il est possible de définir une règle avec une regex pour rediriger automatiquement tous les micro-services vers leurs sous-domaines, néanmoins il faut pour cela définir un DNS car le proxy pass ne permet pas la résolution d’URL dynamiques dans le proxy pass.

Et la sécurité dans tout ça ? Nous avons bien isolé nos micro-services mais on n’a encore rien sécurisé ici

OAuth2 et Jwt

Comment ça marche ?

Peu importe comment vous voulez appeler vos microservices (de serveur à serveur, de client à serveur ou même d’utilisateurs à serveurs), OAuth2 permet une méthode d’authentification.

Pour commencer prenons un exemple simple de client à serveur. Dans un premier temps le client va récupérer un token qu’il utilisera pour s’authentifier sur les différents micro-services

Et JWT dans tout ça ?

L’avantage de JWT est sur 2 points : la validation du token et le contenu de celui-ci.

Le premier avantage de l’utiliser est la validation : un JWT est généré avec une clé privée et peut-être validé grâce à une clé publique, il est impossible de générer ou modifier un token sans clé privée. Cela a un gros avantage, il nous permet donc de pouvoir valider le token sans faire d’appel dans le cas où on stocke la clé publique dans notre micro-service.

L’autre avantage d’utiliser JWT est qu’il contient des informations. Prenons un exemple simple :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Un JWT est composé de 3 parties (chaque partie est séparée par un point) le header, le payload, et la signature, on peut simplement lire le contenu du header et du payload avec une base64 décrypte.

Le header permet d’avoir les informations concernant la nature du token, ainsi que la méthode de validation dans notre exemple :

{
"alg": "HS256",
"typ": "JWT"
}

Le payload lui permet d’avoir les informations sur l’utilisateur qui a généré le token ou toute autre information qui pourrait être nécessaire, Attention ! il ne faut pas mettre d’informations critiques dans le payload du JWT car ces informations seront visibles par tous, dans notre exemple :

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

La signature permet de valider le token grâce à la clé publique.

Grâce à cette méthode on peut donc même authentifier des utilisateurs, des applications ou n’importe quel autre service. Tous nos micro-services pourront valider et récupérer les informations de l’utilisateur actuel sans avoir besoin de faire d’autres appels, ce qui est une bonne chose dans une architecture micro-service car le moindre appel peut causer des soucis de performances.

Pour pouvoir gérer Oauth2 et JWT il va donc falloir créer un micro-service et nous allons utiliser Keycloak pour ça.

Pourquoi Keycloak

Lors de nos recherches on nous avait conseillé d’utiliser Keycloak, mais nous ne connaissions pas Keycloak ou autre, et on aime bien faire les choses nous-même. On avait donc décidé de développer notre propre solution, mais on est vite tombé dans la complexité de gérer les utilisateurs et tout le processus oauth2.

Nous sommes donc partis sur Keycloak qui permet l’authentification par client et utilisateur, il permet aussi de faire une SSO, de gérer tous nos utilisateurs via interface ou API, en utilisant open id connect.

Et la gateway dans tout ça ?

Bravo, vous avez compris que la gateway doit être configurée pour gérer l’authentification

Quand notre client va faire une requête a notre micro-service il doit le faire avec un token qu’il a récupéré au préalable, ensuite la gateway va appeler notre micro-service Oauth avec le token pour le valider, une fois le token valider elle transmet l’appel au micro-service final avec le token et celui-ci pourra l’auto-valider et le décoder pour avoir les informations sur les utilisateurs.

Quelle est la configuration pour Nginx ?

Dans un premier temps il nous faut la configuration pour la route d’introspection

proxy_cache_path /var/cache/nginx/oauth keys_zone=token_responses:1m max_size=2m;server {
listen 80;
server_name gateway.reparcar.fr;
location /book {
rewrite /book/(.*) /$1 break;
proxy_pass http://book.reparcar.fr;
}
location = /_oauth2_token_introspection {
internal;
proxy_method GET;
proxy_set_header Authorization "$http_authorization";
proxy_pass http://oauth.reparcar.fr/introspect
proxy_cache token_responses;
proxy_cache_key $http_authorization;
proxy_cache_lock on;
proxy_cache_valid 200 10s;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
}
}

Maintenant on peut doit informer Nginx qu’on a besoin d’appeler cette route pour vérifier le token lorsqu’on appelle notre micro-service de livre pour cela :

proxy_cache_path /var/cache/nginx/oauth keys_zone=token_responses:1m max_size=2m;server {
listen 80;
server_name gateway.reparcar.fr;
location /book {
rewrite /book/(.*) /$1 break;
proxy_pass http://book.reparcar.fr;
auth_request /_oauth2_token_introspection;
}
location = /_oauth2_token_introspection {
internal;
proxy_method GET;
proxy_set_header Authorization "$http_authorization";
proxy_pass http://oauth.reparcar.fr/introspect
proxy_cache token_responses;
proxy_cache_key $http_authorization;
proxy_cache_lock on;
proxy_cache_valid 200 10s;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
}
}

Parfait maintenant Nginx va faire à notre place la requête pour valider le token avec notre micro-service oauth avant de le transmettre à notre micro-service de livre

Autovalidation et Sécuritée avec symfony 5

Maintenant que notre gateway est configuré, nous allons voir comment configurer un microservice d’API avec nginx.

Pour cela sur Symfony on va utiliser le bundle lexik/jwt-authentication-bundle.

Pour la configuration de lexik Jwt on doit mettre un database less user. Pour cela il faut créer la Class User :

<?phpnamespace App\Security;use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUser;final class User extends JWTUser
{
const ROLE_USER = 'ROLE_USER';
const ROLE_CLIENT = 'ROLE_CLIENT';
/** @var string */
private $email;
public function __construct(string $username, ?string $email, array $roles = [])
{
parent::__construct($username, $roles);
$this->email = $email;
}
/**
* {@inheritdoc}
*/
public static function createFromPayload($username, array $payload)
{
if (array_key_exists('clientId', $payload)) {
$roles = [self::ROLE_CLIENT];
} else {
$roles = [self::ROLE_USER];
}
if (isset($payload['resource_access']) && isset($payload['resource_access']['invoicing'])) {
$roles = array_merge($roles, $payload['resource_access']['invoicing']['roles']);
}
return new static($username, array_key_exists('email', $payload) ? $payload['email'] : null, array_unique($roles));
}
public function getEmail(): ?string
{
return $this->email;
}
public function getUserIdentifier(): string
{
return $this->getUsername();
}
}

À noter qu’ici la classe User est configurée pour marcher avec Keycloak. Donc le formatage du payload est fait en fonction de Keycloak.

Maintenant configurons le bundle, dans le security bundle :

security:
providers:
users_in_memory: { memory: null }
jwt:
lexik_jwt:
class: App\Security\User
enable_authenticator_manager: true
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: jwt
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator

Parfait, grâce à cette configuration Symfony est prêt pour pouvoir récupérer l’utilisateur, il est maintenant possible de faire un ->getuser() dans un contrôler ou encore d’utiliser les rôles security de Symfony.

Conclusion

La sécurité dans vos micro-services est une chose importante, pouvoir tout centralisé et que les micro-services restent indépendants est encore mieux.

Ce pour quoi cette configuration reste la plus optimum pour ce genre d’infrastructure.

L’association Nginx et Keycloak permet une configuration simple, utile et rapide à mettre en place.

--

--

Yann Dardot
Reparcar
Editor for

Développeur jusqu’au plus profond de mon âme.