Dans cet article, je vais vous montrer comment créer et protéger une API REST avec SpringBoot 3 et Keycloak.
Pour cela, nous allons utiliser:
- Spring Boot 3.x
- Java 17.x
- Maven 3.x
- Keycloak 25.x
Installation et configuration de Keycloak
Nous aurons besoin de faire les actions suivantes:
- Installer Keycloak
- Créer un realm: myrealm
- Créer dans ce realm un client avec la configuration par défaut. client-id: demo
- Créer dans ce realm un utilisateur.
- Créer dans ce realm deux roles: user et admin
Ces différentes étapes sont expliquées pas à pas dans cet article sur l’installation et la prise en main de Keycloak.
Par la suite, il faudra attribuer le role correspondant à l’utilisateur selon le endpoint à tester.
API REST SpringBoot 3.x
Pour développer et sécuriser l’API REST avec SpringBoot 3.x, nous allons utiliser les dépendances Maven suivantes:
- spring-boot-starter-web
- spring-boot-starter-security
- spring-boot-starter-oauth2-resource-server
Pour générer le projet, vous pouvez utiliser spring initializr. Une fois généré, ouvrez le projet dans votre IDE préféré. Vous pouvez aussi télécharger directement le résultat final ici.
Nous allons avoir besoin de trois classes pour notre implémentation .
Une classe pour la configuration de la sécurité
@Configuration
@EnableWebSecurity //Enable spring security
@EnableMethodSecurity //Enable annotation based access control for methods
public class SecurityConfig {
@Autowired
JwtAuthConverter authConverter; // This JWT converter will extract the client and realm roles and put them into
// the format expected by spring security
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/").permitAll() //Access is public for this endpoint. No Authentication required
.anyRequest().authenticated() // Any other request requires authentication
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(authConverter) // The converter is used here.
)
);
return http.build();
}
}
Quelques explications sur cette classe:
- @Configuration: cette annotation signale à SpringBoot que notre classe est dédiée à la configuration de Bean
- @EnableWebSecurity: cette annotation active Spring Security qui est le module permettant la sécurisation de notre API REST.
- @EnableMethodSecurity Cette annotation permet de protéger les méthodes de notre projet SpringBoot avec des annotations.
- SecurityFilterChain: Ce bean définit les règles de sécurisation de notre API REST. Il spécifie que le endpoint “/” sera accessible sans authentification, tandis que pour tous les autres endpoints, il faudra être authentifié pour y accéder. Pour chaque requête HTTP, le bean SecurityFilterChain va:
- Intercepter la requête HTTP
- Extraire le jeton JWT
- Decoder le jeton JWT et s’assurer qu’il est valide
- Extraire les roles et les mettre sous le format attendu par Spring Security grâce à la classe JwtAuthConverter ci-dessous
Une classe pour extraire les rôles du jeton Keycloak et les mettre au bon format
@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private Logger log = LoggerFactory.getLogger(JwtAuthConverter.class);
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
@Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
private String oauth2ClientId; //Used to get the roles associated with this client from the token
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream.concat(
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
extractRoles(jwt).stream()
).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities);
}
private Collection<? extends GrantedAuthority> extractRoles(Jwt jwt) {
Set<String> roles = new HashSet<>();
// Extract roles from realm_access
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null && realmAccess.containsKey("roles")) {
roles.addAll((Collection<? extends String>) realmAccess.get("roles"));
}
// Extract roles from resource_access.client-id
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
if (resourceAccess != null && resourceAccess.containsKey(oauth2ClientId) ){
Map<String, Object> demoAccess = (Map<String, Object>) resourceAccess.get(oauth2ClientId);
if (demoAccess != null && demoAccess.containsKey("roles")) {
roles.addAll((Collection<? extends String>) demoAccess.get("roles"));
}
}
// Debugging extracted roles
log.debug("Extracted roles: {}", roles);
// Spring expects roles to start with the prefix ROLE_
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}
Cette classe sert de convertisseur pour le jeton JWT.
- Le méthode convert(Jwt jwt) est le point d’entrée. Elle prend en paramètre le jeton JWT, et retourne un objet contenant à la fois le jeton une liste d’objets GrantedAuthority représentant les rôles. Spring Security s’attend en effet à ce que chaque rôle soit représenté par un objet de type GrantedAuthority.
- La méthode extractRole(Jwt jwt) est appelée par le méthode convert(Jwt jwt). Elle extrait les rôles du realm et ceux du client à partir du jeton JWT reçu. Pour chaque rôle, cette méthode ajoute le préfixe ROLE_ car Spring Security s’attend à ce que le nom de chaque rôle commence par ce préfixe. Elle retourne une collection de GrantedAuthority, un pour chaque rôle.
Un controller pour exposer les endpoints
@RestController
public class DemoController {
@GetMapping
public String hello() {
return "hello - anonymous";
}
@GetMapping("/hello-user")
@PreAuthorize("hasRole('user')")
public String helloUser() {
return "hello user" ;
}
@GetMapping("/hello-admin")
@PreAuthorize("hasRole('admin')")
public String hello2() {
return "hello - admin";
}
@GetMapping("/token")
@PreAuthorize("hasRole('admin')")
public Map<String, Object> token(@AuthenticationPrincipal Jwt principal){
return principal.getClaims();
}
}
Ce controller SpringBoot expose des endpoints REST pour notre API.
- La méthode hello() expose le endpoint “/” . Ce dernier est accessible sans contrôle d’accès.
- La méthode helloUser() expose le endpoint /hello-user. Ce dernier est accessible par un utilisateur ayant le rôle user, et ce à cause de l’annotation @PreAuthorize(“hasRole(‘user’)”)
- La méthode token(@AuthenticationPrincipal Jwt principal) expose le endpoint /token. Elle prend en paramètre le jeton JWT, ce qui lui permet d’accéder à tous les claims du jeton. Ce endpoint est accessible par un utilisateur ayant le rôle admin.
Le fichier de configuration de l’application
Le fichier application.yaml a le contenu ci-dessous.
spring:
application:
name: keycloak-springboot3
security:
oauth2:
client:
registration:
keycloak:
client-id: demo
scope: openid
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/myrealm
jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
server:
port: 8081
- issuer-uri: ce paramètre donne l’URL du realm Keycloak qui sert à authentifier les utilisateurs.
- jwk-set-uri: ce paramètre indique à l’application où récupérer la clé publique de Keycloak afin de vérifier la signature du jeton JWT.
- client-id: dans notre implémentation, ce paramètre sert uniquement à dire à la classe JwtAuthConverter quelle clé json contient les rôles du client dans les claims du jetons JWT.
- Ce fichier configure également le nom de l’application ainsi que le port d’exposition.
Grâce à la librairie Spring OAuth2 etaux paramètres issuer-uri et jwk-set-uri, l’application est en mesure de valider la signature de chaque jeton JWT qu’elle reçoit.
Tester l’implémentation
Il faut:
- Démarrer Keycloak et s’assurer qu’il est configuré correctement (voir la section Installation de Keycloak ci-dessus)
$ cd keycloak-25.0.6 $ /bin/kc.sh start-dev
- Démarrer l’API SpringBoot.
$ cd keycloak-springboot3-api $ mvn: clean spring-boot:run
- Récupérer un jeton JWT après de Keycloak
$ curl -d "grant_type=password&username=user&password=user&client_id=demo" http://localhost:8080/realms/myrealm/protocol/openid-connect/token
- Utiliser ce jeton pour appeler les endpoints de l’API REST:
$ curl http://localhost:8081 # works without a token $ curl http://localhost:8081/hello-user # return 401 error $ curl -H 'Authorization: Bearer jwt_token_here' http://localhost:8081/hello-user # Return *hello user* if the token has role *user*. 403 error otherwise.
Conclusion
Dans cet article, nous avons vu comment créer pas à pas une API REST avec SpringBoot 3.x et le protéger par Keycloak. Vous pouvez retrouver l’intégralité du code dans ce repo gitlab. Si cet article vous a été utile, n’hésitez pas à le partager autour de vous. Vous pouvez me suivre sur LinkedIn pour être informé de mes articles et posts sur Keycloak et la cyber sécurité. A bientôt!
