├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── be │ │ └── codesandnotes │ │ ├── Application.java │ │ ├── greetings │ │ ├── GreetingWebObject.java │ │ └── GreetingsRest.java │ │ ├── security │ │ ├── RESTAuthenticationEntryPoint.java │ │ ├── SecurityConfiguration.java │ │ ├── StatelessCsrfFilter.java │ │ ├── TokenBasedAuthenticationFilter.java │ │ └── TokenBasedAuthorizationFilter.java │ │ └── users │ │ ├── MyUserDetailsService.java │ │ └── UsersConfiguration.java └── resources │ └── application.properties └── test └── java └── be └── codesandnotes ├── IntegrationTestsApplication.java ├── TestRestClient.java ├── greetings └── GreetingsRestTest.java └── security └── SecurityConfigurationTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.project 3 | /jwt-rest-spring.iml 4 | /target/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 codesandnotes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jwt-rest-spring 2 | 3 | This code is a companion for the tutorial available [on my blog](https://codesandnotes.be), which is divided in multiple parts: 4 | 5 | * [Introduction](https://www.codesandnotes.be/2017/09/25/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-1-intro/) 6 | * [Session-based authentication](https://www.codesandnotes.be/2017/10/02/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-2-session-based-authentication/) 7 | * [Token-based authentication](https://www.codesandnotes.be/2017/10/09/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-3-token-based-authentication/) 8 | * [JWT-based authentication](https://www.codesandnotes.be/2017/10/16/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-4-jwt-based-authentication/) 9 | * [Stateless CSRF](https://www.codesandnotes.be/2017/10/23/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-5-stateless-csrf/) 10 | * [Should I go stateless?](https://www.codesandnotes.be/2017/11/17/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-6-should-i-go-stateless/) 11 | * [Reference material](https://www.codesandnotes.be/2017/12/18/from-stateful-to-stateless-restful-security-using-spring-and-jwts-part-7-reference-material/) 12 | 13 | Enjoy! 14 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | be.codesandnotes 8 | jwt-rest-spring 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 1.8 13 | 14 | 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-jetty 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-tomcat 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-security 39 | 40 | 41 | io.jsonwebtoken 42 | jjwt 43 | 0.7.0 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-logging 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-test 56 | test 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-dependencies 65 | 1.5.6.RELEASE 66 | pom 67 | import 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-compiler-plugin 77 | 78 | 1.8 79 | 1.8 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/Application.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | // SpringBootApplication conveniently declares @Configuration, @EnableAutoConfiguration and @ComponentScan. 7 | @SpringBootApplication 8 | public class Application { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(Application.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/greetings/GreetingWebObject.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.greetings; 2 | 3 | public class GreetingWebObject { 4 | private String message; 5 | 6 | public GreetingWebObject() { 7 | } 8 | 9 | public GreetingWebObject(String message) { 10 | super(); 11 | this.message = message; 12 | } 13 | 14 | public String getMessage() { 15 | return message; 16 | } 17 | 18 | public void setMessage(String message) { 19 | this.message = message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/greetings/GreetingsRest.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.greetings; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.security.Principal; 8 | 9 | import static org.springframework.http.MediaType.*; 10 | import static org.springframework.web.bind.annotation.RequestMethod.*; 11 | 12 | @RestController 13 | @RequestMapping("/rest") 14 | public class GreetingsRest { 15 | 16 | // Make sure you don't use "consumes" in the RequestMapping, otherwise one gets 415 codes 17 | @RequestMapping(path = "/unsecure/greetings", method = GET, produces = APPLICATION_JSON_VALUE) 18 | ResponseEntity unsecuredGreetings() { 19 | return ResponseEntity.ok(new GreetingWebObject("Greetings and salutations!")); 20 | } 21 | 22 | @RequestMapping(path = "/secure/greetings", method = GET, produces = APPLICATION_JSON_VALUE) 23 | ResponseEntity securedGreetings(Principal principal) { 24 | return ResponseEntity.ok(new GreetingWebObject("Greetings and salutations, " + principal.getName() + "!")); 25 | } 26 | 27 | @RequestMapping(path = "/secure/greetings", method = POST, produces = APPLICATION_JSON_VALUE) 28 | ResponseEntity securedGreetingsPost(Principal principal) { 29 | return ResponseEntity.ok(new GreetingWebObject("Greetings and POST-salutations, " + principal.getName() + "!")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/security/RESTAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.security; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | @Component 13 | public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint { 14 | 15 | @Override 16 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) 17 | throws IOException, ServletException { 18 | 19 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/security/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.security; 2 | 3 | import be.codesandnotes.users.MyUserDetailsService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.security.web.AuthenticationEntryPoint; 13 | import org.springframework.security.web.access.AccessDeniedHandler; 14 | import org.springframework.security.web.access.AccessDeniedHandlerImpl; 15 | import org.springframework.security.web.csrf.CsrfFilter; 16 | 17 | import javax.annotation.Resource; 18 | import java.security.NoSuchAlgorithmException; 19 | import java.util.Base64; 20 | 21 | @Configuration 22 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 23 | 24 | public static final String CSRF_COOKIE = "CSRF-TOKEN"; 25 | public static final String CSRF_HEADER = "X-CSRF-TOKEN"; 26 | 27 | static final long TOKEN_LIFETIME = 604_800_000; 28 | static final String TOKEN_PREFIX = "Bearer "; 29 | static final String TOKEN_SECRET = Base64.getEncoder().encodeToString("ThisIsOurSecretKeyToSignOurTokens".getBytes()); 30 | 31 | @Resource 32 | private AuthenticationEntryPoint authenticationEntryPoint; 33 | 34 | private StatelessCsrfFilter statelessCsrfFilter = new StatelessCsrfFilter(); 35 | 36 | @Resource 37 | private MyUserDetailsService myUserDetailsService; 38 | 39 | @Bean 40 | public AccessDeniedHandler accessDeniedHandler() { 41 | return new AccessDeniedHandlerImpl(); 42 | } 43 | 44 | @Bean 45 | public PasswordEncoder passwordEncoder() throws NoSuchAlgorithmException { 46 | return new BCryptPasswordEncoder(); 47 | } 48 | 49 | @Autowired 50 | protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { 51 | authenticationManagerBuilder.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); 52 | } 53 | 54 | protected void configure(HttpSecurity http) throws Exception { 55 | 56 | http 57 | .csrf().disable() 58 | .addFilterBefore(statelessCsrfFilter, CsrfFilter.class); 59 | 60 | http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); 61 | 62 | http 63 | .httpBasic().disable() 64 | .formLogin().disable() 65 | .logout().disable() 66 | .addFilter(new TokenBasedAuthenticationFilter(authenticationManager())) 67 | .addFilter(new TokenBasedAuthorizationFilter(authenticationManager())); 68 | 69 | http.authorizeRequests() 70 | .antMatchers("/rest/unsecure/**").permitAll() 71 | .antMatchers("/**").authenticated(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/security/StatelessCsrfFilter.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.security; 2 | 3 | import org.springframework.security.access.AccessDeniedException; 4 | import org.springframework.security.web.access.AccessDeniedHandler; 5 | import org.springframework.security.web.access.AccessDeniedHandlerImpl; 6 | import org.springframework.web.filter.OncePerRequestFilter; 7 | 8 | import javax.servlet.FilterChain; 9 | import javax.servlet.ServletException; 10 | import javax.servlet.http.Cookie; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | import java.util.Arrays; 15 | import java.util.HashSet; 16 | import java.util.Optional; 17 | import java.util.Set; 18 | 19 | import static be.codesandnotes.security.SecurityConfiguration.*; 20 | 21 | public class StatelessCsrfFilter extends OncePerRequestFilter { 22 | 23 | private static final Set SAFE_METHODS = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); 24 | 25 | private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); 26 | 27 | @Override 28 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 29 | throws ServletException, IOException { 30 | 31 | if (csrfTokenIsRequired(request)) { 32 | String csrfHeaderToken = request.getHeader(CSRF_HEADER); 33 | 34 | String csrfCookieToken = null; 35 | Cookie[] cookies = request.getCookies(); 36 | if (cookies != null) { 37 | Optional csrfCookie = Arrays.stream(cookies) 38 | .filter(cookie -> cookie.getName().equals(CSRF_COOKIE)) 39 | .findFirst(); 40 | if (csrfCookie.isPresent()) { 41 | csrfCookieToken = csrfCookie.get().getValue(); 42 | } 43 | } 44 | 45 | if (csrfHeaderToken == null || csrfCookieToken == null || !csrfCookieToken.equals(csrfHeaderToken)) { 46 | accessDeniedHandler.handle(request, response, new AccessDeniedException("CSRF tokens missing or not matching")); 47 | return; 48 | } 49 | 50 | } 51 | 52 | filterChain.doFilter(request, response); 53 | } 54 | 55 | private boolean csrfTokenIsRequired(HttpServletRequest request) { 56 | return !SAFE_METHODS.contains(request.getMethod()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/security/TokenBasedAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.security; 2 | 3 | import io.jsonwebtoken.Jwts; 4 | import io.jsonwebtoken.SignatureAlgorithm; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.userdetails.User; 9 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 10 | 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | import java.util.Date; 17 | import java.util.UUID; 18 | 19 | import static be.codesandnotes.security.SecurityConfiguration.*; 20 | 21 | public class TokenBasedAuthenticationFilter extends UsernamePasswordAuthenticationFilter { 22 | 23 | TokenBasedAuthenticationFilter(AuthenticationManager authenticationManager) { 24 | setAuthenticationManager(authenticationManager); 25 | } 26 | 27 | @Override 28 | protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 29 | Authentication authentication) throws IOException, ServletException { 30 | String token = Jwts.builder() 31 | .setId(UUID.randomUUID().toString()) 32 | .setSubject(((User) authentication.getPrincipal()).getUsername()) 33 | .setExpiration(new Date(System.currentTimeMillis() + TOKEN_LIFETIME)) 34 | .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET) 35 | .compact(); 36 | 37 | response.addHeader(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/security/TokenBasedAuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.security; 2 | 3 | import io.jsonwebtoken.Jwts; 4 | import org.springframework.http.HttpHeaders; 5 | import org.springframework.security.authentication.AuthenticationManager; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 9 | 10 | import javax.servlet.FilterChain; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | import java.util.Collections; 16 | 17 | import static be.codesandnotes.security.SecurityConfiguration.*; 18 | 19 | public class TokenBasedAuthorizationFilter extends BasicAuthenticationFilter { 20 | 21 | TokenBasedAuthorizationFilter(AuthenticationManager authenticationManager) { 22 | super(authenticationManager); 23 | } 24 | 25 | @Override 26 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 27 | throws IOException, ServletException { 28 | 29 | String authorizationToken = request.getHeader(HttpHeaders.AUTHORIZATION); 30 | 31 | if (authorizationToken != null && authorizationToken.startsWith(TOKEN_PREFIX)) { 32 | authorizationToken = authorizationToken.replaceFirst(TOKEN_PREFIX, ""); 33 | 34 | String username = Jwts.parser() 35 | .setSigningKey(TOKEN_SECRET) 36 | .parseClaimsJws(authorizationToken) 37 | .getBody() 38 | .getSubject(); 39 | 40 | SecurityContextHolder.getContext() 41 | .setAuthentication(new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList())); 42 | } 43 | 44 | chain.doFilter(request, response); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/users/MyUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.users; 2 | 3 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 4 | import org.springframework.security.core.userdetails.User; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | 10 | import java.util.Collections; 11 | 12 | public class MyUserDetailsService implements UserDetailsService { 13 | 14 | private PasswordEncoder passwordEncoder; 15 | 16 | public MyUserDetailsService() { 17 | } 18 | 19 | public MyUserDetailsService(PasswordEncoder passwordEncoder) { 20 | this.passwordEncoder = passwordEncoder; 21 | } 22 | 23 | @Override 24 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 25 | if (!username.equals("user")) { 26 | throw new UsernameNotFoundException("not found"); 27 | } 28 | 29 | return new User("user", passwordEncoder.encode("password"), Collections.singleton(new SimpleGrantedAuthority("USER"))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/be/codesandnotes/users/UsersConfiguration.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.users; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.password.PasswordEncoder; 6 | 7 | import javax.annotation.Resource; 8 | 9 | @Configuration 10 | public class UsersConfiguration { 11 | 12 | @Resource 13 | private PasswordEncoder passwordEncoder; 14 | 15 | @Bean 16 | public MyUserDetailsService myUserDetailsService() { 17 | return new MyUserDetailsService(passwordEncoder); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codesandnotes/jwt-rest-spring/54cf759c2e06bde7796d75dbe3e97344f37c3e0b/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/test/java/be/codesandnotes/IntegrationTestsApplication.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.web.client.RestTemplateBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.FilterType; 9 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 10 | 11 | /** 12 | * Prepares the Integration tests' context.
13 | *
14 | * We do exclude the scan of Application.class, so that we can specify an alternative Spring Boot Application context 15 | * for these tests, while preventing Spring Boot from loading the original Application context. 16 | */ 17 | @ComponentScan( 18 | basePackages = "be.codesandnotes", 19 | excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Application.class)} 20 | ) 21 | @Configuration 22 | @EnableAutoConfiguration 23 | public class IntegrationTestsApplication { 24 | 25 | @Bean 26 | public RestTemplateBuilder restTemplateBuilder() { 27 | return new RestTemplateBuilder().detectRequestFactory(false).requestFactory(SimpleClientHttpRequestFactory.class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/be/codesandnotes/TestRestClient.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes; 2 | 3 | import be.codesandnotes.security.SecurityConfiguration; 4 | import org.springframework.boot.test.web.client.TestRestTemplate; 5 | import org.springframework.http.HttpEntity; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.ResponseEntity; 8 | 9 | import java.io.OutputStream; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | import static org.springframework.http.HttpMethod.*; 14 | 15 | public class TestRestClient { 16 | 17 | private TestRestTemplate rest; 18 | 19 | public TestRestClient(TestRestTemplate testRestTemplate) { 20 | this.rest = testRestTemplate; 21 | } 22 | 23 | public ResponseEntity get(String restPath, Credentials credentials, Class responseType) { 24 | 25 | HttpHeaders headers = new HttpHeaders(); 26 | headers.add(HttpHeaders.AUTHORIZATION, credentials.token); 27 | 28 | return rest.exchange(restPath, GET, new HttpEntity<>(headers), responseType); 29 | } 30 | 31 | public ResponseEntity post(String restPath, Credentials credentials, Object body, Class responseType, String csrfToken) { 32 | HttpHeaders headers = new HttpHeaders(); 33 | headers.add(HttpHeaders.AUTHORIZATION, credentials.token); 34 | if (csrfToken != null) { 35 | headers.set(HttpHeaders.COOKIE, SecurityConfiguration.CSRF_COOKIE + "=" + csrfToken); 36 | headers.set(SecurityConfiguration.CSRF_HEADER, csrfToken); 37 | } 38 | 39 | return rest.exchange(restPath, POST, new HttpEntity<>(body, headers), responseType); 40 | } 41 | 42 | public Credentials login(String username, String password) { 43 | 44 | return rest.execute( 45 | "/login", 46 | POST, 47 | request -> { 48 | // Body 49 | OutputStream body = request.getBody(); 50 | body.write(("username=" + username + "&password=" + password).getBytes()); 51 | body.flush(); 52 | body.close(); 53 | 54 | // Headers 55 | HttpHeaders headers = request.getHeaders(); 56 | String csrfToken = UUID.randomUUID().toString(); 57 | headers.set(HttpHeaders.COOKIE, SecurityConfiguration.CSRF_COOKIE + "=" + csrfToken); 58 | headers.set(SecurityConfiguration.CSRF_HEADER, csrfToken); 59 | 60 | }, response -> { 61 | Credentials credentials = null; 62 | 63 | List authorizationTokens = response.getHeaders().get(HttpHeaders.AUTHORIZATION); 64 | if (authorizationTokens != null && authorizationTokens.size() > 0) { 65 | credentials = new Credentials(authorizationTokens.get(0)); 66 | } 67 | 68 | return credentials; 69 | } 70 | ); 71 | } 72 | 73 | public static class Credentials { 74 | public String token; 75 | public Credentials(String token) { 76 | this.token = token; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/be/codesandnotes/greetings/GreetingsRestTest.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.greetings; 2 | 3 | import be.codesandnotes.TestRestClient; 4 | import be.codesandnotes.IntegrationTestsApplication; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import javax.annotation.Resource; 15 | 16 | import java.util.UUID; 17 | 18 | import static org.junit.Assert.*; 19 | 20 | @SpringBootTest( 21 | classes = IntegrationTestsApplication.class, 22 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 23 | ) 24 | @RunWith(SpringRunner.class) 25 | public class GreetingsRestTest { 26 | 27 | private static final String USERNAME = "user"; 28 | private static final String PASSWORD = "password"; 29 | private static final String CSRF_TOKEN = UUID.randomUUID().toString(); 30 | private static final Object BODY = null; 31 | 32 | @Resource 33 | private TestRestTemplate restTemplate; 34 | 35 | private TestRestClient restClient; 36 | 37 | @Before 38 | public void init() { 39 | restClient = new TestRestClient(restTemplate); 40 | } 41 | 42 | @Test 43 | public void returnUnsecuredGreetings() { 44 | 45 | ResponseEntity response 46 | = restTemplate.getForEntity("/rest/unsecure/greetings", GreetingWebObject.class); 47 | 48 | assertEquals(HttpStatus.OK, response.getStatusCode()); 49 | assertEquals("Greetings and salutations!", response.getBody().getMessage()); 50 | } 51 | 52 | @Test 53 | public void returnSecureGreetings() { 54 | 55 | TestRestClient.Credentials credentials = restClient.login(USERNAME, PASSWORD); 56 | ResponseEntity response = restClient.get("/rest/secure/greetings", credentials, GreetingWebObject.class); 57 | 58 | assertEquals(HttpStatus.OK, response.getStatusCode()); 59 | assertEquals("Greetings and salutations, user!", response.getBody().getMessage()); 60 | } 61 | 62 | @Test 63 | public void protectAccessToSecureGreetingsWhenUserIsNotLogged() { 64 | 65 | ResponseEntity response = restTemplate.getForEntity("/rest/secure/greetings", GreetingWebObject.class); 66 | 67 | assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); 68 | } 69 | 70 | @Test 71 | public void protectAccessToSecurePOSTGreetingWhenUserIsNotLogged() { 72 | 73 | TestRestClient.Credentials credentials = new TestRestClient.Credentials(null); 74 | ResponseEntity response = restClient.get("/rest/secure/greetings", credentials, GreetingWebObject.class); 75 | 76 | assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); 77 | } 78 | 79 | @Test 80 | public void protectAccessToSecurePOSTGreetingWhenCSRFTokensAreNotSent() { 81 | 82 | TestRestClient.Credentials credentials = restClient.login(USERNAME, PASSWORD); 83 | ResponseEntity response = restClient.post("/rest/secure/greetings", credentials, 84 | BODY, GreetingWebObject.class, null); 85 | 86 | assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); 87 | } 88 | 89 | @Test 90 | public void returnSecurePOSTGreetings() { 91 | 92 | TestRestClient.Credentials credentials = restClient.login(USERNAME, PASSWORD); 93 | ResponseEntity response = restClient.post("/rest/secure/greetings", credentials, 94 | BODY, GreetingWebObject.class, CSRF_TOKEN); 95 | 96 | assertEquals(HttpStatus.OK, response.getStatusCode()); 97 | assertEquals("Greetings and POST-salutations, user!", response.getBody().getMessage()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/be/codesandnotes/security/SecurityConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package be.codesandnotes.security; 2 | 3 | import be.codesandnotes.IntegrationTestsApplication; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.web.client.TestRestTemplate; 8 | import org.springframework.http.HttpHeaders; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.client.ClientHttpResponse; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import javax.annotation.Resource; 14 | import java.io.IOException; 15 | import java.io.OutputStream; 16 | import java.util.List; 17 | import java.util.UUID; 18 | 19 | import static org.junit.Assert.*; 20 | import static org.springframework.http.HttpMethod.*; 21 | 22 | @SpringBootTest( 23 | classes = IntegrationTestsApplication.class, 24 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 25 | ) 26 | @RunWith(SpringRunner.class) 27 | public class SecurityConfigurationTest { 28 | 29 | private static final String USERNAME = "user"; 30 | private static final String PASSWORD = "password"; 31 | private static final String CSRF_TOKEN = UUID.randomUUID().toString(); 32 | private static final String DIFFERENT_CSRF_TOKEN = UUID.randomUUID().toString(); 33 | 34 | @Resource 35 | private TestRestTemplate rest; 36 | 37 | @Test 38 | public void authenticateAnExistingUser() throws IOException { 39 | 40 | ClientHttpResponse clientHttpResponse = login(USERNAME, PASSWORD, CSRF_TOKEN); 41 | 42 | assertNotNull(clientHttpResponse); 43 | assertEquals(HttpStatus.OK, clientHttpResponse.getStatusCode()); 44 | 45 | assertEquals(true, clientHttpResponse.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)); 46 | List authorizationTokens = clientHttpResponse.getHeaders().get(HttpHeaders.AUTHORIZATION); 47 | assertEquals(1, authorizationTokens.size()); 48 | 49 | String authorizationToken = authorizationTokens.get(0); 50 | assertEquals(true, authorizationToken.startsWith("Bearer ")); 51 | } 52 | 53 | @Test 54 | public void blockAuthenticationWhenCSRFTokensAreAbsent() throws IOException { 55 | 56 | ClientHttpResponse clientHttpResponse = login(USERNAME, PASSWORD); 57 | 58 | assertNotNull(clientHttpResponse); 59 | assertEquals(HttpStatus.FORBIDDEN, clientHttpResponse.getStatusCode()); 60 | } 61 | 62 | @Test 63 | public void blockAuthenticationWhenCSRFTokenIsAbsentFromHeader() throws IOException { 64 | 65 | ClientHttpResponse clientHttpResponse = login(USERNAME, PASSWORD, null, CSRF_TOKEN); 66 | 67 | assertNotNull(clientHttpResponse); 68 | assertEquals(HttpStatus.FORBIDDEN, clientHttpResponse.getStatusCode()); 69 | } 70 | 71 | @Test 72 | public void blockAuthenticationWhenCSRFTokenIsAbsentFromCookie() throws IOException { 73 | 74 | ClientHttpResponse clientHttpResponse = login(USERNAME, PASSWORD, CSRF_TOKEN, null); 75 | 76 | assertNotNull(clientHttpResponse); 77 | assertEquals(HttpStatus.FORBIDDEN, clientHttpResponse.getStatusCode()); 78 | } 79 | 80 | @Test 81 | public void blockAuthenticationWhenCSRFTokensDoNotMatch() throws IOException { 82 | 83 | ClientHttpResponse clientHttpResponse = login(USERNAME, PASSWORD, CSRF_TOKEN, DIFFERENT_CSRF_TOKEN); 84 | 85 | assertNotNull(clientHttpResponse); 86 | assertEquals(HttpStatus.FORBIDDEN, clientHttpResponse.getStatusCode()); 87 | } 88 | 89 | private ClientHttpResponse login(String username, String password) { 90 | return rest.execute( 91 | "/login", 92 | POST, 93 | request -> { 94 | OutputStream body = request.getBody(); 95 | body.write(("username=" + username + "&password=" + password).getBytes()); 96 | body.flush(); 97 | body.close(); 98 | }, 99 | response -> response 100 | ); 101 | } 102 | 103 | private ClientHttpResponse login(String username, String password, String csrfToken) { 104 | return login(username, password, csrfToken, csrfToken); 105 | } 106 | 107 | private ClientHttpResponse login(String username, String password, String csrfHeaderToken, String csrfCookieToken) { 108 | return rest.execute( 109 | "/login", 110 | POST, 111 | request -> { 112 | // Body 113 | OutputStream body = request.getBody(); 114 | body.write(("username=" + username + "&password=" + password).getBytes()); 115 | body.flush(); 116 | body.close(); 117 | 118 | // Headers 119 | HttpHeaders headers = request.getHeaders(); 120 | if (csrfHeaderToken != null) { 121 | headers.set(HttpHeaders.COOKIE, SecurityConfiguration.CSRF_COOKIE + "=" + csrfHeaderToken); 122 | } 123 | if (csrfCookieToken != null) { 124 | headers.set(SecurityConfiguration.CSRF_HEADER, csrfCookieToken); 125 | } 126 | }, 127 | response -> response 128 | ); 129 | } 130 | } 131 | --------------------------------------------------------------------------------