Les torches crépitent dans la salle du trésor. Le cellier est déjà sous clé, mais le livre de comptes et le coffre-fort exigent mieux qu'une simple serrure.
Ici, une clé ne suffit plus. Ce qu’il faut, c’est une preuve.
Cette preuve prend la forme d’un jeton scellé : un JWT.
Un artefact que l’on ne forge qu’une fois, que l’on présente à chaque passage, et dont l’authenticité peut être vérifiée sans jamais révéler de secret.
Dans cet article, nous mettons en place cette mécanique dans Quarkus pour protéger deux accès sensibles :
- le livre de comptes (rôle
accountant) - le coffre-fort (rôle
treasurer)
Pour comprendre pleinement ce mécanisme, nous nous appuierons sur deux approches issues du code :
- une première où les jetons sont fournis afin d’en observer la vérification
- une seconde, où ils sont forgés à la demande via un login et une base JPA
Deux manières d’aborder un même principe, entre observation et mise en pratique, afin de mieux saisir comment le trésor reste fermé… même lorsque la porte semble ouverte.
Rappel rapide sur JWT
JWT (JSON Web Token) est un mécanisme d'authentification basé sur un jeton signé :
- Le client envoie un header
Authorization: Bearer <token>. - Le serveur ne fait qu’une chose : vérifier le jeton (signature, expiration, issuer).
Il ne connaît jamais le mot de passe et ne génère aucun token dans ce module. - Le token contient des claims (ex:
issuer,exp,groups). - Les rôles sont lus dans le token et appliqués via
@RolesAllowed.
Point clé à retenir : le serveur ne reçoit plus de mot de passe après le login.
Le mot de passe sert à obtenir un JWT, ensuite seul le token circule.
JWT avec jetons de démonstration
Premier niveau de défense : ici, on ne forge pas encore les jetons.
On les reçoit déjà scellés, prêts à l’usage.
C’est une approche simple, presque didactique : elle permet de tester rapidement les rôles et les accès sans se soucier du processus de création.
Les jetons sont fournis, signés à l’avance avec une clé privée de démonstration.
Le garde, lui, ne fait qu’une chose : vérifier le sceau.
Les dépendances
Pour mettre en place cette mécanique dans Quarkus, le module s’appuie sur quelques briques essentielles :
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
</dependencies>quarkus-smallrye-jwtpermet de vérifier les jetonsquarkus-securityrelie les rôles aux accès via@RolesAllowed
La configuration JWT
Ici, tout repose sur la capacité à reconnaître un jeton valide :
# Verification JWT (publique)
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=tavern-issuerConcrètement :
mp.jwt.verify.publickey.locationdésigne la clé publique utilisée pour vérifier la signaturemp.jwt.verify.issuerimpose l’origine attendue du jeton
La clé publique est accessible au garde.
La clé privée, elle, reste entre les mains de celui qui a forgé le sceau.
L’API protégée
Le livre de comptes et le coffre-fort sont gardés par une seule porte, mais chacun répond à une autorité différente :
Chaque accès est contrôlé par un rôle :
accountantpour le livre de comptestreasurerpour le coffre-fort
Deux éléments assurent la garde :
@SecuritySchemeexpose le mécanisme Bearer JWT dans la documentation@RolesAlloweddécide qui peut passer… et qui reste dehors
Le trésor derrière la porte
Derrière ces accès protégés, on retrouve des données de démonstration - juste assez pour comprendre ce qui est en jeu :
@ApplicationScoped
public class TreasuryService {
public LedgerResponse ledger() {
return new LedgerResponse("grand-ledger", List.of(
"Ale festival - 12 silver",
"Mead barrels - 7 silver",
"Royal tax - 3 silver"
));
}
public VaultResponse vault() {
return new VaultResponse("iron-vault", "sealed", List.of(
"Emerald chalice",
"Guild charter",
"Emergency gold stash"
));
}
}Le contenu importe peu ici.
Ce qui compte, c’est que l’accès dépend désormais du jeton présenté.
Jetons de démonstration
Pour mettre à l’épreuve le système, deux jetons sont fournis.
Valides jusqu’au 2030-01-01, chacun porte un rôle précis :
LEDGER_TOKEN- permet d’accéder au livre de comptes (accountant)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRhdmVybi1rZXkifQ.eyJpc3MiOiJ0YXZlcm4taXNzdWVyIiwiaWF0IjoxNzc1MDg4MDAwLCJleHAiOjE4OTM0NTYwMDAsInN1YiI6ImVsb3JhIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZWxvcmEiLCJncm91cHMiOlsiYWNjb3VudGFudCJdfQ.JwKFZD-c0fKaoGPpElS-BZ2IhKLOH6A8Tx3dBt1QhdECOClf2WtAtpi7epQRxQaPnxRL0RZ7Aq-tAd_bWFZ-uuq5l1IBKdcCOMSEOkoKRmHS_tGlKYq2ci6-RXjCOJpkXLr6JJ0tqsyR_G8qiF40TJeIsAnARKSHT8o4CS0ACblPahnyQ8ms35SGBwOkkgKgV0JmcQqdnohKXYb2UnT-EpJ7S1Fq5RKv_aX_q7WKDq0M9OvLesC21Irb3tvUdOivgLz0IwG2Re43xnyT5vCoJm0sXLC8czuyfteY71c1SldTEtkwWvvcVo1nPabaztewnWEbCYDUPQYDDIYR5zGm4wVAULT_TOKEN- ouvre le coffre (treasurer)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRhdmVybi1rZXkifQ.eyJpc3MiOiJ0YXZlcm4taXNzdWVyIiwiaWF0IjoxNzc1MDg4MDAwLCJleHAiOjE4OTM0NTYwMDAsInN1YiI6ImJvcmluIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYm9yaW4iLCJncm91cHMiOlsidHJlYXN1cmVyIl19.GUZJ3bsjD21LXlPBzrs0kxfJzkp822UfgEjN-MnTcrZnf4GpSW1HJrWNn153aeeZobRss2EGc_m3kCEmn0xA2uEwb7NsGytPv-tz2EFf4sDEJWo3SPu88L8yzTdLhyzMouZXkdMnoQ766COcXkhufuRa06WqCGfG6RyNs5W9IoIitS9usTmRcWli9uM9gxiRdS3Cpp3z_RrxtX3ZY5px6zjwGXDNCo_gbXH6GzEitoOmE5-5jF8nfpQvepaO4yxXwjW3M149eFwUygGtsM4BH7cWuDmnG1G7qE-eD0qCl-g824CgMzTik6xab6uS47RsZDWYIitwBl1mNCyNHGZbsgCes jetons sont déjà scellés.
Ils permettent d’observer le mécanisme en place, sans intervenir dans la forge.
JWT + JPA avec login
Deuxième niveau de défense : cette fois, les jetons ne sont plus fournis.
Ils sont forgés à la demande.
Pour accéder au livre de comptes ou au coffre-fort, il ne suffit plus de présenter un sceau valide :
il faut d’abord prouver son identité.
Les utilisateurs sont enregistrés en base, leurs secrets soigneusement conservés, et chaque passage par le portail d’authentification permet de générer un JWT signé, valable pour une durée limitée.
Ici, la taverne ne se contente plus de vérifier les sceaux.
Elle les crée.
Les dépendances
Pour mettre en place cette forge dans Quarkus, le module s’appuie sur plusieurs briques :
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-common</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
</dependencies>quarkus-smallrye-jwt: vérification des jetonsquarkus-smallrye-jwt-build: génération et signature- JPA + H2 : stockage des utilisateurs
elytron: gestion des mécanismes de sécurité (dont le hash des mots de passe)
La configuration
Ici, deux mondes coexistent :
- celui de la mémoire (les utilisateurs)
- celui du sceau (les jetons)
# Datasource H2 en memoire
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:tavern-jwt-jpa;DB_CLOSE_DELAY=-1
quarkus.datasource.username=sa
quarkus.datasource.password=sa
quarkus.hibernate-orm.database.generation=drop-and-create
# JWT verification + signature
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=tavern-issuer
mp.jwt.signer.key.location=privateKey.pem
smallrye.jwt.sign.key.location=privateKey.pem- la clé publique permet de vérifier les jetons
- la clé privée permet de les signer
Le garde vérifie.
La forge, elle, signe.
Les utilisateurs en base
Avant toute chose, il faut des identités.
Au démarrage, quelques comptes sont inscrits dans les registres de la taverne :
@ApplicationScoped
public class StartupService {
@Transactional
void init(@Observes StartupEvent event) {
if (TavernUser.count() > 0) {
return;
}
TavernUser accountant = new TavernUser();
accountant.username = "elora";
accountant.password = BcryptUtil.bcryptHash("ledger123");
accountant.role = "accountant";
accountant.persist();
TavernUser treasurer = new TavernUser();
treasurer.username = "borin";
treasurer.password = BcryptUtil.bcryptHash("vault123");
treasurer.role = "treasurer";
treasurer.persist();
}
}Chaque mot de passe est haché avec bcrypt avant d’être conservé.
Ainsi, même dans les archives, aucun secret n’apparaît en clair.
Le portail d’authentification
Pour obtenir un jeton, il faut passer par une porte dédiée :
@Path("/api/tavern/auth")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Tavern Auth", description = "Authentification et generation de JWT.")
public class AuthResource {
private final AuthService authService;
@Inject
public AuthResource(AuthService authService) {
this.authService = authService;
}
@POST
@Path("/login")
@Operation(summary = "Login", description = "Genere un JWT pour acceder aux ressources protegees.")
@APIResponse(
responseCode = "200",
description = "JWT genere",
content = @Content(schema = @Schema(implementation = LoginResponse.class))
)
@APIResponse(responseCode = "401", description = "Identifiants invalides")
public Response login(LoginRequest request) {
if (request == null || request.username() == null || request.password() == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
String token = authService.login(request.username(), request.password());
if (token == null) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
LoginResponse response = new LoginResponse(token, "Bearer", authService.expiresAtSeconds());
return Response.ok(response).build();
}
}Ce point d’entrée agit comme un véritable guichet :
- il vérifie les identifiants
- il refuse les imposteurs
- il délivre un jeton aux membres reconnus
Un échec renvoie un refus immédiat (401).
Un succès remet un sceau valide entre les mains du visiteur.
La forge du jeton
C’est ici que tout se joue.
@ApplicationScoped
public class AuthService {
private static final String ISSUER = "tavern-issuer";
private static final long TOKEN_TTL_SECONDS = 3600;
public String login(String username, String password) {
TavernUser user = TavernUser.find("username", username).firstResult();
if (user == null || user.password == null || !BcryptUtil.matches(password, user.password)) {
return null;
}
Instant now = Instant.now();
return Jwt.issuer(ISSUER)
.upn(user.username)
.preferredUserName(user.username)
.groups(Set.of(user.role))
.issuedAt(now)
.expiresAt(now.plusSeconds(TOKEN_TTL_SECONDS))
.sign();
}
public long expiresAtSeconds() {
return Instant.now().plusSeconds(TOKEN_TTL_SECONDS).getEpochSecond();
}
}Après vérification des identifiants :
- un jeton est créé
- signé avec la clé privée
- enrichi des rôles (
groups) - limité dans le temps (1 heure)
Quelques points clés :
groupscontient les rôles exploités par@RolesAllowedissuerdoit correspondre à celui attendu côté vérificationexpiresAtimpose une durée de vie stricte
Le jeton devient alors un passe d’accès temporaire, accepté par le reste du système.
Tests rapides
Une fois le jeton obtenu, le mécanisme redevient le même :
TOKEN=$(curl -s -X POST http://localhost:8080/api/tavern/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"elora","password":"ledger123"}' | jq -r .token)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/tavern/ledgerLe visiteur ne présente plus ses identifiants.
Il présente le sceau obtenu.
Conclusion
Le livre de comptes et le coffre-fort ne se protègent pas comme une simple réserve.
Une clé suffit pour une porte. Mais lorsqu’il s’agit de richesses, il faut des preuves.
Avec JWT, la taverne change de logique.
Ce n’est plus l’identité que l’on présente à chaque passage, mais un sceau : forgé une fois, vérifié à chaque entrée.
Le garde ne questionne plus.
Il observe le jeton, en vérifie l’origine, contrôle qu’il est encore valide… puis décide.
Mais ce modèle impose sa propre discipline.
Un jeton accepté reste valable jusqu’à son expiration.
Il ne se retire pas aisément, ne se conteste pas.
La sécurité ne repose donc pas uniquement sur la signature, mais sur un équilibre : durée de vie maîtrisée, renouvellement, et vigilance constante.
Ainsi, le livre demeure entre les mains légitimes,
et le coffre ne s’ouvre qu’à ceux qui portent le bon sceau.
La taverne, elle, peut prospérer en toute confiance.
Car ici, ce ne sont plus les mots qui ouvrent les portes…
mais la preuve qu’ils ont déjà été donnés.



