├── .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 |
--------------------------------------------------------------------------------