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:

Installation et configuration de Keycloak

Nous aurons besoin de faire les actions suivantes:

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:

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:

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.

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.

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

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:

$ cd keycloak-25.0.6
$ /bin/kc.sh start-dev
$ cd keycloak-springboot3-api
$ mvn: clean spring-boot:run
$ curl -d "grant_type=password&username=user&password=user&client_id=demo" http://localhost:8080/realms/myrealm/protocol/openid-connect/token
$ 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!

Leave a Reply