Commit 851a612a authored by Ananas Orange's avatar Ananas Orange
Browse files

Minor code reviews

parent a9d04b28
......@@ -3,8 +3,6 @@ package fr.gouv.clea.ws.configuration;
import fr.gouv.clea.ws.exception.CleaForbiddenException;
import fr.gouv.clea.ws.exception.CleaUnauthorizedException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.GenericFilterBean;
......@@ -15,20 +13,18 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
@Slf4j
public class JwtValidationFilter extends GenericFilterBean {
private final boolean checkAuthorization;
private final String robertJwtPublicKey;
private final PublicKey robertJwtPublicKey;
private final HandlerExceptionResolver handlerExceptionResolver;
public JwtValidationFilter(boolean checkAuthorization, String robertJwtPublicKey, HandlerExceptionResolver handlerExceptionResolver) {
public JwtValidationFilter(boolean checkAuthorization, PublicKey robertJwtPublicKey, HandlerExceptionResolver handlerExceptionResolver) {
this.checkAuthorization = checkAuthorization;
this.robertJwtPublicKey = robertJwtPublicKey;
this.handlerExceptionResolver = handlerExceptionResolver;
......@@ -58,11 +54,7 @@ public class JwtValidationFilter extends GenericFilterBean {
private void verifyJWT(String token) throws CleaForbiddenException {
try {
byte[] encoded = Decoders.BASE64.decode(this.robertJwtPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(SignatureAlgorithm.RS256.getFamilyName());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
PublicKey jwtPublicKey = keyFactory.generatePublic(keySpec);
Jwts.parserBuilder().setSigningKey(jwtPublicKey).build().parseClaimsJws(token);
Jwts.parserBuilder().setSigningKey(robertJwtPublicKey).build().parseClaimsJws(token);
} catch (Exception e) {
log.warn("Failed to verify JWT token!", e);
throw new CleaForbiddenException();
......
package fr.gouv.clea.ws.configuration;
import fr.inria.clea.lsp.LocationSpecificPartDecoder;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
......@@ -12,37 +14,54 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.annotation.PostConstruct;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
@EnableWebSecurity
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final boolean checkAuthorization;
private final String robertJwtPublicKey;
private final String encodedRobertJwtPublicKey;
private final HandlerExceptionResolver handlerExceptionResolver;
private PublicKey robertJwtPublicKey;
@Autowired
public SecurityConfiguration(
@Value("${clea.conf.security.report.checkAuthorization}") boolean checkAuthorization,
@Value("${clea.conf.security.report.robertJwtPublicKey}") String robertJwtPublicKey,
@Value("${clea.conf.security.report.robertJwtPublicKey}") String encodedRobertJwtPublicKey,
HandlerExceptionResolver handlerExceptionResolver
) {
this.checkAuthorization = checkAuthorization;
this.robertJwtPublicKey = robertJwtPublicKey;
this.encodedRobertJwtPublicKey = encodedRobertJwtPublicKey;
this.handlerExceptionResolver = handlerExceptionResolver;
}
@Bean
public LocationSpecificPartDecoder getLocationSpecificPartDecoder() {
return new LocationSpecificPartDecoder(null);
return new LocationSpecificPartDecoder();
}
@PostConstruct
private void initPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] encoded = Decoders.BASE64.decode(this.encodedRobertJwtPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(SignatureAlgorithm.RS256.getFamilyName());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
this.robertJwtPublicKey = keyFactory.generatePublic(keySpec);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/clea/*").permitAll()
.antMatchers("/api/clea/**").permitAll()
.and()
.addFilterAfter(new JwtValidationFilter(checkAuthorization, robertJwtPublicKey, handlerExceptionResolver), BasicAuthenticationFilter.class)
.httpBasic().disable()
......
......@@ -5,7 +5,6 @@ import fr.gouv.clea.ws.dto.ApiError;
import fr.gouv.clea.ws.utils.UriConstants;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
......@@ -33,7 +32,6 @@ class JwtValidationFilterTest {
private final MockHttpServletRequest request = new MockHttpServletRequest("POST", UriConstants.API_V1 + UriConstants.REPORT);
private final MockHttpServletResponse response = new MockHttpServletResponse();
private final MockFilterChain chain = new MockFilterChain();
private String jwtPublicKey;
private KeyPair keyPair;
@Autowired
private ObjectMapper objectMapper;
......@@ -43,7 +41,6 @@ class JwtValidationFilterTest {
@BeforeEach
void init() {
keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
jwtPublicKey = Encoders.BASE64.encode(keyPair.getPublic().getEncoded());
}
@Test
......@@ -52,7 +49,7 @@ class JwtValidationFilterTest {
long jwtLifeTime = 5;
Instant now = Instant.now();
Instant expiration = now.plus(jwtLifeTime, ChronoUnit.MINUTES);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, jwtPublicKey, handlerExceptionResolver);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, keyPair.getPublic(), handlerExceptionResolver);
request.addHeader(HttpHeaders.AUTHORIZATION, this.newJwtToken(now, expiration));
jwtValidationFilter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(200);
......@@ -62,7 +59,7 @@ class JwtValidationFilterTest {
@DisplayName("if authorization check is active, a token with an expired date should cause filter to return 403")
void testEnabledAuthWithExpiredToken() {
Instant now = Instant.now();
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, jwtPublicKey, handlerExceptionResolver);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, keyPair.getPublic(), handlerExceptionResolver);
request.addHeader(HttpHeaders.AUTHORIZATION, this.newJwtToken(now, now));
jwtValidationFilter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(403);
......@@ -71,7 +68,7 @@ class JwtValidationFilterTest {
@Test
@DisplayName("if authorization check is active, a null token should cause filter to return 401")
void testEnabledAuthWithNullToken() throws IOException {
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, jwtPublicKey, handlerExceptionResolver);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, keyPair.getPublic(), handlerExceptionResolver);
jwtValidationFilter.doFilter(request, response, chain);
ApiError apiError = objectMapper.readValue(response.getContentAsString(), ApiError.class);
assertThat(response.getStatus()).isEqualTo(401);
......@@ -81,7 +78,7 @@ class JwtValidationFilterTest {
@Test
@DisplayName("if authorization check is inactive, a null token should have no impact")
void testDisabledAuthWithNullToken() {
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(false, jwtPublicKey, handlerExceptionResolver);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(false, keyPair.getPublic(), handlerExceptionResolver);
jwtValidationFilter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(200);
}
......@@ -89,7 +86,7 @@ class JwtValidationFilterTest {
@Test
@DisplayName("if authorization check is active, an invalid token should cause filter to return 403")
void testEnabledAuthWithInvalidToken() throws IOException {
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, jwtPublicKey, handlerExceptionResolver);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(true, keyPair.getPublic(), handlerExceptionResolver);
request.addHeader(HttpHeaders.AUTHORIZATION, RandomStringUtils.randomAlphanumeric(9));
jwtValidationFilter.doFilter(request, response, chain);
ApiError apiError = objectMapper.readValue(response.getContentAsString(), ApiError.class);
......@@ -100,7 +97,7 @@ class JwtValidationFilterTest {
@Test
@DisplayName("if authorization check is inactive, an invalid token should have no impact")
void testDisabledAuthWithInvalidToken() {
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(false, jwtPublicKey, handlerExceptionResolver);
JwtValidationFilter jwtValidationFilter = new JwtValidationFilter(false, keyPair.getPublic(), handlerExceptionResolver);
request.addHeader(HttpHeaders.AUTHORIZATION, RandomStringUtils.randomAlphanumeric(9));
jwtValidationFilter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(200);
......
......@@ -3,17 +3,25 @@ package fr.gouv.clea.ws.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.gouv.clea.ws.dto.ApiError;
import fr.gouv.clea.ws.service.impl.ReportService;
import fr.gouv.clea.ws.utils.UriConstants;
import fr.gouv.clea.ws.vo.ReportRequest;
import fr.gouv.clea.ws.vo.Visit;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
......@@ -21,7 +29,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -30,14 +44,20 @@ import static org.assertj.core.api.Assertions.assertThat;
@TestPropertySource(properties = {"clea.conf.security.report.checkAuthorization=true"})
class CleaControllerAuthEnabledTest {
/*
* needed to generate a jwt key, that we can decrypt using public key in test confs
*/
private static final String privateJwtKet = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSnywoJusbV6UYYbyf8KrQ5V5MD1X8zo40oCuF5IRYQ4mb+9brS11BbuEn6UJDUuCDiAs0vcunwa71Hv6dakKcqfAZaIvQEpacbfVJDY7TX9QxrbJYUlEkJsbj2zOA+3g+G+taw007I74rPUfA7axN9eRGknl2+pkXQS3bConLZR5U7MT/9r9me4HPC4Z8kRcE5Pvvh4US9Bad9fZkRY5q+KaI8qxvjoeqDgumZVHRDuIfwOPcPTkefTc7zTr3a2Y2MaXidafk/9v1gR0kgBYdxMEWJEyBuwpEbYo0CD7TitxmfH/D7Fw2qyQq9xHXVMLNS40+woEKlniisC2vg7/tAgMBAAECggEAZbIhXngf+gcAa7jeq9CxqdJtZTP94CskVzwA9A1b/hxaBebXWrwbIpdVc+lGHIwPTSu/GgXKi8C7KSkmUOiy6xazgewRjXBXJojd6J2Owu1ksFBZswjlXr3GlaQkRQImlG2pAHsVxj80V6lZa2dua2RxwME3nl6ScJ60v4i/qmKbNxcAcHOjK1mvtcOiyViCkJHG+qq+koVxTQ7se9hfXG6wLtVUpCQgKgkhuIUg1IwgkKoyOt9OKMhiJp+E1IwUOg3XazL3PA2pBIrp8mxhB/I75s9un7cBjOuv1ywZO3KfYLYD6hSq6RWeywRIVSH7vn2lAJtivrxBJAKDB6jZwQKBgQDN1YvgwSJYiFnSi+4S+Hoheh7qHHiXKaL4nubAhZlW1DKeh79OpUrbVMwHZdSQzf46TrFBnhQ0t4kFWgLxgqldY8lO/Hndly2i5c04EEEeCzcWaXcWv24omUMpbYD7JpRsj6JjS08zS2X8nkJjj2OgcUb4HcVCKof8Jj3i5GRzpwKBgQC2Wzt0MqRacDdWsCDTxP6nGJS8yA4oDBONFjdYaTuypqTM0SQ1XiyUvBwx1o79PZAhknb4/UXkMApj5e0BKmmtmNDypWgthQfPK7RamKu2FpcEMSnM4HSFvDYzWCrmddbWdGwm7FsU7W4FCMTKnMRBQ6K0UQPlEc1tnds6yW7ySwKBgQCZjbSjQBGaUGYJ7z/1QQ8DiHIlnoXL51Df/tMQTtp87yKwJ37tcdwtUc4/upTgTfxZjTkpRX+3cDA1INhPSXWF6RpV5X4YdF6kRqFZMK8TdbRr8NPZ0Yehm+yBrGJrenWBo4m2X4k/MAFuerX2RhNBryANm0/8M3RtBC5o5I+XwwKBgBCuWcrwUv5+42EHrYkRrSXF5t06A6mAKU2vqZJp1e8qtUqTGxfSrItShdW9Rck+l2+qwT1Xlcwg5OJshvijU5VwtDRuExCO6b72xYHAE30NpfTZNnSqV55gMCkUOKBqSSPG9Jm+5zoL2hOV0MKkAoPh1wFdo9iRf1Q2q3Y+NOrvAoGAU2RldX35DEub3EtWl70wEvNx1DuGPCBPL5OyMsJzJh3x5cpzHZc5B9TLjIT3YeRjKiaZxtJ/evulCw/ugScEFph4GtvJZgZYutVRO8LdsqYlb+1ikE/Hd9sqhp7PhDJhfQSV3uLGrfX5fd+bizNAr8swwAFxu35N8vdKOKHtnp8=";
@Value("${controller.path.prefix}" + UriConstants.API_V1)
private String pathPrefix;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private ReportService reportService;
@Captor
private ArgumentCaptor<ReportRequest> reportRequestArgumentCaptor;
@Test
void testWhenReportRequestWithMissingAuthenticationThenUNAUTHORIZED() throws JsonProcessingException {
......@@ -81,7 +101,7 @@ class CleaControllerAuthEnabledTest {
}
@Test
@DisplayName("unsecured apis should return 200 when called without auth toke")
@DisplayName("unsecured apis should return 200 when called without auth token")
void testUnsecuredApiWithoutToken() {
ResponseEntity<String> response = restTemplate.getForEntity("/actuator", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
......@@ -89,4 +109,39 @@ class CleaControllerAuthEnabledTest {
response = restTemplate.getForEntity("/swagger-ui/", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@DisplayName("secured apis should return 200 when called with a valid auth token")
void validRequestWithAuthEnabledAndValidToken() throws NoSuchAlgorithmException, InvalidKeySpecException {
Instant now = Instant.now();
Instant yesterday = now.minus(1, ChronoUnit.DAYS);
Instant tomorrow = now.plus(1, ChronoUnit.DAYS);
HttpHeaders headers = CleaControllerTest.newJsonHeader();
headers.setBearerAuth(this.newJwtToken(yesterday, tomorrow));
HttpEntity<ReportRequest> request = new HttpEntity<>(
new ReportRequest(List.of(new Visit("qr1", 1L)), 2L),
headers
);
ResponseEntity<String> response = restTemplate.postForEntity(pathPrefix + UriConstants.REPORT, request, String.class);
Mockito.verify(reportService).report(reportRequestArgumentCaptor.capture());
assertThat(reportRequestArgumentCaptor.getValue().getPivotDateAsNtpTimestamp()).isEqualTo(2L);
assertThat(reportRequestArgumentCaptor.getValue().getVisits().size()).isEqualTo(1);
assertThat(reportRequestArgumentCaptor.getValue().getVisits().get(0).getQrCode()).isEqualTo("qr1");
assertThat(reportRequestArgumentCaptor.getValue().getVisits().get(0).getQrCodeScanTimeAsNtpTimestamp()).isEqualTo(1L);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
private String newJwtToken(Instant now, Instant expiration) throws NoSuchAlgorithmException, InvalidKeySpecException {
SignatureAlgorithm rs256 = SignatureAlgorithm.RS256;
byte[] decoded = Decoders.BASE64.decode(privateJwtKet);
KeyFactory keyFactory = KeyFactory.getInstance(rs256.getFamilyName());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
return Jwts.builder()
.setHeaderParam("type", "JWT")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiration))
.signWith(keyFactory.generatePrivate(keySpec), rs256)
.compact();
}
}
......@@ -5,7 +5,7 @@ clea:
security:
report:
checkAuthorization: false
robertJwtPublicKey: TODO
robertJwtPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkp8sKCbrG1elGGG8n/Cq0OVeTA9V/M6ONKArheSEWEOJm/vW60tdQW7hJ+lCQ1Lgg4gLNL3Lp8Gu9R7+nWpCnKnwGWiL0BKWnG31SQ2O01/UMa2yWFJRJCbG49szgPt4PhvrWsNNOyO+Kz1HwO2sTfXkRpJ5dvqZF0Et2wqJy2UeVOzE//a/ZnuBzwuGfJEXBOT774eFEvQWnfX2ZEWOavimiPKsb46Hqg4LpmVR0Q7iH8Dj3D05Hn03O80692tmNjGl4nWn5P/b9YEdJIAWHcTBFiRMgbsKRG2KNAg+04rcZnx/w+xcNqskKvcR11TCzUuNPsKBCpZ4orAtr4O/7QIDAQAB
controller:
path:
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment