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