Sur un serveur Springboot, sécuriser toutes ses api en vérifiant la validité d'un jeton au format JWT(fourni par exemple par Keycloak).
Avoir la possibilité de désactiver simplement cette sécurisation grâce à une propriété dans le fichier application.yml
Configuration de springdoc.
Ajouter la dépendance dans POM.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Ajouter une classe dans le package config par exemple
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import java.util.Collections;
import java.util.Map;
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
protected String issuerUri;
@Value("${spring.security.oauth2.resourceserver.jwt.audience}")
protected String audience;
@Value("${spring.security.oauth2.resourceserver.jwt.sub-replace-by}")
protected String subReplaceBy;
@Value("${spring.security.enabled:true}")
protected boolean enabledSecurity;
@Value("${spring.security.oauth2.resourceserver.jwt.roles.resource-name:}")
protected String resourceName;
@Value("${spring.security.oauth2.resourceserver.jwt.roles.for-realm:true}")
protected boolean roleForRealm;
@Override
protected void configure(HttpSecurity http) throws Exception {
if (enabledSecurity)
http
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeRequests()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/**").authenticated()
.and()
.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
else
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
jwtDecoder.setClaimSetConverter(new ReplaceSubClaimAdapter());
return jwtDecoder;
}
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter(roleForRealm, resourceName));
return jwtAuthenticationConverter;
}
class ReplaceSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
@Override
public Map<String, Object> convert(Map<String, Object> claims) {
Map<String, Object> convertedClaims = this.delegate.convert(claims);
String substitueValue = (String) convertedClaims.get(subReplaceBy);
convertedClaims.put("sub", substitueValue);
return convertedClaims;
}
}
On ajoute la classe qui va permettre de valider l'audience
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private final String audience;
public AudienceValidator(String aud) {
this.audience = aud;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains(audience)) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error("invalid token", "The required audience is missing", null);
return OAuth2TokenValidatorResult.failure(error);
}
}
et la classe qui va substituer le sub de keycloak par un élément définissant l'utilisateur:
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
private String resourceName;
private boolean roleForRealm = true;
public static final String REALM_ACCESS = "realm_access";
public static final String RESOURCE_ACCESS = "resource_access";
public static final String ROLES = "roles";
public static final String ROLE_PREFIX = "ROLE_";
public KeycloakRealmRoleConverter() {
}
public KeycloakRealmRoleConverter(boolean roleForRealm, String resourceName) {
this.roleForRealm = roleForRealm;
this.resourceName = resourceName;
}
@SuppressWarnings("unchecked")
@Override
public Collection<GrantedAuthority> convert(final Jwt jwt) {
Map<String, Object> access;
if (!roleForRealm && resourceName != null) {
access = (Map<String, Object>) jwt.getClaims().get(RESOURCE_ACCESS);
if (access.containsKey(resourceName)) {
access = (Map<String, Object>) access.get(resourceName);
}
} else {
access = (Map<String, Object>) jwt.getClaims().get(REALM_ACCESS);
}
return ((List<String>) access.get(ROLES)).stream()
.map(roleName -> ROLE_PREFIX + roleName.toUpperCase())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
Ajouter les propriétés dans le fichier application.yml (remplacer les valeurs SERVER & REALM par les votres)
spring:
security:
enabled: true
oauth2:
resourceserver:
jwt:
issuer-uri: https://SERVER/auth/realms/REALM
audience: MyDemo
sub-replace-by: email # On remplace la valeur sub du claim par l'email
roles:
resource-name: agentk # Optionnel Le rôle est basé sur l'agent et non sur le realm
for-realm: false # True par défaut. Par défaut on va chercher les rôles sur le Realm
springdoc:
swagger-ui:
oauth:
clientId: MyDemo #Valeur par défaut renseignée dans la fenêtre d'autorisation
Ajouter la dépendance Springdoc dans POM.xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${springdoc.version}</version>
</dependency>
Modifier la configuration de springdoc pour appliquer la demande du jeton sur toutes les opérations.
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.*;
import io.swagger.v3.oas.models.servers.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
@Configuration
public class SpringDocConfig {
private static final Logger log = LoggerFactory.getLogger(SpringDocConfig.class);
@Value("${server.version}")
String version;
@Value("${server.oasBaseUrl}")
String serverUrl;
@Value("${server.oasDescription}")
String serverDescription;
@Value("${spring.security.enabled:true}")
boolean enabledSecurity;
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
String authUrl;
@Bean
public OpenAPI springbootOpenAPI() {
OpenAPI oai = new OpenAPI()
.info(new Info().title("MyAPI")
.description("Serveur d'API")
.version(version)
.license(new License().name("EULA")
.url("https://SERVER/EULA")));
if (serverUrl != null && !serverUrl.isEmpty()) {
Server server = new Server();
server.setUrl(serverUrl);
if (serverDescription != null && !serverDescription.isEmpty()) {
server.setDescription(serverDescription);
}
if (oai.getServers() == null) {
oai.setServers(new ArrayList<>(1));
}
oai.getServers().add(server);
}
if (enabledSecurity) {
oai
//.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.components(new Components()
.addSecuritySchemes("oauth", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.description("Keycloak Authentification")
.flows(new OAuthFlows()
.implicit(new OAuthFlow()
.authorizationUrl(authUrl + "/protocol/openid-connect/auth")
.refreshUrl(authUrl + "/protocol/openid-connect/token")
.tokenUrl(authUrl + "/protocol/openid-connect/token")
.scopes(new Scopes())
)))
.addSecuritySchemes("api_key", // Pour une injection manuelle du jeton keycloak
new SecurityScheme()
.name("api_key")
.type(SecurityScheme.Type.HTTP)
.scheme("Bearer")
.bearerFormat("JWT")
))
.security(Arrays.asList(
new SecurityRequirement().addList("oauth"),
new SecurityRequirement().addList("api_key")));
}
log.info("OpenApi Documentation initialisé");
return oai;
}
}
Pour désactiver la demande du jeton sur une méthode, il suffit d'ajouter l'annotation suivante
@SecurityRequirements(value = {})
Pour injecter l'utilisateur dans une méthode d'accès à une ressource, il suffit d'injecter @AuthenticationPrincipal
@Operation(summary = "Retourne une liste d'item")
public List<Item> getAllItem( @Parameter(description = "Nom de l'item")
@RequestParam(name = "itemName", required = false) String itemName,
@AuthenticationPrincipal Jwt principal) {
}
On peut aussi récupèrer l'utilisateur authentifié
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();