Skip to content

Commit

Permalink
feat: P4ADEV-1437 add spring security config (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
antocalo authored Nov 29, 2024
1 parent 58fc0f0 commit dea9b14
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 1 deletion.
1 change: 1 addition & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ microservice-chart:
APPLICATIONINSIGHTS_PREVIEW_PROFILER_ENABLED: "false"
ENABLE_AUDIT_APPENDER: "TRUE"

AUTH_SERVER_BASE_URL: "http://p4pa-auth-microservice-chart:8080/payhub"

envSecret:
APPLICATIONINSIGHTS_CONNECTION_STRING: appinsights-connection-string
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/it/gov/pagopa/payhub/pdnd/dto/auth/UserInfoDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package it.gov.pagopa.payhub.pdnd.dto.auth;

import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@Data
@Builder
@AllArgsConstructor
@RequiredArgsConstructor
public class UserInfoDTO {

private String userId;
private String mappedExternalUserId;
@ToString.Exclude
private String fiscalCode;
@ToString.Exclude
private String familyName;
@ToString.Exclude
private String name;
@ToString.Exclude
private String email;
private String issuer;
private String organizationAccess;
@Builder.Default
private List<UserOrganizationRolesDTO> organizations = new ArrayList<>();

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package it.gov.pagopa.payhub.pdnd.dto.auth;

import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@Data
@Builder
@AllArgsConstructor
@RequiredArgsConstructor
public class UserOrganizationRolesDTO {

private String operatorId;
private String organizationIpaCode;
@ToString.Exclude
private String email;
@Builder.Default
private List<String> roles = new ArrayList<>();

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.gov.pagopa.payhub.pdnd.exception.custom;

public class InvalidAccessTokenException extends RuntimeException {
public InvalidAccessTokenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package it.gov.pagopa.payhub.pdnd.security;

import it.gov.pagopa.payhub.pdnd.dto.auth.UserInfoDTO;
import it.gov.pagopa.payhub.pdnd.exception.custom.InvalidAccessTokenException;
import it.gov.pagopa.payhub.pdnd.service.auth.AuthorizationService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final AuthorizationService authorizationService;

public JwtAuthenticationFilter(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(authorization)) {
String token = authorization.replace("Bearer ", "");
UserInfoDTO userInfo = authorizationService.validateToken(token);
Collection<? extends GrantedAuthority> authorities = null;
if (userInfo.getOrganizationAccess() != null) {
authorities = userInfo.getOrganizations().stream()
.filter(o -> userInfo.getOrganizationAccess().equals(o.getOrganizationIpaCode()))
.flatMap(r -> r.getRoles().stream())
.map(SimpleGrantedAuthority::new)
.toList();
}
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userInfo, null, authorities);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (InvalidAccessTokenException e){
log.info("An invalid accessToken has been provided: " + e.getMessage());
} catch (Exception e){
log.error("Something gone wrong while validate accessToken", e);
}
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package it.gov.pagopa.payhub.pdnd.security;

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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public WebSecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
// Swagger endpoints
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**"
).permitAll()

// Actuator endpoints
.requestMatchers(
"/actuator",
"/actuator/**"
).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package it.gov.pagopa.payhub.pdnd.service.auth;

import it.gov.pagopa.payhub.pdnd.dto.auth.UserInfoDTO;
import it.gov.pagopa.payhub.pdnd.exception.custom.InvalidAccessTokenException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;

@Service
@Slf4j
public class AuthorizationService {

private final RestTemplate restTemplate;

public AuthorizationService(@Value("${app.auth.base-url}") String authServerBaseUrl,
RestTemplateBuilder restTemplateBuilder) {
DefaultUriBuilderFactory ubf = new DefaultUriBuilderFactory(authServerBaseUrl);
ubf.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
this.restTemplate = restTemplateBuilder
.uriTemplateHandler(ubf)
.build();
}

public UserInfoDTO validateToken(String accessToken){
log.info("Requesting validate token");
try{
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
return restTemplate.exchange("/auth/userinfo", HttpMethod.GET, new HttpEntity<>(headers), UserInfoDTO.class).getBody();
} catch (HttpStatusCodeException ex){
String errorMessage;
if(HttpStatus.UNAUTHORIZED.equals(ex.getStatusCode())){
errorMessage="Bad Access Token provided";
log.info(errorMessage);
} else {
errorMessage="Something gone wrong while validate token";
log.error(errorMessage, ex);
}
throw new InvalidAccessTokenException(errorMessage);
}
}
}
4 changes: 3 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ app:
publicKey: "\${PDND_SERVICE_ANPR_C030_PUBLICKEY:\${PDND_SERVICE_ANPR_PUBLICKEY:\${PDND_SERVICE_PUBLICKEY:}}}"
rest-client:
connect.timeout.millis: "\${CONNECT_TIMEOUT_MILLIS:120000}"
read.timeout.millis: "\${READ_TIMEOUT_MILLIS:120000}"
read.timeout.millis: "\${READ_TIMEOUT_MILLIS:120000}"
auth:
base-url: "\${AUTH_SERVER_BASE_URL:https://auth-server/api}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package it.gov.pagopa.payhub.pdnd.security;

import it.gov.pagopa.payhub.pdnd.dto.auth.UserInfoDTO;
import it.gov.pagopa.payhub.pdnd.dto.auth.UserOrganizationRolesDTO;
import it.gov.pagopa.payhub.pdnd.exception.custom.InvalidAccessTokenException;
import it.gov.pagopa.payhub.pdnd.service.auth.AuthorizationService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

@ExtendWith(MockitoExtension.class)
class JwtAuthenticationFilterTest {

@Mock
private FilterChain filterChainMock;

@Mock
private AuthorizationService authorizationService;

@InjectMocks
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Test
void givenValidTokenWhenDoFilterInternalThenOk() throws ServletException, IOException {
// Given
String accessToken = "ACCESSTOKEN";
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/path");
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);

MockHttpServletResponse response = new MockHttpServletResponse();

UserInfoDTO userInfo = UserInfoDTO.builder()
.mappedExternalUserId("MAPPEDEXTERNALUSERID")
.fiscalCode("FISCALCODE")
.familyName("FAMILYNAME")
.name("NAME")
.issuer("ISSUER")
.organizationAccess("ORG")
.organizations(List.of(UserOrganizationRolesDTO.builder()
.organizationIpaCode("ORG")
.roles(List.of("ROLE"))
.build()))
.build();

Collection<? extends GrantedAuthority> authorities = null;
if (userInfo.getOrganizationAccess() != null) {
authorities = userInfo.getOrganizations().stream()
.filter(o -> userInfo.getOrganizationAccess().equals(o.getOrganizationIpaCode()))
.flatMap(r -> r.getRoles().stream())
.map(SimpleGrantedAuthority::new)
.toList();
}

Mockito.when(authorizationService.validateToken(accessToken)).thenReturn(userInfo);

UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userInfo, null, authorities);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

// When
jwtAuthenticationFilter.doFilterInternal(request, response, filterChainMock);

// Then
Mockito.verify(filterChainMock).doFilter(request, response);
Assertions.assertEquals(
authToken,
SecurityContextHolder.getContext().getAuthentication()
);
}

@Test
void givenInvalidTokenWhenDoFilterInternalThenInvalidAccessTokenException() throws ServletException, IOException {
// Given
String accessToken = "INVALIDACCESSTOKEN";
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/path");
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);

MockHttpServletResponse response = new MockHttpServletResponse();

Mockito.doThrow(new InvalidAccessTokenException("An invalid accessToken has been provided")).when(authorizationService).validateToken(accessToken);

// When
jwtAuthenticationFilter.doFilterInternal(request, response, filterChainMock);

// Then
Mockito.verify(filterChainMock).doFilter(request, response);
}

@Test
void givenInvalidTokenWhenDoFilterInternalThenRuntimeException() throws ServletException, IOException {
// Given
String accessToken = "EXCEPTION";
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/path");
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);

MockHttpServletResponse response = new MockHttpServletResponse();

Mockito.doThrow(new RuntimeException("Something gone wrong while validate accessToken")).when(authorizationService).validateToken(accessToken);

// When
jwtAuthenticationFilter.doFilterInternal(request, response, filterChainMock);

// Then
Mockito.verify(filterChainMock).doFilter(request, response);
}
}
Loading

0 comments on commit dea9b14

Please sign in to comment.