Aller au contenu
BackJavaquarkusSécuritéJWT

Protéger le livre de comptes et le coffre-fort avec JWT dans Quarkus

Dans une taverne où chaque accès se mérite, la clé ne suffit plus. Avec JWT, l’identité s’efface au profit d’un jeton signé, vérifié à chaque passage. Une autre manière de penser la sécurité : plus fine, plus exigeante, et résolument moderne dans Quarkus.

Protéger le livre de comptes et le coffre-fort avec JWT dans Quarkus

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-jwt permet de vérifier les jetons
  • quarkus-security relie 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-issuer

Concrètement :

  • mp.jwt.verify.publickey.location désigne la clé publique utilisée pour vérifier la signature
  • mp.jwt.verify.issuer impose 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 :

  • accountant pour le livre de comptes
  • treasurer pour le coffre-fort

Deux éléments assurent la garde :

  • @SecurityScheme expose le mécanisme Bearer JWT dans la documentation
  • @RolesAllowed dé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_q7WKDq0M9OvLesC21Irb3tvUdOivgLz0IwG2Re43xnyT5vCoJm0sXLC8czuyfteY71c1SldTEtkwWvvcVo1nPabaztewnWEbCYDUPQYDDIYR5zGm4w
  • VAULT_TOKEN - ouvre le coffre (treasurer)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRhdmVybi1rZXkifQ.eyJpc3MiOiJ0YXZlcm4taXNzdWVyIiwiaWF0IjoxNzc1MDg4MDAwLCJleHAiOjE4OTM0NTYwMDAsInN1YiI6ImJvcmluIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYm9yaW4iLCJncm91cHMiOlsidHJlYXN1cmVyIl19.GUZJ3bsjD21LXlPBzrs0kxfJzkp822UfgEjN-MnTcrZnf4GpSW1HJrWNn153aeeZobRss2EGc_m3kCEmn0xA2uEwb7NsGytPv-tz2EFf4sDEJWo3SPu88L8yzTdLhyzMouZXkdMnoQ766COcXkhufuRa06WqCGfG6RyNs5W9IoIitS9usTmRcWli9uM9gxiRdS3Cpp3z_RrxtX3ZY5px6zjwGXDNCo_gbXH6GzEitoOmE5-5jF8nfpQvepaO4yxXwjW3M149eFwUygGtsM4BH7cWuDmnG1G7qE-eD0qCl-g824CgMzTik6xab6uS47RsZDWYIitwBl1mNCyNHGZbsg

Ces 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 jetons
  • quarkus-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 :

  • groups contient les rôles exploités par @RolesAllowed
  • issuer doit correspondre à celui attendu côté vérification
  • expiresAt impose 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/ledger

Le 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.

Sécurisez vos API avec Spring Security : JWT
Protéger une API est essentiel pour garantir la sécurité des données et prévenir les accès non autorisés. Dans cet article, découvrez comment sécuriser vos endpoints avec Spring Security et l’authentification par JSON Web Token.
À la découverte de Quarkus
Quarkus est un framework Java moderne et performant qui simplifie le développement d’applications cloud natives : découvrons-le au travers de la création d’une application exposant des micro-services REST.

Dernier