Skip to content

Commit

Permalink
Merge pull request #72 from InseeFr/feat/auth
Browse files Browse the repository at this point in the history
Feat: add auth with oidc
  • Loading branch information
laurentC35 authored Jan 26, 2024
2 parents ed29e51 + 63e8e8d commit 8058e8c
Show file tree
Hide file tree
Showing 35 changed files with 846 additions and 81 deletions.
40 changes: 34 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
<version>3.2.1</version>
</parent>

<properties>
Expand All @@ -33,6 +33,17 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>compile</scope>
</dependency>
<!-- Auth -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
Expand All @@ -43,18 +54,18 @@
<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.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
Expand Down Expand Up @@ -226,6 +237,23 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
<configuration>
<additionalProperties>
<description>${project.description}</description>
</additionalProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import org.springframework.boot.builder.SpringApplicationBuilder;

import fr.insee.publicenemy.api.configuration.PropertiesLogger;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@SpringBootApplication(scanBasePackages = "fr.insee.publicenemy")
@EnableTransactionManagement
@ConfigurationPropertiesScan
public class PublicEnemyApplication {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package fr.insee.publicenemy.api.application.ports;

import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder;

import java.util.Locale;

public interface I18nMessagePort {
/**
*
Expand All @@ -16,6 +21,8 @@ public interface I18nMessagePort {
*/
String getMessage(String id, String... args);

String getMessage(MessageSourceResolvable msr);

/**
*
* @param id message key
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package fr.insee.publicenemy.api.application.web.auth;

import org.springframework.security.core.Authentication;

public interface AuthenticationHelper {
/**
* Retrieve the auth token of the current user
*
* @return auth token
*/
String getUserToken();

/**
* Retrieve the authentication principal for current user
*
* @return {@link Authentication} the authentication user object
*/
Authentication getAuthenticationPrincipal();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package fr.insee.publicenemy.api.application.web.auth;

public class AuthenticationTokenException extends RuntimeException {
public AuthenticationTokenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fr.insee.publicenemy.api.application.web.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationUserHelper implements AuthenticationHelper {
@Override
public String getUserToken() {
if(getAuthenticationPrincipal() instanceof JwtAuthenticationToken auth) {
return auth.getToken().getTokenValue();
}
throw new AuthenticationTokenException("Cannot retrieve token for the user.");
}

@Override
public Authentication getAuthenticationPrincipal() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package fr.insee.publicenemy.api.configuration;

import fr.insee.publicenemy.api.application.web.auth.AuthenticationHelper;
import fr.insee.publicenemy.api.configuration.rest.WebClientTokenInterceptor;
import io.netty.handler.logging.LogLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
Expand All @@ -28,6 +32,10 @@
@Slf4j
public class AppConfig implements WebMvcConfigurer {

@Autowired
private AuthenticationHelper authenticationHelper;


/**
*
* @param proxyUrl proxy url
Expand All @@ -36,9 +44,12 @@ public class AppConfig implements WebMvcConfigurer {
* @return webclient configured with proxy
*/
@Bean
@ConditionalOnProperty(name="application.proxy.enable", havingValue="true")
public WebClient webClientProxy(@Value("${application.proxy.url}") String proxyUrl,
@Value("${application.proxy.port}") Integer proxyPort, @Value("${application.debug.webclient}") boolean debug,
@ConditionalOnProperty(name="feature.proxy.enabled", havingValue="true")
public WebClient webClientProxy(
@Value("${feature.proxy.url}") String proxyUrl,
@Value("${feature.proxy.port}") Integer proxyPort,
@Value("${feature.debug.webclient}") boolean debug,
@Value("${feature.oidc.enabled}") boolean oidcEnabled,
WebClient.Builder builder) {
HttpClient httpClient = HttpClient.create()
.proxy(proxy -> proxy
Expand All @@ -55,6 +66,8 @@ public WebClient webClientProxy(@Value("${application.proxy.url}") String proxyU
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);

if(oidcEnabled) builder.filter(new WebClientTokenInterceptor(authenticationHelper));
return builder.build();
}

Expand All @@ -64,11 +77,14 @@ public WebClient webClientProxy(@Value("${application.proxy.url}") String proxyU
* @return webclient with json default headers
*/
@Bean
@ConditionalOnProperty(name="application.proxy.enable", havingValue="false")
public WebClient webClient(WebClient.Builder builder) {
@ConditionalOnProperty(name="feature.proxy.enabled", havingValue="false")
public WebClient webClient(
@Value("${feature.oidc.enabled}") boolean oidcEnabled,
WebClient.Builder builder) {
builder
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
if(oidcEnabled) builder.filter(new WebClientTokenInterceptor(authenticationHelper));
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,32 @@

import java.util.List;

import fr.insee.publicenemy.api.configuration.properties.ApplicationProperties;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/** Cors configuration */
@Configuration
@AllArgsConstructor
public class CorsConfig {
private final List<String> corsOrigins;

public CorsConfig(@Value("${application.cors.allowed-origins}") List<String> corsOrigins) {
this.corsOrigins = corsOrigins;
}

@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
protected CorsConfigurationSource corsConfigurationSource(ApplicationProperties applicationProperties) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(applicationProperties.corsOrigins());
configuration.setAllowedMethods(List.of("GET", "PUT", "POST", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
configuration.addExposedHeader("Content-Disposition");
configuration.setMaxAge(3600L);
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
corsOrigins.forEach(config::addAllowedOrigin);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
return bean;
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fr.insee.publicenemy.api.configuration.auth;

public class AuthConstants {
private AuthConstants() {
throw new IllegalStateException("Constants class");
}

public static final String ROLE_PREFIX = "ROLE_";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package fr.insee.publicenemy.api.configuration.auth;

public class AuthorityRole {
private AuthorityRole() {
throw new IllegalArgumentException("Constant class");
}

public static final String HAS_ROLE_DESIGNER = "hasRole('DESIGNER')";
public static final String HAS_ANY_ROLE = "hasAnyRole('DESIGNER', 'ADMIN')";
public static final String HAS_ADMIN_PRIVILEGES = "hasAnyRole('ADMIN')";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.insee.publicenemy.api.configuration.auth;

public enum AuthorityRoleEnum {
ADMIN,
DESIGNER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package fr.insee.publicenemy.api.configuration.auth;

import fr.insee.publicenemy.api.configuration.properties.OidcProperties;
import fr.insee.publicenemy.api.configuration.properties.RoleProperties;
import lombok.AllArgsConstructor;
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.*;
import java.util.stream.Collectors;
@AllArgsConstructor
public class GrantedAuthorityConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
private final OidcProperties oidcProperties;
private final RoleProperties roleProperties;

/**
*
* @param map: Map that represent JWT token
* @param keyPath : jsonPath to wanted value, ex: realm_access.roles
* @return the value of keyPath inside Map
* @param <T>
*/
public <T> T getDeepPropsOfMapForRoles(Map<String, Object> map, String keyPath){
Map subMap = (Map) map;
String[] propertyPath = keyPath.toString().split("\\.");
for (int i = 0; i < propertyPath.length -1; i++) {
subMap = (Map) subMap.get(propertyPath[i]);
}
return (T) subMap.get(propertyPath[propertyPath.length -1]);

}

@SuppressWarnings("unchecked")
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Map<String, Object> claims = jwt.getClaims();

List<String> roles = getDeepPropsOfMapForRoles(claims, oidcProperties.roleClaim());

return roles.stream()
.map(role -> {
if (role.equals(roleProperties.designer())) {
return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.DESIGNER);
}
if (role.equals(roleProperties.admin())) {
return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.ADMIN);
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toCollection(ArrayList::new));
}
}
Loading

0 comments on commit 8058e8c

Please sign in to comment.