How to add SAML token to CXF client request in Spring Boot - spring

We're building a CXF client in Spring Boot. The SAML token to authenticate/authorize against the SOAP server is provided to our app in custom HTTP header from an external auth proxy with every request. Hence, I need a way to add the provided token to every outgoing CXF request.
I know that I could possibly register a custom CXF out interceptor for that. However,
How would I go about registering that interceptor in Spring Boot?
If not done with an interceptor what would be the alternatives?
Currently, the Spring config looks like this:
#Configuration
public class MyConfig {
#Bean
public PartnerServicePortType partnerServicePortType() {
PartnerServicePortType partnerServicePortType = new PartnerServiceV0().getPartnerService();
(PartnerServiceV0 is generated from the service's WSDL with Maven.)
In the above config class we don't currently declare/configure a CXF bus bean.

One possible solution is this:
#Configuration
public class MyConfig {
#Bean
public PartnerServicePortType partnerServicePortType() {
PartnerServicePortType service = new PartnerServiceV0().getPartnerService();
configure(service, path, baseUrl);
return service;
}
private void configureService(BindingProvider bindingProvider, String path, String baseUrl) {
// maybe try the approach outlined at https://github
// .com/kprasad99/kp-soap-ws-client/blob/master/src/main/java/com/kp/swasthik/soap/CxfConfig.java#L24
// as an alternative
bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, baseUrl + path);
Endpoint cxfEndpoint = ClientProxy.getClient(bindingProvider).getEndpoint();
cxfEndpoint.getInInterceptors().add(cxfLoggingInInterceptor);
cxfEndpoint.getInFaultInterceptors().add(cxfLoggingInInterceptor);
cxfEndpoint.getOutInterceptors().add(addSamlAssertionInterceptor);
}
}
And the interceptor
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.binding.soap.SoapHeader;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.Phase;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.soap.wssecurity.Created;
import org.opensaml.soap.wssecurity.Expires;
import org.opensaml.soap.wssecurity.Security;
import org.opensaml.soap.wssecurity.Timestamp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/**
* Adding SOAP header with SAML assertion to request.
*/
#Slf4j
#Component
public class AddSamlAssertionInterceptor extends AbstractSoapInterceptor {
private final SamlAssertionExtractor samlAssertionExtractor;
#Autowired
public AddSamlAssertionInterceptor(SamlAssertionExtractor samlAssertionExtractor) {
super(Phase.POST_LOGICAL);
this.samlAssertionExtractor = samlAssertionExtractor;
}
#Override
public void handleMessage(SoapMessage message) throws Fault {
String decodedToken = SamlTokenHolder.getDecodedToken();
if (StringUtils.isBlank(decodedToken)) {
log.trace("Not adding SOAP header with SAML assertion because SAML token is blank.");
} else {
log.trace("Got decoded SAML token: {}", decodedToken);
log.trace("Adding SOAP header with SAML assertion to request.");
SoapHeader header = createSoapHeaderWithSamlAssertionFrom(decodedToken);
message.getHeaders().add(header);
}
}
private SoapHeader createSoapHeaderWithSamlAssertionFrom(String decodedToken) {
Assertion assertion = samlAssertionExtractor.extractAssertion(decodedToken);
Security security = createNewSecurityObject();
security.getUnknownXMLObjects().add(createTimestampElementFrom(assertion));
security.getUnknownXMLObjects().add(assertion);
log.trace("Creating new SOAP header with WS-Security element for '{}'.",
assertion.getSubject().getNameID().getValue());
SoapHeader header = new SoapHeader(security.getElementQName(), marshallToDom(security));
header.setMustUnderstand(config.isMustUnderstandHeader());
return header;
}
#SneakyThrows(MarshallingException.class)
private Element marshallToDom(Security security) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(security);
return marshaller.marshall(security);
}
/*
* SAML requirements documented at https://docs.oasis-open.org/wss/v1.1/wss-v1.1-spec-errata-os-SOAPMessageSecurity
* .htm#_Toc118717167. Both timestamps must be in UTC and formatted to comply with xsd:dateTime.
*/
private Timestamp createTimestampElementFrom(Assertion assertion) {
Timestamp timestamp = (Timestamp) createOpenSamlXmlObject(Timestamp.ELEMENT_NAME);
Created created = (Created) createOpenSamlXmlObject(Created.ELEMENT_NAME);
Expires expires = (Expires) createOpenSamlXmlObject(Expires.ELEMENT_NAME);
// alternative would be to use timestamp from assertion like so assertion.getConditions().getNotBefore()
created.setValue(ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
// security semantics should ensure that the expiry date here is the same as the expiry of the SAML assertion
expires.setValue(assertion.getConditions().getNotOnOrAfter().toString());
timestamp.setCreated(created);
timestamp.setExpires(expires);
return timestamp;
}
private Security createNewSecurityObject() {
return (Security) createOpenSamlXmlObject(Security.ELEMENT_NAME);
}
private XMLObject createOpenSamlXmlObject(QName elementName) {
XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
XMLObjectBuilder<Security> builder = (XMLObjectBuilder<Security>) builderFactory.getBuilder(elementName);
return builder.buildObject(elementName);
}
}

Related

Call Authenticator Microservice From ApiGateway ServerSecurityContextRepository

Introduction
I have a microservice responsible for authenticating each API call to my server. so when any request comes API gateway is supposed to call the authenticator for the validation with token and after this is a success the actual API call process to the intended destination.
API Gateway is a spring-cloud-starter-gateway project using spring-boot-starter-webflux. And the other microservice can be connected using FeignClient.
Problem
I am trying to call the feignclient method from the ReactiveAuthenticationManager authenticate(). and I need some kind of possibility to intercept it and validate this to either throw error or return a success mono.
But as of now the flatmap is not being called.
Code
import com.atlpay.pgapigateway.JwtAuthenticationToken
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.security.web.server.context.ServerSecurityContextRepository
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
#Component
class SecurityContextRepository : ServerSecurityContextRepository {
#Autowired
private lateinit var authenticationManager: AuthenticationManager
override fun save(swe: ServerWebExchange, sc: SecurityContext): Mono<Void> {
return Mono.empty()
}
override fun load(serverWebExchange: ServerWebExchange): Mono<SecurityContext> {
val path = serverWebExchange.request.path
//send request to authentication manager
val auth: Authentication = JwtAuthenticationToken(
"",
serverWebExchange.request.method!!.name,
path.toString(),
serverWebExchange.request.headers
)
return this.authenticationManager.authenticate(auth)
.map { authentication ->
SecurityContextImpl(authentication)
}
}
}
The above SecurityContextRepository calls the AuthenticationManager mentioned below.
import com.atlpay.pgapigateway.JwtAuthenticationToken
import com.atlpay.pgjavacontracts.feign.TestFeign
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
#Component
class AuthenticationManager : ReactiveAuthenticationManager {
#Autowired
private lateinit var testFeign: TestFeign
override fun authenticate(authentication: Authentication): Mono<Authentication> {
val authToken = (authentication as JwtAuthenticationToken).token
val toReturnUserNamePasswordAuthenticationToken = JwtAuthenticationToken(
authentication.token,
authentication.httpMethod,
authentication.path,
authentication.httpHeaders
)
return testFeign.getUsers()
.flatMap { response ->
//if error throw error
>>>>>> println("This is just a test >> ${response.status()}")
Mono.just(toReturnUserNamePasswordAuthenticationToken as Authentication)
}
.then(Mono.just(toReturnUserNamePasswordAuthenticationToken as Authentication))
}
}
From the above class I am expecting to intercept the call the validate the response code of the feignclient. at this line -
println("This is just a test >> ${response.status()}")
But this is never called!!
FeignClient is just a mock api for testing -
import feign.Response
import org.springframework.web.bind.annotation.PostMapping
import reactivefeign.spring.config.ReactiveFeignClient
import reactor.core.publisher.Mono
#ReactiveFeignClient(url = "https://630369f20de3cd918b34e39e.mockapi.io", name = "testFeign")
interface TestFeign {
#PostMapping("/users")
fun getUsers(): Mono<Response>
}
Attempts
Tried to make a call using Restclient blocking api. and got the result.
but I want it to be load balanced so changed to a LoadBalanced rest client and used the URL 'http://myMicroServiceName/validate' this gives me a block()/blockfirst()/blocklast() are blocking error.
So this is out of scope for now.
I think I am missing some basic structure in the Reactive part or its an issue with the Apigateway ReactiveAuthenticationManager.
Update
Found an solution with webfilter. will update with an answer soon.
Any suggestion is appreciated.

Unable to Login in Spring Boot

I am new to spring boot, i am trying to login in my application. I am facing some issues.
I am not able to login. It cant authenticate to login with my credential and return with message login invalid.
I want the user to be authenticate when they try to access client site (eg localhost:8080/). I also want to implement logout when user dont valid on a link file.
Here is my main application
package oidc.controller;
import eu.olympus.client.interfaces.UserClient;
import eu.olympus.model.Attribute;
import eu.olympus.model.AttributeIdentityProof;
import eu.olympus.model.Operation;
import eu.olympus.model.Policy;
import eu.olympus.model.Predicate;
import eu.olympus.model.exceptions.AuthenticationFailedException;
import eu.olympus.model.exceptions.ExistingUserException;
import eu.olympus.model.exceptions.OperationFailedException;
import eu.olympus.model.exceptions.TokenGenerationException;
import eu.olympus.model.exceptions.UserCreationFailedException;
import eu.olympus.model.server.rest.IdentityProof;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import oidc.model.AttributeContainer;
import oidc.model.ChangeAttributesRequest;
import oidc.model.ChangePasswordRequest;
import oidc.model.CreateUserRequest;
import oidc.model.DeleteAccountRequest;
import oidc.model.LoginRequest;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.RedirectView;
#Controller
public class OidcController {
private static final Logger logger = LoggerFactory.getLogger(OidcController.class);
#Autowired
UserClient userClient;
#Autowired
Policy policy;
// Login
#RequestMapping("/login")
public String login(Model model, #RequestParam String redirect_uri, #RequestParam String state, #RequestParam String nonce, HttpServletRequest request) {
request.getSession().setAttribute("redirectUrl", redirect_uri);
request.getSession().setAttribute("state", state);
request.getSession().setAttribute("nonce", nonce);
LoginRequest loginRequest = new LoginRequest();
model.addAttribute("loginRequest", loginRequest);
policy.setPolicyId(nonce);
return "/login";
}
#RequestMapping("/loginFailed")
public String login(Model model) {
LoginRequest loginRequest = new LoginRequest();
model.addAttribute("loginRequest", loginRequest);
model.addAttribute("loginError", true);
return "/login";
}
#RequestMapping("/loginPage")
public String loginPage(Model model) {
LoginRequest loginRequest = new LoginRequest();
model.addAttribute("loginRequest", loginRequest);
model.addAttribute("hasCreated", false);
return "/login";
}
#PostMapping("/authenticate")
public RedirectView authenticate(LoginRequest loginRequest, Model model, HttpServletRequest request) throws AuthenticationFailedException, TokenGenerationException {
try {
// TODO We need to get the audience somehow?
policy.getPredicates().add(new Predicate("audience", Operation.REVEAL, new Attribute("olympus-service-provider")));
String token = userClient.authenticate(loginRequest.getUsername(), loginRequest.getPassword(), policy, null, "NONE");
model.addAttribute("username", loginRequest.getUsername());
model.addAttribute("token", token);
String redirectUrl = (String) request.getSession().getAttribute("redirectUrl");
String state = (String) request.getSession().getAttribute("state");
return new RedirectView(redirectUrl + "#state=" + state + "&id_token=" + token + "&token_type=bearer");
} catch (Exception e) {
e.printStackTrace();
if (ExceptionUtils.indexOfThrowable(e, AuthenticationFailedException.class) != -1) {
return new RedirectView("/loginFailed", true);
} else {
throw e;
}
} finally {
userClient.clearSession();
}
}
here is login Request
package oidc.model;
import lombok.Getter;
import lombok.Setter;
/**
* A container for a login request
*/
#Getter
#Setter
public class LoginRequest {
private String username;
private String password;
}
I suggest you to use Spring Security. It is a dependency and you can add it via your build tool such as Maven, Gradle etc. After studying your code what I can see is that you are trying to build security mechanism from scratch.
I wouldn't advice you to do that unless you have a high motivation factor to do so. If you can use Spring Security, it is very powerful and equipped with all the features you are looking for. You can easily overcome user authentication, authorization and even it can provide a default login page.
When it comes to authentication, you can have few types of user stores such as in-memory user store, JDBC user store, LDAP user store or even your own custom user store. Apart from username and password authentication via a GUI, you are able to do sys-to-sys authentication. You can easily achieve JWT token authentication with few steps just like adding a filter and minor configuration.
It is very difficult to cover and give the whole source code as an answer here but I will provide you a sample code such that you can get a glimpse of it. Please be advice that the below mentioned code is purely for demonstration purpose and can modify it to your standards.
Spring Security Configuration Class
package com.example.sankalpaspringbootcicd.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Security configuration class.
* Created date - 2021/08/02
*/
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* Authentication
* Responsible for configuring user-store.
* Overridden from WebSecurityConfigurerAdapter level.
* #param theAuthenticationManager AuthenticationManagerBuilder
* #throws Exception - Exception
*/
#Override
public void configure(AuthenticationManagerBuilder theAuthenticationManager) throws Exception {
theAuthenticationManager.inMemoryAuthentication()
//Admin user
.withUser("admin")
.password("super")
.roles("ADMIN")
.and()
//Normal user
.withUser("user")
.password("password")
.roles("USER");
}
/**
* Authorization
* Responsible for security configuration.
* Overridden from WebSecurityConfigurerAdapter level.
* #param theHttpSecurity HttpSecurity
* #throws Exception - Exception
*/
#Override
public void configure(HttpSecurity theHttpSecurity) throws Exception {
theHttpSecurity.csrf().disable();
theHttpSecurity
.authorizeRequests()
.antMatchers("/welcome/**").access("permitAll")
.antMatchers("/user/**").hasRole("ADMIN")
.anyRequest().fullyAuthenticated() //All requests should be authenticated
.and().headers().frameOptions().sameOrigin() //To allow H2-Console
.and().httpBasic();
}
/**
* Method constructing a password encoder bean.
* Constructs 'NoOpPasswordEncoder'.
* #return PasswordEncoder
*/
#Bean
public PasswordEncoder getPasswordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
This is a very simple example and I will take you through each method. The first method is configure(AuthenticationManagerBuilder theAuthenticationManager) and what it does is basically creating you a in-memory user store.
The second method is configure(HttpSecurity theHttpSecurity) and it does allow you to do customizations to your security behaviour. It does allow some routes for everyone, restrict some routes for only with users with particular roles, allows route for H2 embedded database console, disable CSRF (Cross-Site Request Forgery) etc. This will prompt you a default login page as well. You can further add logics related to your login and logout mechanisms here as well.
The third method is PasswordEncoder getPasswordEncoder() and it does create a password encoder bean and put it in Spring Application Context to be used anytime.

Get response body from NoFallbackAvailableException in spring cloud circuit breaker resilience4j

I want to call a third party API. I use spring cloud circuit breaker resilience4j.
Here is my service class :
package ir.co.isc.resilience4jservice.service;
import ir.co.isc.resilience4jservice.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
#Service
public class EmployeeService {
#Autowired
private RestTemplate restTemplate;
#Autowired
private CircuitBreakerFactory circuitBreakerFactory;
public Employee getEmployee() {
try {
String url = "http://localhost:8090/employee";
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuit-breaker");
return circuitBreaker.run(() -> restTemplate.getForObject(url, Employee.class));
} catch (NoFallbackAvailableException e) {
//I should extract error response body and do right action then return correct answer
return null;
}
}
}
ResilienceConfig:
package ir.co.isc.resilience4jservice.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
#Configuration
public class CircuitBreakerConfiguration {
#Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10)
.minimumNumberOfCalls(10)
.failureRateThreshold(25)
.permittedNumberOfCallsInHalfOpenState(3)
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4))
.build();
return factory ->
factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(circuitBreakerConfig)
.timeLimiterConfig(timeLimiterConfig)
.build());
}
}
in some situation third party api return ResponseEntity with statusCode = 500 and
body = {"errorCode":"CCBE"}.
response is look like this :
[503] during [POST] to [http://localhost:8090/employee]:[{"errorCode":"CCBE"}]
When I call this API and get internal server error with body, my catch block catchs api response.
In catch block I need retrieve response body and do some actions according to errorCode.
But I can not do this.
How can I extract body in this situation?

How to set same-site cookie flag in Spring Boot?

Is it possible to set Same-Site Cookie flag in Spring Boot?
My problem in Chrome:
A cookie associated with a cross-site resource at http://google.com/
was set without the SameSite attribute. A future release of Chrome
will only deliver cookies with cross-site requests if they are set
with SameSite=None and Secure. You can review cookies in developer
tools under Application>Storage>Cookies and see more details at
https://www.chromestatus.com/feature/5088147346030592 and
https://www.chromestatus.com/feature/5633521622188032.
How to solve this problem?
Spring Boot 2.6.0
Spring Boot 2.6.0 now supports configuration of SameSite cookie attribute:
Configuration via properties
server.servlet.session.cookie.same-site=strict
Configuration via code
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
#Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofStrict();
}
}
Spring Boot 2.5.0 and below
Spring Boot 2.5.0-SNAPSHOT doesn't support SameSite cookie attribute and there is no setting to enable it.
The Java Servlet 4.0 specification doesn't support the SameSite cookie attribute. You can see available attributes by opening javax.servlet.http.Cookie java class.
However, there are a couple of workarounds. You can override Set-Cookie attribute manually.
The first approach (using custom Spring HttpFirewall) and wrapper around request:
You need to wrap request and adjust cookies right after session is created. You can achieve it by defining the following classes:
one bean (You can define it inside SecurityConfig if you want to hold everything in one place. I just put #Component annotation on it for brevity)
package hello.approach1;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
#Component
public class CustomHttpFirewall implements HttpFirewall {
#Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
return new RequestWrapper(request);
}
#Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new ResponseWrapper(response);
}
}
first wrapper class
package hello.approach1;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Wrapper around HttpServletRequest that overwrites Set-Cookie response header and adds SameSite=None portion.
*/
public class RequestWrapper extends FirewalledRequest {
/**
* Constructs a request object wrapping the given request.
*
* #param request The request to wrap
* #throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* Must be empty by default in Spring Boot. See FirewalledRequest.
*/
#Override
public void reset() {
}
#Override
public HttpSession getSession(boolean create) {
HttpSession session = super.getSession(create);
if (create) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
}
return session;
}
#Override
public String changeSessionId() {
String newSessionId = super.changeSessionId();
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
return newSessionId;
}
private void overwriteSetCookie(HttpServletResponse response) {
if (response != null) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // set
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // add
}
}
}
}
second wrapper class
package hello.approach1;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Dummy implementation.
* To be aligned with RequestWrapper.
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
/**
* Constructs a response adaptor wrapping the given response.
*
* #param response The response to be wrapped
* #throws IllegalArgumentException if the response is null
*/
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
}
The second approach (using Spring's AuthenticationSuccessHandler):
This approach doesn't work for basic authentication.
In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.
package hello.approach2;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
addSameSiteCookieAttribute(response); // add SameSite=strict to Set-Cookie attribute
response.sendRedirect("/hello"); // redirect to hello.html after success auth
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
}
}
}
The third approach (using javax.servlet.Filter):
This approach doesn't work for basic authentication.
In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.
package hello.approach3;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
public class SameSiteFilter implements javax.servlet.Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
addSameSiteCookieAttribute((HttpServletResponse) response); // add SameSite=strict cookie attribute
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
}
}
#Override
public void destroy() {
}
}
You can look at this demo project on the GitHub for more details on the configuration for org.springframework.security.web.authentication.AuthenticationSuccessHandler or javax.servlet.Filter.
The SecurityConfig contains all the necessary configuration.
Using addHeader is not guaranteed to work because basically the
Servlet container manages the creation of the Session and Cookie. For
example, the second and third approaches won't work in case you return JSON in
response body because application server will overwrite Set-Cookie
header during flushing of response. However, second and third approaches will
work in cases, when you redirect a user to another page after successful
authentication.
Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section (at least at the time of writing). You can look at Set-Cookie response header or use curl to see if SameSite cookie attribute was added.
This is an open issue with Spring Security (https://github.com/spring-projects/spring-security/issues/7537)
As I inspected in Spring-Boot (2.1.7.RELEASE), By Default it uses DefaultCookieSerializer which carry a property sameSite defaulting to Lax.
You can modify this upon application boot, through the following code.
Note: This is a hack until a real fix (configuration) is exposed upon next spring release.
#Component
#AllArgsConstructor
public class SameSiteInjector {
private final ApplicationContext applicationContext;
#EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
DefaultCookieSerializer cookieSerializer = applicationContext.getBean(DefaultCookieSerializer.class);
log.info("Received DefaultCookieSerializer, Overriding SameSite Strict");
cookieSerializer.setSameSite("strict");
}
}
From spring boot version 2.6.+ you may specify your samesite cookie either programatically or via configuration file.
Spring boot 2.6.0 documentation
If you would like to set samesite to lax via configuration file then:
server.servlet.session.cookie.same-site=lax
Or programatically
#Configuration
public class MySameSiteConfiguration {
#Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofLax();
}
}
Ever since the last update, chrome started showing that message to me too. Not really an answer regarding spring, but you can add the cookie flag to the header of the session. In my case, since I'm using spring security, I intend to add it when the user logs in, since I'm already manipulating the session in order to add authentication data.
For more info, check this answer to a similar topic: https://stackoverflow.com/a/43250133
To add the session header right after the user logs in, you can base your code on this topic (by creating a spring component that implements AuthenticationSuccessHandler): Spring Security. Redirect to protected page after authentication
For me none of the above worked. My problem was, that after a login, the SameSite flag created with other methods mentioned in this post was simply ignored by redirect mechanizm.
In our spring boot 2.4.4 application I managed to get it done with custom SameSiteHeaderWriter:
import org.springframework.security.web.header.HeaderWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;
/**
* This header writer just adds "SameSite=None;" to the Set-Cookie response header
*/
public class SameSiteHeaderWriter implements HeaderWriter {
private static final String SAME_SITE_NONE = "SameSite=None";
private static final String SECURE = "Secure";
#Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (response.containsHeader(SET_COOKIE)) {
var setCookie = response.getHeader(SET_COOKIE);
var toAdd = new ArrayList<String>();
toAdd.add(setCookie);
if (! setCookie.contains(SAME_SITE_NONE)) {
toAdd.add(SAME_SITE_NONE);
}
if (! setCookie.contains(SECURE)) {
toAdd.add(SECURE);
}
response.setHeader(SET_COOKIE, String.join("; ", toAdd));
}
}
}
then in my WebSecurityConfigurerAdapter#configure I just added this header writer to the list using:
if (corsEnabled) {
httpSecurity = httpSecurity
.cors()
.and()
.headers(configurer -> {
configurer.frameOptions().disable();
configurer.addHeaderWriter(new SameSiteHeaderWriter());
});
}
This feature have to be explicitly enabled in our app by user knowing the risks.
Just thought this might help someone in the future.
Starting from Spring Boot 2.6.0 this is now possible and easy:
import org.springframework.http.ResponseCookie;
ResponseCookie springCookie = ResponseCookie.from("refresh-token", "000")
.sameSite("Strict")
.build();
and return it in a ResponseEntity, could be like this :
ResponseEntity
.ok()
.header(HttpHeaders.SET_COOKIE, springCookie.toString())
.build();
If you use spring-redis-session, you can customize the Cookie (🍪) by creating a bean like the following:
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setSameSite(null);
return serializer;
}
You can look here more detail information.
Follow the documentation to solve this issue:
https://github.com/GoogleChromeLabs/samesite-examples
It has examples with different languages

Spring Cloud Contracts and Spring Security issues

I am using Spring Cloud Contracts in projects to test microservices, everything is ok. But when I added Spring Security in the producer side, the GET return 401 status code instead of 200.
#Autowired
WebApplicationContext context;
#Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
My question is:
I have to avoid Security settings in the contract tests?
If I want to consider the security configuration, how to make it work.
I successfully used a custom annotation on the base class, as documented here test-method-withsecuritycontext
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public #interface WithMockCustomUserDetails {
String username() default "email#example.com";
String role() default "DEFAULT_ROLE";
String password() default "123456";
}
and then
#WithMockCustomUserDetails
class AccountBase {
...
}
Two options AFAIK.
A) Use authorization header
request {
method 'POST'
urlPath '/check'
headers {
contentType(applicationJsonUtf8())
header(authorization(), "Bearer eyJhb.... ")
}
}
B)
Add #WithMockUser in my base test
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.WebApplicationContext;
#SpringBootTest
#WithMockUser //this will ensure a mock user will be injected to all requests
public abstract class BaseTestCloudContract {
#Autowired
private WebApplicationContext context;
#BeforeEach
public void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
}
}

Resources