From 14e5f9a458317f4c6c85dccbfefe7b95d09a3e58 Mon Sep 17 00:00:00 2001 From: justin flanagan Date: Sun, 1 May 2022 21:24:06 +0100 Subject: [PATCH] Added spring boot integration test which tests the login filter and also the JWT filet. Also refactored the JwtTokenVerifierFilter so it extends AbstractAuthenticationProcessingFilter --- pom.xml | 13 ++++ .../demo/jwt/InvalidJwtTokenException.java | 9 +++ ...ifier.java => JwtTokenVerifierFilter.java} | 50 +++++++-------- ...ernameAndPasswordAuthenticationFilter.java | 6 +- .../security/ApplicationSecurityConfig.java | 8 ++- .../demo/student/StudentControllerTest.java | 63 +++++++++++++++++++ 6 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/example/demo/jwt/InvalidJwtTokenException.java rename src/main/java/com/example/demo/jwt/{JwtTokenVerifier.java => JwtTokenVerifierFilter.java} (52%) create mode 100644 src/test/java/com/example/demo/student/StudentControllerTest.java diff --git a/pom.xml b/pom.xml index e05603c..5423d74 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,19 @@ + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + diff --git a/src/main/java/com/example/demo/jwt/InvalidJwtTokenException.java b/src/main/java/com/example/demo/jwt/InvalidJwtTokenException.java new file mode 100644 index 0000000..4667da6 --- /dev/null +++ b/src/main/java/com/example/demo/jwt/InvalidJwtTokenException.java @@ -0,0 +1,9 @@ +package com.example.demo.jwt; + +import org.springframework.security.core.AuthenticationException; + +public class InvalidJwtTokenException extends AuthenticationException { + public InvalidJwtTokenException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/src/main/java/com/example/demo/jwt/JwtTokenVerifier.java b/src/main/java/com/example/demo/jwt/JwtTokenVerifierFilter.java similarity index 52% rename from src/main/java/com/example/demo/jwt/JwtTokenVerifier.java rename to src/main/java/com/example/demo/jwt/JwtTokenVerifierFilter.java index f4aebae..015bc52 100644 --- a/src/main/java/com/example/demo/jwt/JwtTokenVerifier.java +++ b/src/main/java/com/example/demo/jwt/JwtTokenVerifierFilter.java @@ -3,14 +3,12 @@ import com.google.common.base.Strings; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import javax.crypto.SecretKey; import javax.servlet.FilterChain; @@ -23,29 +21,23 @@ import java.util.Set; import java.util.stream.Collectors; -public class JwtTokenVerifier extends OncePerRequestFilter { +public class JwtTokenVerifierFilter extends AbstractAuthenticationProcessingFilter { private final SecretKey secretKey; private final JwtConfig jwtConfig; - public JwtTokenVerifier(SecretKey secretKey, - JwtConfig jwtConfig) { + public JwtTokenVerifierFilter(String url, + SecretKey secretKey, + JwtConfig jwtConfig) { + super(url); this.secretKey = secretKey; this.jwtConfig = jwtConfig; } @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { String authorizationHeader = request.getHeader(jwtConfig.getAuthorizationHeader()); - - if (Strings.isNullOrEmpty(authorizationHeader) || !authorizationHeader.startsWith(jwtConfig.getTokenPrefix())) { - filterChain.doFilter(request, response); - return; - } - String token = authorizationHeader.replace(jwtConfig.getTokenPrefix(), ""); try { @@ -64,18 +56,26 @@ protected void doFilterInternal(HttpServletRequest request, .map(m -> new SimpleGrantedAuthority(m.get("authority"))) .collect(Collectors.toSet()); - Authentication authentication = new UsernamePasswordAuthenticationToken( - username, - null, - simpleGrantedAuthorities - ); + return new UsernamePasswordAuthenticationToken(username,null, simpleGrantedAuthorities); - SecurityContextHolder.getContext().setAuthentication(authentication); - - } catch (JwtException e) { - throw new IllegalStateException(String.format("Token %s cannot be trusted", token)); + } catch (Exception e) { + throw new InvalidJwtTokenException(String.format("Token %s cannot be trusted", token), e); } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + SecurityContextHolder.getContext().setAuthentication(authResult); + chain.doFilter(request, response); + } + + @Override + protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { + return super.requiresAuthentication(request, response) && containsToken(request); + } - filterChain.doFilter(request, response); + private boolean containsToken(HttpServletRequest request) { + String authorizationHeader = request.getHeader(jwtConfig.getAuthorizationHeader()); + return !Strings.isNullOrEmpty(authorizationHeader) && authorizationHeader.startsWith(jwtConfig.getTokenPrefix()); } } diff --git a/src/main/java/com/example/demo/jwt/JwtUsernameAndPasswordAuthenticationFilter.java b/src/main/java/com/example/demo/jwt/JwtUsernameAndPasswordAuthenticationFilter.java index b00379f..bc1d02c 100644 --- a/src/main/java/com/example/demo/jwt/JwtUsernameAndPasswordAuthenticationFilter.java +++ b/src/main/java/com/example/demo/jwt/JwtUsernameAndPasswordAuthenticationFilter.java @@ -35,7 +35,6 @@ public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authenti @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - try { UsernameAndPasswordAuthenticationRequest authenticationRequest = new ObjectMapper() .readValue(request.getInputStream(), UsernameAndPasswordAuthenticationRequest.class); @@ -45,15 +44,14 @@ public Authentication attemptAuthentication(HttpServletRequest request, authenticationRequest.getPassword() ); - Authentication authenticate = authenticationManager.authenticate(authentication); - return authenticate; + return authenticationManager.authenticate(authentication); } catch (IOException e) { throw new RuntimeException(e); } - } + @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, diff --git a/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java b/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java index c9ad882..267058c 100644 --- a/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java +++ b/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java @@ -2,7 +2,7 @@ import com.example.demo.auth.ApplicationUserService; import com.example.demo.jwt.JwtConfig; -import com.example.demo.jwt.JwtTokenVerifier; +import com.example.demo.jwt.JwtTokenVerifierFilter; import com.example.demo.jwt.JwtUsernameAndPasswordAuthenticationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -26,6 +26,8 @@ @EnableGlobalMethodSecurity(prePostEnabled = true) public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { + private static final String SECURE_URL_PATTERN = "/api/**"; + private final PasswordEncoder passwordEncoder; private final ApplicationUserService applicationUserService; private final SecretKey secretKey; @@ -50,10 +52,10 @@ protected void configure(HttpSecurity http) throws Exception { .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig, secretKey)) - .addFilterAfter(new JwtTokenVerifier(secretKey, jwtConfig),JwtUsernameAndPasswordAuthenticationFilter.class) + .addFilterAfter(new JwtTokenVerifierFilter(SECURE_URL_PATTERN, secretKey, jwtConfig),JwtUsernameAndPasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/", "index", "/css/*", "/js/*").permitAll() - .antMatchers("/api/**").hasRole(STUDENT.name()) + .antMatchers(SECURE_URL_PATTERN).hasRole(STUDENT.name()) .anyRequest() .authenticated(); } diff --git a/src/test/java/com/example/demo/student/StudentControllerTest.java b/src/test/java/com/example/demo/student/StudentControllerTest.java new file mode 100644 index 0000000..166fa1a --- /dev/null +++ b/src/test/java/com/example/demo/student/StudentControllerTest.java @@ -0,0 +1,63 @@ +package com.example.demo.student; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class StudentControllerTest { + + private static final String URL = "api/v1/students/"; + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @BeforeEach + public void setup() { + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @ParameterizedTest + @CsvSource({"annasmith,password,200,true", "fred,password123,401,false"}) + public void testOnlyCorrectlyAuthenticatedUsersCanObtainJwtToken(String username, String password, int statusCode, boolean checkToken) throws Exception { + String jWtToken = performLogin(username, password, statusCode); + if(checkToken) { + assertThat(jWtToken.startsWith("Bearer")); + } else { + assertThat(jWtToken).isNullOrEmpty(); + } + } + + @ParameterizedTest + @CsvSource({"annasmith,password,200", "linda,password,403", "tom,password,403"}) + public void testOnlyAuthenticatedUserWithStudentRoleCanAccessAPI(String username, String password, int statusCode) throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/api/v1/students/1") + .header("Authorization", performLogin(username, password, 200))) + .andExpect(status().is(statusCode)); + } + + private String performLogin(String username, String password, int expectedStatusCode) throws Exception { + String body = "{\"username\":\"" + username + "\", \"password\":\"" + password + "\"}"; + + MvcResult result = mvc.perform(post("/login").content(body)).andExpect(status().is(expectedStatusCode)).andReturn(); + + return result.getResponse().getHeader("Authorization"); + } +} \ No newline at end of file