1. Introduction
This documentation contains some help to examples from spring-security-examples repository. It’s contains some spring-security playground projects
2. CSRF Protection with Single Page Apps using JS
user / password can’t do post admin / admin can
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Autowired
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER")
.and()
.withUser("admin")
.password("admin")
.roles("ADMIN");
}
@Override
public void configure(final WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(
"/favicon.ico",
"/webjars/**",
"/login.html",
"/index.html",
"/logout.html"
);
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(POST)
.hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
.invalidateHttpSession(false)
.permitAll()
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER)
;
}
}
@GetMapping("/logout")
public String logoutGet(final HttpServletRequest request, final HttpServletResponse response) {
return logout(request, response);
}
@PostMapping("/logout")
public String logoutPost(final HttpServletRequest request, final HttpServletResponse response) {
return logout(request, response);
}
private String logout(final HttpServletRequest request, final HttpServletResponse response) {
Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.ifPresent(authentication -> new SecurityContextLogoutHandler().logout(request, response, authentication));
return "redirect:/login";
}
function getCookie(cookiePrefix) {
var name = cookiePrefix + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
var headers = {
'X-XSRF-TOKEN': getCookie('XSRF-TOKEN'),
'content-type': 'application/json',
};
var options = {
method: 'post',
headers: headers,
credentials: 'include',
body: { ololo: 'trololo '}
};
fetch("/user", options)
.then(data => data.json())
.then(json => render(JSON.stringify(json)));
links:
4. Spring 5 Security OAuth2 (Github / Facebook)
4.1. spring-5-security-oauth2
-
spring-framework 5
-
spring-boot 2
-
oauth2
-
github
-
facebook
-
facebook + github together
bash ./gradlew bash spring-mvc-facebook-github/build/libs/*.jar bash spring-mvc-facebook/build/libs/*.jar bash spring-mvc-github/build/libs/*.jar http :8080 http :8080/login
TODO:
-
authorization callback URL: http://localhost:8080/login/oauth2/code/github (github is registration id from applicatin.yaml)
-
okta
-
google
links:
generated by daggerok-fatjar yeoman generator
5. Others
5.1. Web MVC: testing with mock user
@Log4j2
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//@formatter:off
http
.authorizeRequests()
.antMatchers("/login", "/webjars", "/favicon.*")
.permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.failureForwardUrl("/login?failure")
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "PLAY_SESSION", "NXSESSIONID", "csrfToken", "SESSION")
.permitAll(true)
;
//@formatter:on
}
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//@formatter:off
HashMap.of("usr", "pwd")
.forEach((username, password) -> Try.run(() -> auth
.inMemoryAuthentication()
.withUser(username)
.password(passwordEncoder().encode(password))
.roles("APP", "APP_USER", "APPLICATION_USER")));
//@formatter:on
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Index Page</title>
<link rel="shortcut icon" th:href="@{/favicon.ico}" type="image/x-icon">
</head>
<body>
<h1>Hola!</h1>
</body>
</html>
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class MockMvcSecurityTests {
@Autowired
WebApplicationContext wac;
private MockMvc mvc;
@Before
public void setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(wac)
.apply(springSecurity())
.build();
}
@Test
@SneakyThrows
public void unauthorized_request_should_be_redirected_to_login_page() {
mvc.perform(get("/"))
.andExpect(status().isFound())
.andExpect(header().string("location", containsString("/login")))
;
}
@Test
@SneakyThrows
public void login_page_is_publicly_accessible() {
mvc.perform(get("/login"))
.andExpect(status().isOk())
;
}
@Test
@SneakyThrows
@WithMockUser
public void authorized_request_test() {
mvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().contentType(parseMediaType("text/html;charset=UTF-8")))
.andExpect(content().string(containsString("<title>Index Page</title>")))
;
}
5.2. Web MVC: testing with web driver
@Log4j2
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//@formatter:off
http
.authorizeRequests()
.antMatchers("/login", "/webjars", "/favicon.*")
.permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.cors()
.disable()
.csrf()
.csrfTokenRepository(new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository()))
.and()
.headers()
.frameOptions()
.sameOrigin()
.xssProtection()
.xssProtectionEnabled(true)
.and()
.and()
.formLogin()
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.failureForwardUrl("/login?failure")
.and()
.sessionManagement()
.sessionCreationPolicy(IF_REQUIRED)
.invalidSessionUrl("/login?invalidSession")
.sessionAuthenticationErrorUrl("/login?sessionAuthenticationError")
.sessionFixation()
.migrateSession()
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "PLAY_SESSION", "NXSESSIONID", "csrfToken", "SESSION")
.permitAll(true)
;
//@formatter:on
}
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//@formatter:off
HashMap.of("usr", "pwd")
.forEach((username, password) -> Try.run(() -> auth
.inMemoryAuthentication()
.withUser(username)
.password(passwordEncoder().encode(password))
.roles("APP", "APP_USER", "APPLICATION_USER")));
//@formatter:on
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
@Controller
class IndexPage {
// @GetMapping
// with get mapping here we're receiving an error like:
// POST method is not supported right after re-login
@RequestMapping({ "/", "/err", "/index" })
public String index() {
return "index";
}
@GetMapping("")
public String redirect() {
return "forward:/";
}
@RequestMapping({ "/logout", "/logout/**" })
public String logout() {
return "redirect:/login?logout";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Index Page</title>
<link rel="shortcut icon" th:href="@{/favicon.ico}" type="image/x-icon">
</head>
<body>
<h1>Hola!</h1>
</body>
</html>
5.2.1. HtmlUnit e2e testing
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class HtmlUnitWebDriverSecurityTests {
@Autowired
Environment env;
private HtmlUnitDriver driver;
@Before
public void setUp() throws Exception {
this.driver = new LocalHostWebConnectionHtmlUnitDriver(env);
}
@Test
@SneakyThrows
public void login_page_is_publicly_accessible() {
driver.get("/");
assertThat(driver.getTitle()).contains("Login Page");
}
@Test
@SneakyThrows
public void login_test() {
driver.get("/");
final WebElement form = driver.findElementByTagName("form");
form.findElement(By.cssSelector("input[name=username]"))
.sendKeys("usr");
form.findElement(By.cssSelector("input[name=password]"))
.sendKeys("pwd");
form.submit();
assertThat(driver.getTitle()).contains("Index Page");
}
}
5.2.2. E2E testing in Chrome by using WebDriver
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class ChromeWebDriverSecurityTests {
@LocalServerPort
int port;
private ChromeDriver driver;
private String baseUrl;
@Before
public void setUp() throws Exception {
final boolean headless = false;
System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
this.driver = new ChromeDriver(new ChromeOptions().setHeadless(headless));
this.baseUrl = format("http://127.0.0.1:%d", port);
}
public void open(final String uri) {
final boolean isValidUri = null != uri && uri.startsWith("/");
final String path = isValidUri ? uri : "/" + uri;
driver.get(baseUrl + path);
}
@Test
@SneakyThrows
public void login_page_is_publicly_accessible() {
open("/");
assertThat(driver.getTitle()).contains("Login Page");
}
@Test
@SneakyThrows
public void login_test() {
open("/");
assertThat(driver.getTitle()).contains("Login Page");
final WebElement form = driver.findElementByTagName("form");
form.findElement(By.cssSelector("input[name=username]"))
.sendKeys("usr");
form.findElement(By.cssSelector("input[name=password]"))
.sendKeys("pwd");
form.submit();
assertThat(driver.getTitle()).contains("Index Page");
}
}
5.2.3. E2E testing in Chrome by using Selenide
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class ChromeWebDriverSecurityTests {
@LocalServerPort
int port;
private ChromeDriver driver;
private String baseUrl;
@Before
public void setUp() throws Exception {
final boolean headless = false;
System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
this.driver = new ChromeDriver(new ChromeOptions().setHeadless(headless));
this.baseUrl = format("http://127.0.0.1:%d", port);
}
public void open(final String uri) {
final boolean isValidUri = null != uri && uri.startsWith("/");
final String path = isValidUri ? uri : "/" + uri;
driver.get(baseUrl + path);
}
@Test
@SneakyThrows
public void login_page_is_publicly_accessible() {
open("/");
assertThat(driver.getTitle()).contains("Login Page");
}
@Test
@SneakyThrows
public void login_test() {
open("/");
assertThat(driver.getTitle()).contains("Login Page");
final WebElement form = driver.findElementByTagName("form");
form.findElement(By.cssSelector("input[name=username]"))
.sendKeys("usr");
form.findElement(By.cssSelector("input[name=password]"))
.sendKeys("pwd");
form.submit();
assertThat(driver.getTitle()).contains("Index Page");
}
}
6. Basic security
Add needed dependencies fist:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Implement simple in memory security:
@Configuration
public class SecurityCfg extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(User.withUsername("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build())
;
}
}
Setup RestTemplate to use user basic authentication for remote calls:
@Configuration
public class RestTemplateCfg {
@Bean
RestTemplate restTemplate() {
return new RestTemplateBuilder().basicAuthentication("user", "password")
.build();
}
}
Fetch remote data before rendering MVC template with Thymeleaf view engine by using configured rest template for basic authentication:
@Controller
@RequiredArgsConstructor
public class IndexPage {
private final RestTemplate restTemplate;
@GetMapping({"", "/"})
String index(Model model) {
var response = restTemplate.getForEntity("http://127.0.0.1:8080/api/greeting", Map.class);
var map = response.getBody();
model.addAttribute("message", map.get("message"))
.addAttribute("ctx", SecurityContextHolder.getContext());
return "index";
}
}
<p data-th-if="message != null">message: [[ ${message} ?: 'no message' ]]</p>
<pre data-th-if="ctx != null">ctx: [[ ${ctx} ?: 'no ctx' ]]</pre>
7. links
This repository contains spring-security playgroung projects
Other spring-related repositories:
-
GitHub: daggerok/angular2-spring-boot Spring Security Angular
-
GitHub: daggerok/react-spring-data-rest Spring Security React
-
GitHub: daggerok/spring-auth-ldap-data-inmemory Spring Security LDAP
-
Spring OAuth2 (JDBC token store) authorization server + resource server + client web app
-
GitHub: daggerok/csrf-spring-webflux-mustache Webflux, Reactive security, CSRF, etc..
-
GitHub: daggerok/spring-security-testing yet another spring secururity repository
Other related repositories