├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── codesandnotes │ └── secure │ └── rest │ └── spring │ └── tut │ ├── Application.java │ ├── configuration │ ├── ApplicationSecurity.java │ ├── SecurityConfiguration.java │ ├── cors │ │ └── CORSFilter.java │ ├── csrf │ │ ├── CSRF.java │ │ └── CsrfTokenResponseCookieBindingFilter.java │ └── rest │ │ ├── RESTAuthenticationEntryPoint.java │ │ ├── RESTAuthenticationSuccessHandler.java │ │ └── RESTLogoutSuccessHandler.java │ └── rest │ ├── Greetings.java │ ├── open │ └── OpenWebServices.java │ └── secure │ └── SecureWebServices.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secure-rest-spring-tut 2 | A secure REST layer using Spring, with Spring Security authentication, CORS and CSRF. For experimentation purposes, or to start new projects! 3 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | codesandnotes 8 | secure-rest-spring-tut 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | io.spring.platform 13 | platform-bom 14 | 1.1.2.RELEASE 15 | 16 | 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-jetty 23 | 24 | 25 | org.eclipse.jetty.websocket 26 | websocket-server 27 | 28 | 29 | org.eclipse.jetty.websocket 30 | javax-websocket-server-impl 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-logging 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-security 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-web 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-tomcat 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | org.apache.httpcomponents 65 | httpcore 66 | 4.3.3 67 | 68 | 69 | 70 | 71 | secure-rest-spring-tut 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-maven-plugin 77 | 78 | 79 | 80 | 81 | 82 | 1.8 83 | 84 | 85 | 86 | 87 | central 88 | https://repo1.maven.org/maven2 89 | 90 | true 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/Application.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut; 2 | 3 | import codesandnotes.secure.rest.spring.tut.configuration.ApplicationSecurity; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | 11 | @ComponentScan 12 | @Configuration 13 | @EnableAutoConfiguration 14 | public class Application { 15 | 16 | @Bean 17 | public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() { 18 | return new ApplicationSecurity(); 19 | } 20 | 21 | public static void main(String[] args) { 22 | SpringApplication application = new SpringApplication(Application.class); 23 | application.run(args); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/ApplicationSecurity.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration; 2 | 3 | import codesandnotes.secure.rest.spring.tut.configuration.cors.CORSFilter; 4 | import codesandnotes.secure.rest.spring.tut.configuration.csrf.CsrfTokenResponseCookieBindingFilter; 5 | import org.springframework.boot.autoconfigure.security.SecurityProperties; 6 | import org.springframework.core.annotation.Order; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.web.AuthenticationEntryPoint; 12 | import org.springframework.security.web.access.channel.ChannelProcessingFilter; 13 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 14 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 15 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 16 | import org.springframework.security.web.csrf.CsrfFilter; 17 | import org.springframework.security.web.util.matcher.AndRequestMatcher; 18 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 19 | import org.springframework.security.web.util.matcher.NegatedRequestMatcher; 20 | 21 | import javax.annotation.Resource; 22 | 23 | @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 24 | public class ApplicationSecurity extends WebSecurityConfigurerAdapter { 25 | 26 | @Resource 27 | private AuthenticationEntryPoint authenticationEntryPoint; 28 | @Resource 29 | private AuthenticationFailureHandler authenticationFailureHandler; 30 | @Resource 31 | private AuthenticationSuccessHandler authenticationSuccessHandler; 32 | @Resource 33 | private CORSFilter corsFilter; 34 | @Resource 35 | private LogoutSuccessHandler logoutSuccessHandler; 36 | 37 | @Override 38 | protected void configure(AuthenticationManagerBuilder builder) throws Exception { 39 | builder.inMemoryAuthentication().withUser("user").password("user").roles("USER").and().withUser("admin") 40 | .password("admin").roles("ADMIN"); 41 | } 42 | 43 | @Override 44 | protected void configure(HttpSecurity http) throws Exception { 45 | http.authorizeRequests() 46 | .antMatchers(HttpMethod.OPTIONS, "/*/**").permitAll() 47 | .antMatchers("/login", "/rest/open/**").permitAll() 48 | .antMatchers("/logout", "/rest/**").authenticated(); 49 | 50 | // Handlers and entry points 51 | http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); 52 | http.formLogin().successHandler(authenticationSuccessHandler); 53 | http.formLogin().failureHandler(authenticationFailureHandler); 54 | 55 | // Logout 56 | http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); 57 | 58 | // CORS 59 | http.addFilterBefore(corsFilter, ChannelProcessingFilter.class); 60 | 61 | // CSRF 62 | http.csrf().requireCsrfProtectionMatcher( 63 | new AndRequestMatcher( 64 | // Apply CSRF protection to all paths that do NOT match the ones below 65 | 66 | // We disable CSRF at login/logout, but only for OPTIONS methods 67 | new NegatedRequestMatcher(new AntPathRequestMatcher("/login*/**", HttpMethod.OPTIONS.toString())), 68 | new NegatedRequestMatcher(new AntPathRequestMatcher("/logout*/**", HttpMethod.OPTIONS.toString())), 69 | 70 | new NegatedRequestMatcher(new AntPathRequestMatcher("/rest*/**", HttpMethod.GET.toString())), 71 | new NegatedRequestMatcher(new AntPathRequestMatcher("/rest*/**", HttpMethod.HEAD.toString())), 72 | new NegatedRequestMatcher(new AntPathRequestMatcher("/rest*/**", HttpMethod.OPTIONS.toString())), 73 | new NegatedRequestMatcher(new AntPathRequestMatcher("/rest*/**", HttpMethod.TRACE.toString())), 74 | new NegatedRequestMatcher(new AntPathRequestMatcher("/rest/open*/**")) 75 | ) 76 | ); 77 | http.addFilterAfter(new CsrfTokenResponseCookieBindingFilter(), CsrfFilter.class); // CSRF tokens handling 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration; 2 | 3 | import codesandnotes.secure.rest.spring.tut.configuration.cors.CORSFilter; 4 | import codesandnotes.secure.rest.spring.tut.configuration.rest.RESTAuthenticationEntryPoint; 5 | import codesandnotes.secure.rest.spring.tut.configuration.rest.RESTAuthenticationSuccessHandler; 6 | import codesandnotes.secure.rest.spring.tut.configuration.rest.RESTLogoutSuccessHandler; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 10 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 11 | 12 | @Configuration 13 | public class SecurityConfiguration { 14 | 15 | @Bean 16 | public RESTAuthenticationEntryPoint authenticationEntryPoint() { 17 | return new RESTAuthenticationEntryPoint(); 18 | } 19 | 20 | @Bean 21 | public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() { 22 | return new SimpleUrlAuthenticationFailureHandler(); 23 | } 24 | 25 | @Bean 26 | public RESTAuthenticationSuccessHandler authenticationSuccessHandler() { 27 | return new RESTAuthenticationSuccessHandler(); 28 | } 29 | 30 | @Bean 31 | public CORSFilter corsFilter() { 32 | return new CORSFilter(); 33 | } 34 | 35 | @Bean 36 | public LogoutSuccessHandler logoutSuccessHandler() { 37 | return new RESTLogoutSuccessHandler(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/cors/CORSFilter.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration.cors; 2 | 3 | import codesandnotes.secure.rest.spring.tut.configuration.csrf.CSRF; 4 | 5 | import javax.servlet.Filter; 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.FilterConfig; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.ServletRequest; 10 | import javax.servlet.ServletResponse; 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.List; 16 | 17 | public class CORSFilter implements Filter { 18 | 19 | // This is to be replaced with a list of domains allowed to access the server 20 | private final List allowedOrigins = Arrays.asList("http://localhost:8080", "http://127.0.0.1:8080"); 21 | 22 | public void destroy() { 23 | } 24 | 25 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { 26 | 27 | // Lets make sure that we are working with HTTP (that is, against HttpServletRequest and HttpServletResponse objects) 28 | if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) { 29 | HttpServletRequest request = (HttpServletRequest) req; 30 | HttpServletResponse response = (HttpServletResponse) res; 31 | 32 | // Access-Control-Allow-Origin 33 | String origin = request.getHeader("Origin"); 34 | response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : ""); 35 | response.setHeader("Vary", "Origin"); 36 | 37 | // Access-Control-Max-Age 38 | response.setHeader("Access-Control-Max-Age", "3600"); 39 | 40 | // Access-Control-Allow-Credentials 41 | response.setHeader("Access-Control-Allow-Credentials", "true"); 42 | 43 | // Access-Control-Allow-Methods 44 | response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); 45 | 46 | // Access-Control-Allow-Headers 47 | response.setHeader("Access-Control-Allow-Headers", 48 | "Origin, X-Requested-With, Content-Type, Accept, " + CSRF.REQUEST_HEADER_NAME); 49 | } 50 | 51 | chain.doFilter(req, res); 52 | } 53 | 54 | public void init(FilterConfig filterConfig) { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/csrf/CSRF.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration.csrf; 2 | 3 | public class CSRF { 4 | /** 5 | * The name of the cookie with the CSRF token sent by the server as a response. 6 | */ 7 | public static final String RESPONSE_COOKIE_NAME = "CSRF-TOKEN"; 8 | /** 9 | * The name of the header carrying the CSRF token, expected in CSRF-protected requests to the server. 10 | */ 11 | public static final String REQUEST_HEADER_NAME = "X-CSRF-TOKEN"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/csrf/CsrfTokenResponseCookieBindingFilter.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration.csrf; 2 | 3 | import org.springframework.security.web.csrf.CsrfToken; 4 | import org.springframework.web.filter.OncePerRequestFilter; 5 | 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.Cookie; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.io.IOException; 12 | 13 | public class CsrfTokenResponseCookieBindingFilter extends OncePerRequestFilter { 14 | 15 | protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf"; 16 | 17 | @Override 18 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 19 | throws ServletException, IOException { 20 | 21 | CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME); 22 | 23 | Cookie cookie = new Cookie(CSRF.RESPONSE_COOKIE_NAME, token.getToken()); 24 | cookie.setPath("/"); 25 | 26 | response.addCookie(cookie); 27 | 28 | filterChain.doFilter(request, response); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/rest/RESTAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration.rest; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | /** 12 | * An authentication entry point implementation adapted to a REST approach. 13 | */ 14 | public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint { 15 | 16 | @Override 17 | public void commence(HttpServletRequest request, HttpServletResponse response, 18 | AuthenticationException authException) throws IOException, ServletException { 19 | 20 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/rest/RESTAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration.rest; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | /** 12 | * An authentication success handler implementation adapted to a REST approach. 13 | */ 14 | public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 15 | 16 | @Override 17 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 18 | Authentication authentication) throws IOException, ServletException { 19 | 20 | clearAuthenticationAttributes(request); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/configuration/rest/RESTLogoutSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.configuration.rest; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | /** 12 | * An authentication logout success handler implementation adapted to a REST approach. 13 | */ 14 | public class RESTLogoutSuccessHandler implements LogoutSuccessHandler { 15 | @Override 16 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) 17 | throws IOException, ServletException { 18 | 19 | // Do... absolutely... nothing! If you've reached this, the user session has been removed already! 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/rest/Greetings.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.rest; 2 | 3 | public class Greetings { 4 | 5 | private String greetings; 6 | 7 | public Greetings() { 8 | } 9 | 10 | public Greetings(String greetings) { 11 | this.greetings = greetings; 12 | } 13 | 14 | public String getGreetings() { 15 | return greetings; 16 | } 17 | 18 | public void setGreetings(String greetings) { 19 | this.greetings = greetings; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/rest/open/OpenWebServices.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.rest.open; 2 | 3 | import codesandnotes.secure.rest.spring.tut.rest.Greetings; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RequestMapping("/rest/open") 13 | @RestController 14 | public class OpenWebServices { 15 | 16 | @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 17 | public ResponseEntity getGreetings() { 18 | Greetings greetings = new Greetings("Hello, open REST!"); 19 | return new ResponseEntity<>(greetings, HttpStatus.OK); 20 | } 21 | 22 | @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) 23 | public ResponseEntity postGreetings(@RequestBody Greetings greetings) { 24 | System.out.println("Greetings have been OPENLY posted. They say: " + greetings.getGreetings()); 25 | return new ResponseEntity(HttpStatus.OK); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/codesandnotes/secure/rest/spring/tut/rest/secure/SecureWebServices.java: -------------------------------------------------------------------------------- 1 | package codesandnotes.secure.rest.spring.tut.rest.secure; 2 | 3 | import codesandnotes.secure.rest.spring.tut.rest.Greetings; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RequestMapping("/rest/secure") 13 | @RestController 14 | public class SecureWebServices { 15 | 16 | @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 17 | public ResponseEntity getGreetings() { 18 | Greetings greetings = new Greetings("Hello, secure REST!"); 19 | return new ResponseEntity<>(greetings, HttpStatus.OK); 20 | } 21 | 22 | @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) 23 | public ResponseEntity postGreetings(@RequestBody Greetings greetings) { 24 | System.out.println("Greetings have been SECURELY posted. They say: " + greetings.getGreetings()); 25 | return new ResponseEntity(HttpStatus.OK); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | 3 | logging.level.org.springframework=INFO 4 | logging.level.org.springframework.security=INFO 5 | 6 | server.port=8081 7 | --------------------------------------------------------------------------------