La sécurité est souvent négligée et perçue comme un fardeau qui est en opposition à la vitesse de développement. Mais plus il y a de couches de sécurité, plus c'est sûr. C'est ce que signifie la "sécurité en profondeur" (Security in Depth), et l'une de ses composantes consiste à sécuriser notre API REST.
Dans ce blog, on va apprendre comment ajouter l'authentification à une API REST Spring Boot.
Au lieu de créer une implémentation personnalisée fragile qui pourrait contenir des problèmes de sécurité, il est préférable de s'appuyer sur des solutions éprouvées. L'une d'entre elles est Keycloak, une solution d'identité et de gestion open source développée par Red Hat.
Dans un précédent blog, on a déjà appris comment configurer Keycloak avec Docker, et on va utiliser cette configuration comme point de départ.
Bien sûr, vous pouvez configurer votre instance Keycloak comme bon vous semble. La seule exigence est d'avoir une instance Keycloak en cours d'exécution.
Configuration du projet
La première étape consiste à générer le projet Spring Boot en utilisant Spring Initializr. Dans ce tutoriel, nous utilisons la ligne de commande Spring via SDKman, mais vous pouvez également le faire rapidement en utilisant l'interface web https://start.spring.io/ ou directement depuis votre IDE https://www.jetbrains.com/help/idea/spring-boot.html.
Pour savoir comment configurer l'interface de ligne de commande sur votre propre machine, suivez ce guide https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing.cli.sdkman. Une fois que vous avez installé l'interface de ligne de commande, exécutez cette commande pour générer le projet avec les dépendances nécessaires.
spring init --dependencies=web,data-jpa,h2,lombok,security spring-boot-keycloak
On ajoute les dépendances suivantes :
- la dépendance web pour l'API REST
- Spring Data JPA pour la couche d'accès aux données, qui utilise Hibernate comme outil de mappage objet-relation par défaut
- la bibliothèque H2 pour fournir une base de données embarquée en mémoire facile à utiliser. Ce type de base de données convient aux petits projets comme celui-ci, mais ne doit pas être utilisé pour des projets sérieux destinés à être mis en production.
- Lombok pour générer des extraits de code via des annotations et éviter tout code inutile
- La dépendance security pour accéder à Spring Security
En plus de cela, on ajoute également la dépendance Keycloak Spring Boot Adapter à notre fichier pom.xml.
...
<properties>
<keycloak.version>17.0.0</keycloak.version>
</properties>
...
<dependencies>
...
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak.version}</version>
</dependency>
</dependencies>
Cet adaptateur fournit une implémentation pour intégrer Keycloak avec Spring Security.
La configuration de l'adaptateur se trouve dans le fichier application.yml.
server:
port: 9000
spring:
datasource:
url: jdbc:h2:mem:mydb
username: mozen
password: password
keycloak:
realm: master
auth-server-url: http://${KEYCLOAK_HOST:localhost}:${KEYCLOAK_PORT:8180}/auth
resource: spring-app
bearer-only: true
Cette ressource est configurée en tant que "bearer only", ce qui signifie que cette application ne participe pas au flux de connexion et qu'elle s'attend à ce que toutes les requêtes reçues contiennent le jeton d'authentification.
Avec ce type de client, on compte sur d'autres clients pour exécuter le flux d'authentification et obtenir le jeton d'authentification. Habituellement, une application frontend gère la connexion et obtient le jeton d'authentification, puis envoie des requêtes à l'application backend en fournissant le jeton dans les en-têtes HTTP.
Nous utilisons le realm "master", qui est le realm par défaut de Keycloak.
Il existe de nombreuses autres propriétés que vous pouvez configurer ici pour répondre à vos besoins. Pour obtenir un aperçu de toutes ces propriétés, vous pouvez consulter cette page.
Construction de l'application sans sécurité
On définit d'abord les composants nécessaires pour notre endpoint HTTP.
Tout d'abord, on crée une entité "Plant" qui représentera la ressource de notre API REST.
package com.mozen.springbootkeycloack.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.NaturalId;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "plant")
@Getter
@Setter
@ToString
@EqualsAndHashCode
public class Plant {
public Plant() {
this.createdAt = Instant.now();
}
public Plant(String name, String scientificName, String family) {
this.name = name;
this.scientificName = scientificName;
this.family = family;
this.createdAt = Instant.now();
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@NaturalId()
@Column(name = "name")
private String name;
@NaturalId()
@Column(name = "scientificName")
private String scientificName;
@Column(name = "family")
private String family;
@Column(name = "createdAt")
private Instant createdAt = Instant.now();
}
Ensuite, on crée un référentiel Spring JPA pour cette entité.
package com.mozen.springbootkeycloack.repository;
import com.mozen.springbootkeycloack.model.Plant;
import org.springframework.data.repository.CrudRepository;
public interface PlantRepository extends CrudRepository<Plant, Long> {
}
En étendant l'interface CrudRepository, on a accès à toutes les opérations CRUD sur l'entité.
On continue avec la couche métier.
package com.mozen.springbootkeycloack.service;
import com.mozen.springbootkeycloack.model.Plant;
import com.mozen.springbootkeycloack.repository.PlantRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Transactional
@Service
public class PlantService {
private PlantRepository plantRepository;
public PlantService(
PlantRepository plantRepository) {
this.plantRepository = plantRepository;
}
public Plant getPlant(long plantId) throws RuntimeException {
Optional<Plant> plantOpt = plantRepository.findById(plantId);
if (!plantOpt.isPresent()) {
throw new RuntimeException("Plant could not be found with id : " + plantId);
}
return plantOpt.get();
}
}
Cette application de démonstration est si petite qu'on pourrait sauter la mise en œuvre de ce service et mettre en œuvre la logique directement dans le contrôleur, mais faisons les choses correctement.
Enfin, on crée notre endpoint HTTP à l'intérieur d'un contrôleur Spring.
package com.mozen.springbootkeycloack.controller;
import com.mozen.springbootkeycloack.model.Plant;
import com.mozen.springbootkeycloack.service.PlantService;
import com.sun.istack.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController()
@RequestMapping("/plant")
public class PlantController {
private PlantService plantService;
public PlantController(PlantService plantService) {
this.plantService = plantService;
}
@GetMapping("/{plantId}")
public Plant getPlant(@PathVariable @NotNull Long plantId) {
log.info("Request for plant " + plantId + " received");
return plantService.getPlant(plantId);
}
}
Si vous souhaitez plus de détails sur cette implémentation, j'ai réalisé une application similaire dans un blog précédent où je vais plus en détail sur le comment et le pourquoi de cette implémentation.
Configuration de Keycloak
On peut maintenant plonger dans la configuration Keycloak.
Une fois connecté à la console d'administration, on peut accéder à la page des clients et créer le client pour notre application Spring Boot.
On doit définir le type d'accès sur "bearer-only" pour correspondre à la configuration du fichier application.yml.
Pour tester notre endpoint, on définie un deuxième client qui représentera le client (dans le sens de la relation client-serveur) pour notre application Spring.
Ce client est de type "public" et est responsable de la connexion avec Keycloak pour obtenir le jeton d'accès qui sera envoyé dans chaque requête à l'application Spring Boot.
Ce type de client est généralement utilisé pour les applications frontales qui gèrent le processus de connexion dans le navigateur.
Configuration de Spring Security
Keycloak est maintenant prêt, tout comme notre application, mais elle doit encore être sécurisée.
Pour ça, on va configurer Spring Security.
On effectue la configuration en créant une classe qui étend WebSecurityConfigurerAdapter
.
On utilise l'adaptateur Keycloak précédemment ajouté en étendant KeycloakWebSecurityConfigurerAdapter
.
package com.mozen.springbootkeycloack.security;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@KeycloakConfiguration
public class WebSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider =
keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf()
.disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Il étend WebSecurityConfigurerAdapter
et configure toutes les configurations liées à Keycloak, telles que la configuration du filtre ou de l'authentification. Tout est fait pour faciliter notre travail et fonctionner out-of-the-box. On doit simplement définir la configuration spécifique à notre application.
Vous pouvez toujours vérifier comment l'adaptateur a été implémenté en explorant le code si vous êtes intéressé par le fonctionnement interne de Spring Security.
La partie la plus importante est l'override de la méthode configure(). C'est là qu'on impose que chaque requête reçue doit être authentifiée, en contenant le jeton d'authentification dans les en-têtes HTTP.
Comme on va consommer l'endpoint à l'aide de Postman, on peut désactiver la protection CSRF.
La désactivation de la protection CSRF signifie que l'application est maintenant vulnérable aux attaques CSRF. Ce type d'attaque ne peut être effectué que depuis une requête provenant d'un navigateur. Si on veut le gérer ce cas, on devrait gérer un cookie CSRF qu'on fournirait lors de la connexion et vérifier s'il est envoyé dans chaque requête qu'on reçoit.
On fournit également un bean SessionAuthenticationStrategy
de type NullAuthenticatedSessionStrategy
. Il ne fait essentiellement rien, mais c'est normal car l'authentification n'est pas gérée par l'application pour les clients en mode "bearer-only".
Au moment de la rédaction de cet article, l'adaptateur Keycloak Spring Boot contient un bug qui provoque une attente de dépendance circulaire au démarrage.
Une solution de contournement consiste à déclarer un bean de type KeycloakConfigResolver
à l'intérieur d'une classe annotée avec @Configuration, mais différente de la classe WebSecurityConfiguration
qu'on a déjà définie.
@Configuration
public class ApplicationConfiguration {
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
Et voilà, notre application Spring Boot est prête.
Configuration du client et tests
On commence par lancer notre application.
mvn spring-boot:run
Pour tester ce qu'on a construit, on a besoin d'une application cliente qui consomme l'endpoint HTTP.
Dans un environnement de production, la requête vient généralement d'une application Single page ou d'un autre service backend. Pour les besoins de ce blog, on va gérer le processus de connexion pour obtenir le jeton d'authentification à l'aide de Postman. On va utiliser le client public qu'on va créé dans la partie précédente.
On commence par créer une collection Postman.
On définit la configuration d'autorisation au niveau de la collection pour que chaque demande de la collection en hérite et envoie le jeton.
Dans le même but, on définit les variables au niveau de la collection pour les rendre disponibles pour chaque demande.
La première requête est la demande de récupération du jeton (GetToken). Elle utilise l'endpoint du jeton OpenID de Keycloak en suivant le flux d'authentification par mot de passe.
Parce que cette demande est envoyée à Keycloak, on doit remplacer la configuration d'autorisation pour indiquer qu'aucune authentification n'est nécessaire.
On pourrait copier manuellement le jeton renvoyé par Keycloak, mais il existe un moyen plus simple en utilisant la fonction de test de Postman.
Cette fonction nous permet d'écrire du code. Elle est généralement utilisée pour exécuter des tests sur la réponse. Mais on peut aussi exploiter cette fonction pour alimenter le jeton dans la variable keycloakToken qu'on a définie au niveau de la collection.
On peut exécuter directement de manière séquentielle la demande GetToken suivie de la demande GetPlant sans effectuer d'action manuelle.
On peut maintenant accéder avec succès au endpoint Plant.
Dans le prochain blog, on verra comment améliorer notre application avec un accès basé sur les rôles.
Le projet de démonstration est disponible sur Github.