1290 views
keycloak spring boot ======= Le but de ce TP est de démontrer l'intégration de keycloak avec une application Spring Boot. Cela consiste à: 1. Démarrer keycloak 2. Configurer un nouveau realm (domaine) keycloak 3. Configurer deux utilisateurs keycloak (un utilisateur avec les droits USER, un avec le rôle ADMIN) 4. Créer une application SpringBoot simple 5. Adapter ce tutoriel à votre propre back Dans le passé, la sécurisation des applications Spring Boot avec Keycloak impliquait l'utilisation de l'adaptateur Spring Keycloak, qui s'appuyait sur l'adaptateur WebSecurityConfigurerAdapter. Cependant, le WebSecurityConfigurerAdapter a été supprimé de la dernière version de Spring Security. Keycloak a officiellement annoncé la suppression de ses adaptateurs sur son site web. Heureusement, Spring Security fournit depuis longtemps un support intégré robuste pour OAuth et OIDC. Par conséquent, il n'est plus nécessaire de maintenir un adaptateur spécifique à Keycloak. Les capacités inhérentes de Spring Security gèrent efficacement OAuth et OIDC, ce qui simplifie l'intégration avec Keycloak et élimine la dépendance à l'égard des adaptateurs spécifiques à Keycloak. Il y a généralement deux façon d'intégrer keycloak avec spring, juste pour l'authentification ou pour la gestion des autorisations. Nous nous placerons dans ce deuxième cas. Nous aprtirons du principe qu'un client de notre service utilise keycloak pour créer un token JWT qu'il nous enverra dans un entête http pour prouver son identité. ## Etape 1: Démarrer Keycloak Depuis un terminal, lancez Keycloak avec la commande suivante : ```bash docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev ``` Cela va démarrer Keycloak exposé sur le port local 8080. Elle créera également un utilisateur admin initial avec le nom d'utilisateur *admin* et le mot de passe *admin*. ## Etape 2: création d'un nouveau realm (domaine) Nous avons tendance à créer un domaine pour chaque application ou pour un ensemble d'application qui partagerons les mêmes utilisateurs et rôles. ### Login to the admin console Allez dans la [console d'adminitration](http://localhost:8080/admin/master/console/) Keycloak et connectez-vous avec le nom d'utilisateur et le mot de passe *admin:admin* ### Création d'un domaine (*realm*) Un domaine (*realm*) dans Keycloak est l'équivalent d'un *tenant*. Elle permet de créer des groupes isolés d'applications et d'utilisateurs. Par défaut, il y a un seul domaine dans Keycloak appelé master. Il est dédié à la gestion de Keycloak et ne doit pas être utilisé pour vos propres applications. #### Créons notre premier domaine. Ouvrez la console d'administration de Keycloak Passez la souris sur le menu déroulant dans le coin supérieur gauche où il est indiqué Master, puis cliquez sur Add realm (Ajouter un domaine (create Realme)) Remplissez le formulaire avec les valeurs suivantes : ``` Name: myspringbootapprealm ``` Cliquez sur Créer ### Création d'un client Tojours sur la [console d'adminitration](http://localhost:8080/admin/master/console/). Basculez sur le domaine que vous ne venez de créer. (en choisissant le domaine *myspringbootapprealm*), créer un nouveau client (menu de gauche **Clients** -> create client) puis metter les information suivantes ``` Client ID: myspringbootapp ``` puis *next*, *next* dans la partie **Valid redirect URIs** : 'http://localhost:8082/*' sans les guillemets. C'est l'url de votre application que vous souhaitez sécuriser. Cliquer sur le bouton save pour finaliser la création de votre client. C'est globalement dans cette partie client que l'on peut configurer tout le flow de l'authentification. ## Etape 3: création de deux utilisateurs et leur rôle associé ### Création de deux utilisateurs Initialement, il n'y a pas d'utilisateurs dans un nouveau domaine, alors créons-en deux : Basculez sur le domaine que vous ne venez de créer à l'étape 2. Ouvrez la console d'administration Keycloak http://localhost:8080/admin/master/console/#/myspringbootapprealm/users Cliquez sur **Users** (menu de gauche) Cliquez sur *Ajouter un utilisateur* (coin supérieur droit du tableau) Remplissez le formulaire avec les valeurs suivantes : ``` Nom d'utilisateur : myuser Prénom : Votre prénom Nom de famille : Votre nom de famille ``` Cliquez sur **Enregistrer** L'utilisateur devra définir un mot de passe initial pour pouvoir se connecter. Pour ce faire, il faut Cliquez sur **Credentials** (en haut de la page). Remplissez le formulaire "Set Password" avec un mot de passe. Cliquez sur **ON** à côté de Temporaire pour éviter de devoir mettre à jour le mot de passe lors de la première connexion. Répéter cette opération pour un deuxième utilisateur. ### Création de deux roles Créer deux rôles, dans le menu gauche Realm role -> create ROLE. Nous créons donc un rôle **USER** et un role **ADMIN** ⚠️ attention la casse est importante ### Affectaction des roles aux utilisateurs Aller dans la section groupe, créer deux groupes, un groupe *ADMINS* et un groupe *USERS*. Affecter un utilisateur au groupe admin et un au groupe user. Affecter le role **USER** au groupe *USERS* et **ADMIN** au groupe *ADMIN*. ### Test de la bonne configuration de keycloak ```bash curl --location 'http://localhost:8080/realms/myspringbootapprealm/protocol/openid-connect/token' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'username=user1' --data-urlencode 'password=test1' --data-urlencode 'grant_type=password' --data-urlencode 'client_id=myspringbootapp' |jq ``` Cette commande devrait vous renvoyer un token. :warning: N'oubliez pas de mettre à jour le login et mdp dans la commande en fonction de l'utilisateur et mdp que vous avez créé. ## Etape 4: Créer une application SpringBoot simple Aller sur https://start.spring.io/ Configurer ce projet en choisissant comme dépendances - **Spring Web WEB** *Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.* - **Spring Security SECURITY** *Highly customizable authentication and access-control framework for Spring applications.* - **Thymeleaf TEMPLATE ENGINES** *A modern server-side Java template engine for both web and standalone environments. Allows HTML to be correctly displayed in browsers and as static prototypes.* De manière optionnelle, (si vous voulez toujours accéder à une base de données) - **Spring Data JPA SQL** *Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.* Les dépendances du pom.xml peuvent ressembler à cela. ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> ``` Importer ce projet dans votre IDE. Tester l'exécution de votre programme sans sécurité en ajoutant un controleur rest tout simple. Si vous avez choisi l'utilisation de datajpa, il faut nécessairement configurer une datasource dans votre fichier *application.properties* ``` # change the port to avoid the use of the keycloak port server.port=8082 spring.h2.console.enabled=true spring.h2.console.path=/h2 spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.datasource.platform=h2 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.datasource.initialization-mode=always ``` Il faudra alors aussi ajouter la dépendance à la base **H2** à l'exécution. Pour ce faire ajoutez la dépendance suivante dans votre fichier *pom.xml* section *dependencies*. ```xml <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> ``` ### Ajout de la sécurité (intégration de keycloak) Cela vous permet de configurer directement l'utilisation de Keycloak dans votre fichier *application.properties* ```txt spring.security.oauth2.resourceserver.jwt.issuer-uri: http://localhost:8080/realms/myspringbootapprealm spring.security.oauth2.resourceserver.jwt.jwk-set-uri: http://localhost:8080/realms/myspringbootapprealm/protocol/openid-connect/certs # change the port to avoid the use of the keycloak port server.port=8082 ``` Puis ajouter la classe suivante dans le répertoire config: ```java package sample.data.jpa.config; import java.util.Collection; import java.util.List; import java.util.Map; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.web.SecurityFilterChain; /** * @author : Olivier Barais * @created : 20-10-2023 */ @Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeHttpRequests(registry -> { try { registry.requestMatchers("/").permitAll() .requestMatchers("/index").hasRole("user") .requestMatchers("/admin").hasRole("admin") .anyRequest().authenticated(); } catch (Exception e) { e.printStackTrace(); } }).oauth2ResourceServer(oauth2Configurer -> oauth2Configurer .jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwt -> { Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access"); Collection<String> roles = realmAccess.get("roles"); List<SimpleGrantedAuthority> grantedAuthorities = roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList(); grantedAuthorities.forEach(e -> { System.err.println(e.getAuthority()); }); return new JwtAuthenticationToken(jwt, grantedAuthorities); }))); return httpSecurity.build(); } } ``` Cette classe est le fichier de configuration de votre application, il utilise l'API de Spring Security. Vous verrez que dans le code, je transforme les rôles issus de keycloak en les préfixant par *ROLE_*. C'est un convention dans Spring, tous les rôles doivent commencer par ce préfixe. ### Création de endpoint http Finalement, il est nécessaire de créer un controler pour configurer quelques *endpoints http*. Nous proposons dans ce TP d'utiliser le controlleur suivant pour configurer quatre routes: un simple index '/' sans autorisation particulière, une route */index* réservé aux utilsateurs avec le role **ROLE_USER**, une route */admin* réservé aux utilsateurs avec le role **ROLE_ADMIN**. Dans la route index, je montrer comment récupérer facilement les éléments du token si nécessaire. ```java package sample.data.jpa.controllers; import java.security.Principal; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; @Controller public class ViewController { @GetMapping("/index") @PreAuthorize("hasRole('USER')") public ModelAndView index( JwtAuthenticationToken authentication) { ModelAndView modelAndView = new ModelAndView("index"); authentication.getToken().getClaims().forEach((e,v)-> { System.err.println(e + ' ' +v); }); modelAndView.addObject("user", authentication); return modelAndView; } @GetMapping("/") public ModelAndView main() { ModelAndView modelAndView = new ModelAndView("indexmain"); return modelAndView; } @GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public ModelAndView admin(Principal principal) { ModelAndView modelAndView = new ModelAndView("admin"); modelAndView.addObject("user", principal); return modelAndView; } } ``` Pour gérer ces différentes routes nous utiliserons des templates de page Web. Voici les différents templates à place dans *src/main/resources/templates/* - **index.html** ```htmlmixed= <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> This is the index page. <br /> Logged user: <span th:if="${user} != null" th:text="${user.name}"></span> <br /> <a href="/admin">admin</a> <a href="/logout">logout</a> </body> </html> ``` - **indexmain.html** ```htmlmixed= <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Main</title> </head> <body> This is the main page. <br /> <a href="/index">index</a> <br /> <a href="/admin">admin</a> </body> </html> ``` - **admin.html** ```htmlmixed= <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Admin</title> </head> <body> This is the admin page <br /> Logged user: <span th:if="${user} != null" th:text="${user.name}"></span> <br /> <a href="/logout">logout</a> </body> </html> ``` - **logout.html** ```htmlmixed= <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Logout</title> </head> <body> This is the logout page. <br /> Logged user: <span th:if="${user} != null" th:text="${user.name}"></span> <br /> <a href="/">main</a> </body> </html> ``` **Relancez** cette application pour tester la sécurité. Pour tester la sécurité, il va falloir jouer le rôle d'un client qui récupère un token JWT au près de l'IDP. Puis fait une requête REST vers le serveur SpringBoot avec une en tête http qui embarque ce token. Je propose de faire cela en ligne de commande à l'aide de curl. Tester la manipulation avec votre utilisateur qui a un rôle USER et celui qui a un rôle admin sur les deux routes. ```bash # Pour récupérer le token, il faut remplacer le user, le password, l'application id et le client secret avec ceux configuré dans keycloak. Vérifier aussi que jq est installé. export TOKEN=$(curl --location 'http://localhost:8080/realms/myspringbootapprealm/protocol/openid-connect/token' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'username=user1' --data-urlencode 'password=test1' --data-urlencode 'grant_type=password' --data-urlencode 'client_id=myspringbootapp' --data-urlencode 'client_secret=Y7xpKQbFalUFTGUGD9XDBcvawvs3zsWZ' --data-urlencode 'scope=openid'| jq -r '.access_token') # Puis pour lancer une requête REST, curl -X GET http://localhost:8082/index -H "Authorization: Bearer $TOKEN" ou curl -X GET http://localhost:8082/admin -H "Authorization: Bearer $TOKEN" ``` Pour ceux qui voudrez faire cela depuis un code front react ou angular. Voici une petite documentation pour le faire depuis angular https://github.com/chiraranw/angualr-keycloak-jwt ### Etape 5 : Adapter ce projet pour votre propre projet