Chapitre API RESTful avec Quarkus
Intégration de l'authentification JWT avec Quarkus
Le but de cet exercice est de mettre en place la sécurité JWT. Nous allons créer une page web qui permet de s'authentifier, afficher les tokens de vérification et de refresh, de lancer un refresh de token et tester l'authentification.
Prérequis
Avant de commencer cet exercice, assurez-vous d'avoir les éléments suivants installés sur votre système :
- JDK 11 ou supérieur
- Apache Maven
- Un éditeur de texte ou une IDE Java (comme Eclipse, IntelliJ IDEA, ou Visual Studio Code)
Création de la page de login
Dans le projet frontend-monster-adoption-store
, nous allons créer une page web appelée login.html
qui va permettre à l'utilisateur de s'authentifier.
Si l'authentication réussit, les tokens de vérification et de refresh seront affichés dans la page.
Deux autres boutons seront présents pour lancer un refresh de token et pour tester l'authentification.
Commencez par créer le fichier login.html
dans le répertoire src/main/resources/META-INF/resources/
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form id="login-form" action="/login/form" method="post">
<label for="username">Username</label>
<input type="text" id="username" name="username" />
<label for="password">Password</label>
<input type="password" id="password" name="password" />
<button type="submit">Login</button>
</form>
<h2>Tokens</h2>
<div id="tokens">
<div id="access-token">{access_token}</div>
<div id="refresh-token">{refresh_token}</div>
</div>
<h2>Refresh token</h2>
<button id="refresh-token-button" onclick="refreshToken()">
Refresh token
</button>
<button id="test-auth-button" onclick="authToken()">
Test authentication
</button>
<script lang="javascript" src="/js/login.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form id="login-form" action="/login/form" method="post">
<label for="username">Username</label>
<input type="text" id="username" name="username" />
<label for="password">Password</label>
<input type="password" id="password" name="password" />
<button type="submit">Login</button>
</form>
<h2>Tokens</h2>
<div id="tokens">
<div>
<label for="access-token">Access token</label>
<textarea cols="120" rows="10" id="access-token">
{access_token}</textarea
>
</div>
<div>
<label for="refresh-token">Refresh token</label>
<textarea cols="120" rows="10" id="refresh-token">
{refresh_token}</textarea
>
</div>
</div>
<h2>Refresh token</h2>
<button id="refresh-token-button" onclick="refreshToken()">
Refresh token
</button>
<button id="test-auth-button" onclick="authToken()">
Test authentication
</button>
<script lang="javascript" src="/js/login.js"></script>
</body>
</html>
Nous allons maintenant créer le fichier login.js
dans le répertoire src/main/resources/META-INF/resources/js/
const loginForm = document.getElementById("login-form");
const accessToken = document.getElementById("access-token");
const refreshToken = document.getElementById("refresh-token");
const refreshTokenButton = document.getElementById("refresh-token-button");
const testAuthButton = document.getElementById("test-auth-button");
function refreshToken() {
console.log("Refreshing token");
fetch("/login/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + refreshToken.innerText,
},
})
.then((response) => response.json())
.then((data) => {
console.log(data);
accessToken.innerText = data.access_token;
refreshToken.innerText = data.refresh_token;
});
}
function authToken() {
console.log("Testing authentication");
fetch("/login/auth", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + refreshToken.innerText,
},
}).then((response) => {
if (response.status === 200) {
alert("Authentication successful");
} else {
alert("Authentication failed");
}
});
}
Ajouter les dépendances
Nous devons désormais ajouter l'extension nécessaire à Quarkus pour supporter les authentifications avec JWT.
Voici trois liens utiles :
- Quarkus JWT Extension
- Quarkus JWT Extension Documentation
- Quarkus JWT Extension Example
- Quarkus build JWT tokens
Ajouter les extensions smallrye-jwt
et smallrye-jwt-build
avec la commande suivante :
quarkus extension add smallrye-jwt,smallrye-jwt-build
Création des endpoints
Nous allons maintenant créer la ressource REST qui alimente la page de login et les différentes actions.
Pour ce faire, nous allons créer une nouvelle classe LoginResource
dans le package com.byoskill.controllers
.
Copiez-coller le code suivant dans la classe LoginResource
:
package com.byoskill.auth.controllers;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.mutiny.Uni;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import java.util.Arrays;
import java.util.HashSet;
@Path("/login")
public class LoginResource {
public static final String ACCESS_TOKEN = "access_token";
public static final String REFRESH_TOKEN = "refresh_token";
@Inject
Template login;
@Inject
JsonWebToken jwt;
public LoginResource() {
}
// This endpoint generates a JWT token if the username is test and the password is password.
@POST
@Path("/form")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Response authenticate(
@FormParam("username") final String username,
@FormParam("password") final String password,
@Context final HttpHeaders request) {
return Response.ok(login.instance()
.data(ACCESS_TOKEN, "invalid")
.data(REFRESH_TOKEN, "invalid"))
.build();
}
@POST
@Path("/refresh")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
@RolesAllowed("User")
public Uni<TemplateInstance> refreshToken(@Context final HttpHeaders request) {
return get(request);
}
@PermitAll
@GET
@Produces(MediaType.TEXT_HTML)
public Uni<TemplateInstance> get(@Context final HttpHeaders request) {
return Uni.createFrom()
.item(login.instance()
.data(ACCESS_TOKEN, request.getHeaderString("access_token"))
.data(REFRESH_TOKEN, request.getHeaderString("refresh_token")));
}
@GET
@Path("/test")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
@RolesAllowed("User")
public Response testAuth(@Context final SecurityContext ctx) {
if (jwt.containsClaim("microservice") && jwt.getClaim("microservice").equals("auth-service")) {
return Response.ok(getResponseString(ctx)).build();
} else {
return Response.status(Response.Status.UNAUTHORIZED).entity("Cannot check the access token").build();
}
}
private String getResponseString(final SecurityContext ctx) {
final String name;
if (null == ctx.getUserPrincipal()) {
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName();
}
return String.format("hello + %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
}
private boolean hasJwt() {
return null != jwt.getClaimNames();
}
}
Ajout des propriétés pour la génération du JWT
Nous devons ajouter quelques propriétés dans le fichier application.properties
pour générer le JWT.
smallrye.jwt.sign.key.location=privateKey.pem
smallrye.jwt.encrypt.key.location=publicKey.pem
quarkus.native.resources.includes=publicKey.pem,privateKey.pem
- Nous définissons l'emplacement de la clé publique pour pointer vers un emplacement de classe publicKey.pem. Nous ajouterons cette clé dans la partie B, Ajout d'une clé publique.
- Nous définissons l'émetteur comme étant la chaîne URL https://example.com/issuer.
- Nous incluons la clé publique en tant que ressource dans l'exécutable natif.
Nous devons également créer un fichier publicKey.pem
dans le répertoire src/main/resources
.
Utilisez ce fichier par exemple :
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----
Ou vous pouvez utiliser le site jwt.io pour générer une clé publique
Pour la génération nous aurons également besoin d'une clé privée comme celle-ci dans privateKey.pem
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----
Modification de l'endpoint d'authentification pour la génération du token JWT
Nous allons modifier l'endpoint /login/form
pour générer le token JWT. Il générera un token si le username est test
et le mode de passe est password
.
@POST
@Path("/form")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Response authenticate(
@FormParam("username") final String username,
@FormParam("password") final String password,
@Context final HttpHeaders request) {
if ("test".equals(username) && "password".equals(password)) {
final String token =
Jwt.issuer("https://example.com/issuer")
.upn("jdoe@quarkus.io")
.groups(new HashSet<>(Arrays.asList("User", "Admin")))
.claim(Claims.birthdate.name(), "2001-07-13")
.claim("microservice", "auth-service")
.sign();
return Response.ok(login.instance().data("access_token", token)
.data("refresh_token", token)).build();
}
return Response.ok(login.instance()
.data(ACCESS_TOKEN, "invalid")
.data(REFRESH_TOKEN, "invalid"))
.build();
}
Tester l'authentification
Pour vérifier les tokens, il faut déclarer la clé publique et le service responsable de l'émission du token dans le fichier application.properties
.
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://example.com/issuer
Insérer le username test
et le mot de passe password
dans le formulaire pour obtenir un token d'authentification.
Vous pouvez ensuite tenter de vous authentifier avec le bouton associé.
N'hésitez pas à jouer avec les claims et les roles pour comprendre le comportement de JWT.
Exercice facultatif : utiliser la propriété mp.jwt.verify.issuer
plutot qu'une constante.