Microservice symfony, sécuriser les APIs avec Keycloak (Bearer token)

Frederic Leaux
4 min readJul 24, 2023

--

Bearer token authentification API avec Keycloak et Symfony

Dans cet article, nous allons voir mettre en place une sécurisation d’api REST Symfony avec Keycloak avec un Bearer token.

Avant de commencer, vous devez avoir un environnement Docker avec Keycloack. Si cela n’est pas le cas, vous pouvez suivre mon tuto dédié :

Pour permettre l’accès aux utilisateurs à nos micro-services, nous allons utiliser le standard JWT.

Commençons par installer le bundle lexik/jwt-authentication-bundle

composer require lexik/jwt-authentication-bundle

Nous utilisons la configuration sans BDD. Nous devons créer la class User src/Security/User.php :

<?php

namespace 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)
{
/* Ajoute le role user si user identifié
sinon c'est une génération du token avec un client ID / secret*/
if (array_key_exists('clientId', $payload)) {
$roles = [self::ROLE_CLIENT];
} else {
$roles = [self::ROLE_USER];
}
/* On récupère les roles du client qui porte le nom du micro-service */
/* !! CHANGE firstservice BY YOUR !! */
if (isset($payload['resource_access']) && isset($payload['resource_access']['firstservice'])) {
$roles = array_merge($roles, $payload['resource_access']['firstservice']['roles']);
}

return new static($username, array_key_exists('email', $payload) ? $payload['email'] : null, array_unique($roles));
}

public function getEmail(): ?string
{
return $this->email;
}

}

Le ROLE_CLIENT et le ROLE_USER sont définis dans la class. Pas besoin de les avoir dans Keycloack. Pour ajouter un autre ROLE, il faut le faire dans Keycloack :

1 — On va créer un client pour notre Micro-service. Dans l’exemple on le nomme “firstservice”

2 — On va sur l’onglet “Roles” du client que l’on vient de créer et on ajoute un role par exemple “ROLE_CAN_VIEW”. Attention il faut que le nom du role comme par ROLE_ pour que Symfony le set correctement.

Ajout d’un role au niveau d’un client

3- On assigne le role qu’on vient de créer à un client :

Assignation d’un role à un client

Et voila, lorsque l’on fait un call API avec un token, on voit dans le profile [mais aussi dans $this->getUser()] le role que l’on vient de créer et d’assigner à cette utilisateur.

Les roles de l’utilisateur remontent dans le profiler de Symfony

On va ensuite modifier la configuration de Lexik JWT dans config/package/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: sub

Ajouter dans le .env :

###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=b4d6f960e1b29fda3a5ba016b633d9b1
###< lexik/jwt-authentication-bundle ###

Dans Keycloak on récupère la public key (aller dans Keycloak admin puis realm settings > keys et clic sur Public Key . Copier le contenu et le mettre dans un fichier config/jwt/public.pem

-----BEGIN PUBLIC KEY-----
PASTE THE KEY HERE
-----END PUBLIC KEY-----

On va maintenant configurer le config/packages/security.yaml

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

# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/api/doc, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: [ ROLE_CLIENT, ROLE_USER ] }
- { path: ^/admin, roles: ROLE_USER }
- { path: ^/connect, roles: PUBLIC_ACCESS }
#- { path: ^/graphql, roles: [ ROLE_CLIENT, ROLE_USER ] }
# ...

On termine avec la configuration de Nelmio config/nelmio_api_doc.yaml

nelmio_api_doc:
documentation:
info:
title: My App
description: This is an awesome app!
version: 1.0.0
components:
securitySchemes:
Bearer:
type: http
scheme: bearer
bearerFormat: JWT
security:
- Bearer: []
areas: # to filter documented areas
path_patterns:
- ^/api(?!/doc$) # Accepts routes under /api except /api/doc

Voila, normalement en allant sur http://local.first-service.fr/api/doc vous devriez avoir un bouton “Authorize” qui vous permettera de saisir un token Bearer. Sans cela vous aurez une 401

{
"code": 401,
"message": "JWT Token not found"
}

Pour récupérer votre token, il faut faire un appel en POST sur l’url de Keycloak http://local.services-domain.fr/auth/realms/master/protocol/openid-connect/token

Voila pour la sécurisation via un Bearer token Keycloak.

Prochaine partie : Mise en place de l’oauth2 pour se connecter à un back office easy admin.

--

--