Unable to Login in Spring Boot - 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.

Related

I'm using Cognito + Spring security. There are any way to use authorization?

I'm using spring Security and cognito for authentication and authorization. I entered some custom roles via aws IAM and I would like to know if there was a method to grant controlled access to resources. On the web I found some that set the cognito:groups as a role and used that, but they use deprecated classes and methods on it. Is there any way to do this with the latest versions?
I tried to create a class:
package com.projectname.name.Configurations;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CognitoAccessTokenConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
public CognitoAccessTokenConverter() {
}
#Override
public AbstractAuthenticationToken convert(#NonNull final Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream
.concat(defaultGrantedAuthoritiesConverter.convert(jwt).stream(), extractResourceRoles(jwt).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities);
}
private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt) {
Collection<String> userRoles = jwt.getClaimAsStringList("cognito:groups");
//System.out.println("\n!!!!!!!!" +userRoles +"!!!!!!!!!!\n"); DEBUG
if (userRoles != null)
return userRoles
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
return Collections.emptySet();
}
}
/*
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.
#Component
public class CognitoAccessTokenConverter extends OAuth2AuthorizationCodeRequestAuthenticationToken{
private static final String COGNITO_GROUPS = "cognito:groups";
private static final String SPRING_AUTHORITIES = "authorities";
private static final String COGNITO_USERNAME = "username";
private static final String SPRING_USER_NAME = "user_name";
}
#Component
public class CognitoAccessTokenConverter extends {
// Note: This the core part.
private static final String COGNITO_GROUPS = "cognito:groups";
private static final String SPRING_AUTHORITIES = "authorities";
private static final String COGNITO_USERNAME = "username";
private static final String SPRING_USER_NAME = "user_name";
#SuppressWarnings("unchecked")
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
if (claims.containsKey(COGNITO_GROUPS))
((Map<String, Object>) claims).put(SPRING_AUTHORITIES, claims.get(COGNITO_GROUPS));
if (claims.containsKey(COGNITO_USERNAME))
((Map<String, Object>) claims).put(SPRING_USER_NAME, claims.get(COGNITO_USERNAME));
return super.extractAuthentication(claims);
}
} */
how can I use this conversion in my spring security configuration?
package com.SSDProject.Booked.Configurations;
import java.io.*;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin").hasAuthority("max")
.requestMatchers("/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login();
return http.build();
}
Help me, I tried to implements it and search everywhere. Some helps? Have you an idea?
I've recently created the same PoC using SpringBoot 2.x and Java 17.
In my case I don't have any deprecation warning from my IDE, here my example:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.and()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.userDetailsService(null)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())));
return http.build();
}
private JwtAuthenticationConverter grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
String[] scopes;
if (jwt.getClaims().containsKey("cognito:groups")) {
scopes = ((JSONArray) jwt.getClaims().get("cognito:groups")).toArray(new String[0]);
} else {
scopes = ((String) jwt.getClaims().getOrDefault("scope", "")).split(" ");
}
return Arrays.stream(scopes)
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase(Locale.ROOT)))
.collect(Collectors.toSet());
}
);
return jwtAuthenticationConverter;
}
Exactly which line is deprecated in your code? And what version of resource-server are you using? For me spring-boot-starter-oauth2-resource-server is 2.7.5.
This is actually not an answer but I don't have the reputation for add comment to the question :)
Is your Spring application serving server-side rendered UI (Thymeleaf, JSF or alike) or is it a REST API (#RestController or #Controller with #ResponseBody)?
In second case, your app is a resource-server. OAuth2 login should be handled by clients, not resource-server: clients acquire access token and send it as Authorization header to resource-server.
In my answer to Use Keycloak Spring Adapter with Spring Boot 3, I explain how to configure both Spring resource-servers and clients. All you'll have to adapt for Cognito are issuer URI and the private-claims name to extract authorities from.
Configuring a resource-server with authorities mapped from cognito:groups using my starters (thin wrappers around spring-boot-starter-oauth2-resource-server) can be as simple as:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.10</version>
</dependency>
#Configuration
#EnableMethodSecurity
public class SecurityConfig {
}
com.c4-soft.springaddons.security.issuers[0].location=https://cognito-idp.Region.amazonaws.com/your user pool ID/.well-known/openid-configuration
com.c4-soft.springaddons.security.issuers[0].authorities.claims=cognito:groups
# This is probably too permissive but can be fine tuned (origins, headers and methods can be defined per path)
com.c4-soft.springaddons.security.cors[0].path=/**
If your application is only a client, my starters won't be of any help.
If your app is both a resource-server and a client (serves JSON payloads and server-side rendered UI with, for instance, Thymeleaf), then you'll have to define a second SecurityFilterChain bean. Details in the answer linked earlier.
If you don't want to use my starters, then you'll have to write quite some java conf. Details in the previously linked answer, again.

Convert JWT Authentication Principal to something more usable in spring

I have a spring boot microservice that validates a JWT (issued by a different service) for authentication. It is working nicely, and I can access the JWT details in my controller like so:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// MyController.java
#RestController
#RequestMapping("/")
public class MyController {
#GetMapping()
public String someControllerMethod(#AuthenticationPrincipal Jwt jwt) {
int userId = Integer.parseInt(jwt.getClaim("userid"));
...
}
}
That works great. I can extract what I need from the JWT and go on to talk to my database with the correct userid etc.
However I find it a bit tedious to have to use the Jwt type to get these values in each controller. Is there a way I can inject a different type as the #AuthenticationPrincipal?
E.g. my own class which has already extracted what is needed from the JWT, and exposes something like .getUserId() that returns an int?
That would also let me centralise the logic of parsing the claims or throwing exceptions if they are not as expected etc.
UPDATE
After more google spelunking, it seems I have two options
Option1: #ControllerAdvice and #ModelAttribute
As explained in this answer. I can do something like:
import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
CustomPrincipal customPrincipal;
if (auth != null && auth.getPrincipal() instanceof Jwt) {
Jwt jwt = (Jwt) auth.getPrincipal();
String sessionId = jwt.getClaimAsString("sessionid");
int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
customPrincipal = new CustomPrincipal(userId, sessionId);
} else {
// log an error and throw an exception?
}
return customPrincipal;
}
}
and then
import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;
#RestController
#ControllerAdvice
public class HelloWorldController {
#GetMapping("/controlleradvice")
public String index(#ModelAttribute CustomPrincipal cp) {
log.info(cp.getUserId());
return "whatever";
}
}
This seems pretty succinct, and neat and tidy. 1 new class with #ControllerAdvice, and bob's your uncle!
Option2: Using jwtAuthenticationConverter()
This answer shows another way to do it, using a "converter", which seems to convert the default Principal from a JWT to a custom object (that extends AbstractAuthenticationToken) that contains the JWT (.getCredentials()) as well as a custom object like CustomPrincipal (or a User class or something).
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
return http.build();
}
}
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;
public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
#Override
public MyAuthenticationToken convert(Jwt jwt) {
var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
return new MyAuthenticationToken(jwt, principal);
}
}
#RestController
public class HelloWorldController {
#GetMapping("/converter")
public String converter(#AuthenticationPrincipal MyPrincipal myPrincipal) {
log.info("/converter triggered");
log.info("" + myPrincipal.getUserId());
return "woo";
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public class MyPrincipal {
private int userId;
private String sessionId;
}
Option 1 is much simpler it seems.
But Option 2 is nice, as, I have Filter's that run to do additional validation (like validate the session id in the JWT). When that filter runs, when it calls SecurityContext.getContext().getAuthentication().getPrincipal(), it will get the MyPrincipal object, and not have to call Jwt.getClaimAsString() and cast it etc.
I guess I am asking, are there pros and cons to these two approaches I have not considered? Is one of them perhaps bastardising/abusing something in a way it is not meant to be?
Or is it much the same and I should select whichever I prefer?

Spring Boot OAuth2 with Github

I am implementing OAuth2 architecture with GitHub as authorization server to log into my dummy web application. Everything works perfectly fine. I got problem when I log in using my GitHub credentials I want the redirected page to show some message for user for example Welcome XYZ, but I could not get username who is logged in currently but a number is shown instead. I used principal object as well as Authentication object. Please would any body tell how do I achieve it?
Here is code for OAuth2 GitHub Configuration.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
#Configuration
public class WebSecurityConfigurerAdapterImp extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login((c)->
{
c.clientRegistrationRepository(this.clientRegistrationRepository());
}
);
http.authorizeRequests().anyRequest().authenticated();
}
private ClientRegistrationRepository clientRegistrationRepository()
{
ClientRegistration c=this.clientRegistration();
return new InMemoryClientRegistrationRepository(c);
}
private ClientRegistration clientRegistration()
{
return CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("72bc31d8b0304575442c").clientSecret("XYZSECRET").build();
}
}
Code for main_controller to which user will be redirected after logging in.
package com.controllers;
import java.security.Principal;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
public class Main_Controller {
#GetMapping("/")
#ResponseBody
public String HomePage(Authentication p)
{
System.out.println("Hellow "+p.getName());
return "Hello People.";
}
}
By default if you use "CommonOAuth2Provider.GITHUB" then principal name is map to "id" of your github user. So, you need to map principal name attribute to "login"

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

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

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);
}
}

Resources