Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .mvn/wrapper/maven-wrapper.jar
Binary file not shown.
4 changes: 3 additions & 1 deletion .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
36 changes: 19 additions & 17 deletions click/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -79,36 +79,38 @@ WARNING: It's not a great idea to return a whole `OAuth2User` in an endpoint sin
There's one final change you'll need to make.

This app will now work fine and authenticate as before, but it's still going to redirect before showing the page.
To make the link visible, we also need to switch off the security on the home page by extending `WebSecurityConfigurerAdapter`:
To make the link visible, we also need to switch off the security on the home page by registering a SecurityFilterChain bean:

.SocialApplication
[source,java]
----
@SpringBootApplication
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
public class SocialApplication {

// ...

@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(a -> a
.antMatchers("/", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.oauth2Login();
// @formatter:on
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/index.html", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.defaultSuccessUrl("/", true) // Always redirect to home after login
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
);
// @formatter:on
return http.build();
}

}
----

Spring Boot attaches special meaning to a `WebSecurityConfigurerAdapter` on the class annotated with `@SpringBootApplication`:
It uses it to configure the security filter chain that carries the OAuth 2.0 authentication processor.

The above configuration indicates a whitelist of permitted endpoints, with every other endpoint requiring authentication.
Expand Down
4 changes: 2 additions & 2 deletions click/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<version>3.5.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<java.version>17</java.version>
</properties>

<dependencies>
Expand Down
20 changes: 12 additions & 8 deletions click/src/main/java/com/example/SocialApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,41 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
public class SocialApplication {

@GetMapping("/user")
public Map<String, Object> user(@AuthenticationPrincipal OAuth2User principal) {
return Collections.singletonMap("name", principal.getAttribute("name"));
}

@Override
protected void configure(HttpSecurity http) throws Exception {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(a -> a
.antMatchers("/", "/error", "/webjars/**").permitAll()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/index.html", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.defaultSuccessUrl("/", true) // Always redirect to home after login
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.oauth2Login();
);
// @formatter:on
return http.build();
}

public static void main(String[] args) {
Expand Down
5 changes: 4 additions & 1 deletion custom-error/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ To achieve this, you can configure an `AuthenticationFailureHandler`, like so:

[source,java]
----
protected void configure(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/");

// @formatter:off
http
// ... existing configuration
.oauth2Login(o -> o
.defaultSuccessUrl("/", true) // Always redirect to home after login
.failureHandler((request, response, exception) -> {
request.getSession().setAttribute("error.message", exception.getMessage());
handler.onAuthenticationFailure(request, response, exception);
Expand Down
4 changes: 2 additions & 2 deletions custom-error/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<version>3.5.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<java.version>17</java.version>
</properties>

<dependencies>
Expand Down
74 changes: 61 additions & 13 deletions custom-error/src/main/java/com/example/SocialApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@
*/
package com.example;

import java.util.function.Supplier;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
Expand All @@ -37,19 +36,28 @@
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.function.client.WebClient;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;

@SpringBootApplication
@Controller
public class SocialApplication extends WebSecurityConfigurerAdapter {
public class SocialApplication {

@Bean
public WebClient rest(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) {
Expand Down Expand Up @@ -100,36 +108,76 @@ public String error(HttpServletRequest request) {
return message;
}

@Override
protected void configure(HttpSecurity http) throws Exception {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/");

// @formatter:off
http.antMatcher("/**")
.authorizeRequests(a -> a
.antMatchers("/", "/error", "/webjars/**").permitAll()
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/index.html", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.csrf(c -> c
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
)
.logout(l -> l
.logoutSuccessUrl("/").permitAll()
)
.oauth2Login(o -> o
.oauth2Login(oauth -> oauth
.defaultSuccessUrl("/", true) // Always redirect to home after login
.failureHandler((request, response, exception) -> {
request.getSession().setAttribute("error.message", exception.getMessage());
handler.onAuthenticationFailure(request, response, exception);
})
);
);
// @formatter:on
return http.build();
}

public static void main(String[] args) {
SpringApplication.run(SocialApplication.class, args);
}

}

final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
}

@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}

51 changes: 43 additions & 8 deletions logout/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ Now we can switch over to the server side to implement that endpoint.
== Adding a Logout Endpoint

Spring Security has built in support for a `/logout` endpoint which will do the right thing for us (clear the session and invalidate the cookie).
To configure the endpoint we simply extend the existing `configure()` method in our `WebSecurityConfigurerAdapter`:
To configure the endpoint we simply extend the existing `securityFilterChain()` method in our `SocialApplication`:

.SocialApplication.java
[source,java]
----
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
// ... existing code here
.logout(l -> l
Expand All @@ -66,24 +66,59 @@ For instance, in Angular, the front end would like the server to send it a cooki
We can implement the same behaviour with our simple jQuery client, and then the server-side changes will work with other front end implementations with no or very few changes.
To teach Spring Security about this we need to add a filter that creates the cookie.

In the `WebSecurityConfigurerAdapter` we do the following:
In the `SecurityFilterChain` we do the following:

.SocialApplication.java
[source,java]
----
@Override
protected void configure(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
// ... existing code here
.csrf(c -> c
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
)
// ... existing code here
// @formatter:on
}
----

final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
}

@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
----
Refer to https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa[spring security documentation for logout] for Single Page Application (SPA).
== Adding the CSRF Token in the Client

Since we are not using a higher level framework in this sample, you'll need to explicitly add the CSRF token, which you just made available as a cookie from the backend.
Expand Down
4 changes: 2 additions & 2 deletions logout/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<version>3.5.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<java.version>17</java.version>
</properties>

<dependencies>
Expand Down
Loading