Log user in with remember-me functionality in Spring 3.1 - spring

I currently log users in programmatically (like when they login through Facebook or other means than using my login form) with:
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, "", authorities)
);
What I want to do instead is log the user in as if they set the remember-me option on in the login form. So I'm guessing I need to use the RememberMeAuthenticationToken instead of the UsernamePasswordAuthenticationToken? But what do I put for the key argument of the constructor?
RememberMeAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities)
UPDATE: I'm using the Persistent Token Approach described here. So there is no key like in the Simple Hash-Based Token Approach.

I assume you already have <remember-me> set in your configuration.
The way remember-me works is it sets a cookie that is recognized when the user comes back to the site after their session has expired.
You'll have to subclass the RememberMeServices (TokenBased or PersistentTokenBased) you are using and make the onLoginSuccess() public. For example:
public class MyTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
#Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
super.onLoginSuccess(request, response, successfulAuthentication);
}
}
<remember-me services-ref="rememberMeServices"/>
<bean id="rememberMeServices" class="foo.MyTokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<!-- etc -->
</bean>
Inject your RememberMeServices into the bean where you are doing the programmatic login. Then call onLoginSuccess() on it, using the UsernamePasswordAuthenticationToken that you created. That will set the cookie.
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, "", authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
getRememberMeServices().onLoginSuccess(request, response, auth);
UPDATE
#at improved upon this, with no subclassing of RememberMeServices:
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, "", authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
// This wrapper is important, it causes the RememberMeService to see
// "true" for the "_spring_security_remember_me" parameter.
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
#Override public String getParameter(String name) { return "true"; }
};
getRememberMeServices().loginSuccess(wrapper, response, auth);

This is the source for the constructor.
public RememberMeAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
if ((key == null) || ("".equals(key)) || (principal == null) || "".equals(principal)) {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
this.keyHash = key.hashCode();
this.principal = principal;
setAuthenticated(true);
}
The key is hashed and its used to determine whether the authentication used for this user in the security context is not a 'forged' one.
Have a look at the RememberMeAuthenicationProvider source.
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
"The presented RememberMeAuthenticationToken does not contain the expected key"));
}
return authentication;
}
So to answer your question, you need to pass the hash code of the key field of the Authentication representing the user.

Related

Get current logged in user from Spring when SessionCreationPolicy.STATELESS is used

I want to implement this example using Keyclock server with Spring Security 5.
I'm going to use OAuth2.0 authentication with JWT token. I'm interested how I can get the current logged in user into the Rest Endpoint?
I have configured Spring Security not to store user sessions using http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);.
One possible way is to use this code:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
But I don't know is it going to work. Can someone give some advice for that case?
SecurityContextHolder, SecurityContext and Authentication Objects
By default, the SecurityContextHolder uses a ThreadLocal to store these details, which means that the security context is always available to methods in the same thread of execution. Using a ThreadLocal in this way is quite safe if care is taken to clear the thread after the present principal’s request is processed. Of course, Spring Security takes care of this for you automatically so there is no need to worry about it.
SessionManagementConfigurer consist of isStateless() method which return true for stateless policy. Based on that http set the shared object with NullSecurityContextRepository and for request cache NullRequestCache. Hence no value will be available within HttpSessionSecurityContextRepository. So there might not be issue with invalid/wrong details for user with static method
Code:
if (stateless) {
http.setSharedObject(SecurityContextRepository.class,
new NullSecurityContextRepository());
}
if (stateless) {
http.setSharedObject(RequestCache.class, new NullRequestCache());
}
Code:
Method to get user details
public static Optional<String> getCurrentUserLogin() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return Optional.ofNullable(extractPrincipal(securityContext.getAuthentication()));
}
private static String extractPrincipal(Authentication authentication) {
if (authentication == null) {
return null;
} else if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
return springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
return (String) authentication.getPrincipal();
}
return null;
}
public static Optional<Authentication> getAuthenticatedCurrentUser() {
log.debug("Request to get authentication for current user");
SecurityContext securityContext = SecurityContextHolder.getContext();
return Optional.ofNullable(securityContext.getAuthentication());
}
sessionManagement
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
You might like to explore Methods with Spring Security to get current user details with SessionCreationPolicy.STATELESS
After the service validate the token, you can parse it, and put it into the securitycontext, it can contains various data, so you have to look after it what you need. For example, subject contains username etc...
SecurityContextHolder.getContext().setAuthentication(userAuthenticationObject);
The SecurityContextHolder's context maintain a ThreadLocal entry, so you can access it on the same thread as you write it in the question.
Note that if you use reactive (webflux) methodology, then you have to put it into the reactive context instead.

Spring REST secure DELETE only the owned (the one created by app end-user, ONLY) resource

I try to find the best solution in how safety (by the owner only) DELETE a REST resource.
GOAL:
The resource could be deleted only by the owner/creator of that resource (means the one it created that resource).
Premises:
Each time an application end-user creates a client account he receives back a JWT token.
To be able to access a REST resource the client should provide a valid JWT.
The validation of the JWT is done for each incoming calls through a customer filter:
#Component public class JwtRequestFilter extends OncePerRequestFilter{
#Autowired
private ClientAuthService clientAuthService;
#Autowired
private JwtUtil jwtUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeaderDate = request.getHeader("Date");
if (authorizationHeaderDate != null){
if (DateTimeUtil.isLaterInMinThenNow(
LocalDateTime.parse(authorizationHeaderDate,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), 2)) {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.clientAuthService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}
}
chain.doFilter(request, response);
}
}
The current implementation of the DELETE REST end-point is:
#DeleteMapping("/clients/{id}")
public ResponseEntity<Client> deleteClientById(#PathVariable(required = true) Long id){
return ResponseEntity.ok(clientService.deleteClientById(id));
}
Letting like each end-user having a valid JWT could delete another end-user client account.
For a hacker is easy to get a JWT, intuit a client ID and delete, one-by-one, all clients accounts
The question is: How can I prevent such a security issue?
You want to use Spring's expression based access control:
https://docs.spring.io/spring-security/site/docs/3.0.x/reference/el-access.html
You can annotate your REST endpoint method or service method and use EL expressions to authorize your user. Here's an example from Spring's documentation that you can adapt:
#PreAuthorize("#n == authentication.name")
Contact findContactByName(#Param("n") String name);
Now - you didn't ask, but you should consider conforming to the REST convention of using the HTTP verb that matches what your action does (i.e. use DELETE HTTP actions for requests that delete resources):
Do not a REST service that uses GET HTTP methods to delete resources - to anyone that knows anything about REST this is not going to make sense:
#GetMapping("/clients/{id}")
It should be
#DeleteMapping("/clients/{id}")

How can I validate OAuth 2.0 token user details in #PreAuthorize annotation in Spring Boot REST service

I need to make a check in #PreAuthorize annotation. Something like:
#PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
That is OK but I also need to validate some user details stored in the OAuth 2.0 token with those in the request path so I would need to do something like (oauthToken.userDetails is just an example:
#PreAuthorize("#pathProfileId.equals(oauthToken.userDetails.profileId)")
(profileId is not userId or userName, it is a user details that we add in the OAuth token when we create it)
What is the simplest way to make OAuth token properties visible in the preauthorized annotation security expression language?
You have two options:
1-
Setting UserDetailsService instance into DefaultUserAuthenticationConverter
and set converter to JwtAccessTokenConverter so when spring calls extractAuthentication method from DefaultUserAuthenticationConverter it found (userDetailsService != null) so it get the whole UserDetails object by calling implementation of loadUserByUsername when calling this line:
userDetailsService.loadUserByUsername((String) map.get(USERNAME))
implemented in next method inside spring class org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter.java but just adding it to clarify how spring get principal object from map (first getting it by username, and if userDetailsService not null so it get the whole object):
//Note: This method implemented by spring but just putting it to show where spring exctract principal object and how extracting it
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
So what you need to implement in your microservice is:
#Bean//this method just used with token store bean example: new JwtTokenStore(tokenEnhancer());
public JwtAccessTokenConverter tokenEnhancer() {
/**
* CustomTokenConverter is a class extends JwtAccessTokenConverter
* which override "enhance" to add extra information to OAuth2AccessToken after
* authenticate the user and get it by loadUserByUsername implementation
* like profileId in your case
**/
JwtAccessTokenConverter converter = new CustomTokenConverter();
DefaultAccessTokenConverter datc = new DefaultAccessTokenConverter();
datc.setUserTokenConverter(userAuthenticationConverter());
converter.setAccessTokenConverter(datc);
//Other method code implementation....
}
#Autowired
private UserDetailsService userDetailsService;
#Bean
public UserAuthenticationConverter userAuthenticationConverter() {
DefaultUserAuthenticationConverter duac = new DefaultUserAuthenticationConverter();
duac.setUserDetailsService(userDetailsService);
return duac;
}
Note: this first way will hit database in every request so it load user by username and get UserDetails object so it assign it to principal object inside authentication.
2-
If for any reason you see it's better to not hit database in each request and no problem about executing data needed like profileId from token passed in request.
Assuming you know that old authorities assigned to user when generating oauth2 token will always be in token till it goes invalid even after you change it in database for user who passes the token in request so user could call a method not allowed to him/her anymore after extracting token and it was allowed before extracting the token.
So this means if user authorities changed after generating the token, new authorities will not be checked by #PreAuthorize as it's not removed or added to token and you have to wait till old token goes invalid or expired so user forced to execute the service again to get new oauth token.
Anyway, in this second option you only need to override extractAuthentication method inside CustomTokenConverter class extends JwtAccessTokenConverter and forget about setting access token converter converter.setAccessTokenConverter from tokenEnhancer() method in first option, and here are the whole CustomTokenConverter you can use it for reading data from token and return principal object not just string username:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
public class CustomTokenConverter extends JwtAccessTokenConverter {
// This is the method you need to override to read data direct from token passed in request
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Object userIdObj = map.get(AuthenticationUtils.USER_ID);
UUID userId = userIdObj != null ? UUID.fromString(userIdObj.toString()) : null;
Object profileIdObj = map.get(AuthenticationUtils.PROFILE_ID);
UUID profileId = profileIdObj != null ? UUID.fromString(profileIdObj.toString()) : null;
Object firstNameObj = map.get(AuthenticationUtils.FIRST_NAME);
String firstName = firstNameObj != null ? String.valueOf(firstNameObj) : null;
Object lastNameObj = map.get(AuthenticationUtils.LAST_NAME);
String lastName = lastNameObj != null ? String.valueOf(lastNameObj) : null;
JwtUser principal = new JwtUser(userId, profileId, authentication.getUserAuthentication().getName(), "N/A", authentication.getUserAuthentication().getAuthorities(), firstName, lastName);
authentication = new OAuth2Authentication(authentication.getOAuth2Request(),
new UsernamePasswordAuthenticationToken(principal, "N/A", authentication.getUserAuthentication().getAuthorities()));
return authentication;
}
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
JwtUser user = (JwtUser) authentication.getPrincipal();
Map<String, Object> info = new LinkedHashMap<>(accessToken.getAdditionalInformation());
if (user.getId() != null)
info.put(AuthenticationUtils.USER_ID, user.getId());
if (user.getProfileId() != null)
info.put(AuthenticationUtils.PROFILE_ID, user.getProfileId());
if (isNotNullNotEmpty(user.getFirstName()))
info.put(AuthenticationUtils.FIRST_NAME, user.getFirstName());
if (isNotNullNotEmpty(user.getLastName()))
info.put(AuthenticationUtils.LAST_NAME, user.getLastName());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
customAccessToken.setAdditionalInformation(info);
return super.enhance(customAccessToken, authentication);
}
private boolean isNotNullNotEmpty(String str) {
return Optional.ofNullable(str).map(String::trim).map(string -> !str.isEmpty()).orElse(false);
}
}
Finally: Guess how i know you are asking about JWT used with OAuth2?
Because i am a part of your company :P and you know that :P

How to design a good JWT authentication filter

I am new to JWT. There isn't much information available in the web, since I came here as a last resort. I already developed a spring boot application using spring security using spring session. Now instead of spring session we are moving to JWT. I found few links and now I can able to authenticate a user and generate token. Now the difficult part is, I want to create a filter which will be authenticate every request to the server,
How will the filter validate the token? (Just validating the signature is enough?)
If someone else stolen the token and make rest call, how will I verify that.
How will I by-pass the login request in the filter? Since it doesn't have authorization header.
Here is a filter that can do what you need :
public class JWTFilter extends GenericFilterBean {
private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = this.resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
if (this.tokenProvider.validateToken(jwt)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
this.resetAuthenticationAfterRequest();
} catch (ExpiredJwtException eje) {
LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
LOGGER.debug("Exception " + eje.getMessage(), eje);
}
}
private void resetAuthenticationAfterRequest() {
SecurityContextHolder.getContext().setAuthentication(null);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
return null;
}
}
And the inclusion of the filter in the filter chain :
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public final static String AUTHORIZATION_HEADER = "Authorization";
#Autowired
private TokenProvider tokenProvider;
#Autowired
private AuthenticationProvider authenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.authenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTFilter customFilter = new JWTFilter(this.tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
// #formatter:off
http.authorizeRequests().antMatchers("/css/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/authenticate").permitAll()
.anyRequest().fullyAuthenticated()
.and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and().logout().permitAll();
// #formatter:on
http.csrf().disable();
}
}
The TokenProvider class :
public class TokenProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
#Value("${spring.security.authentication.jwt.validity}")
private long tokenValidityInMilliSeconds;
#Value("${spring.security.authentication.jwt.secret}")
private String secretKey;
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);
Date issueDate = Date.from(now.toInstant());
Date expirationDate = Date.from(expirationDateTime.toInstant());
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();
Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
LOGGER.info("Invalid JWT signature: " + e.getMessage());
LOGGER.debug("Exception " + e.getMessage(), e);
return false;
}
}
}
Now to answer your questions :
Done in this filter
Protect your HTTP request, use HTTPS
Just permit all on the /login URI (/authenticate in my code)
I will focus in the general tips on JWT, without regarding code implemementation (see other answers)
How will the filter validate the token? (Just validating the signature is enough?)
RFC7519 specifies how to validate a JWT (see 7.2. Validating a JWT), basically a syntactic validation and signature verification.
If JWT is being used in an authentication flow, we can look at the validation proposed by OpenID connect specification 3.1.3.4 ID Token Validation. Summarizing:
iss contains the issuer identifier (and aud contains client_id if using oauth)
current time between iat and exp
Validate the signature of the token using the secret key
sub identifies a valid user
If someone else stolen the token and make rest call, how will I verify that.
Possesion of a JWT is the proof of authentication. An attacker who stoles a token can impersonate the user. So keep tokens secure
Encrypt communication channel using TLS
Use a secure storage for your tokens. If using a web front-end consider to add extra security measures to protect localStorage/cookies against XSS or CSRF attacks
set short expiration time on authentication tokens and require credentials if token is expired
How will I by-pass the login request in the filter? Since it doesn't have authorization header.
The login form does not require a JWT token because you are going to validate the user credential. Keep the form out of the scope of the filter. Issue the JWT after successful authentication and apply the authentication filter to the rest of services
Then the filter should intercept all requests except the login form, and check:
if user authenticated? If not throw 401-Unauthorized
if user authorized to requested resource? If not throw 403-Forbidden
Access allowed. Put user data in the context of request( e.g. using a ThreadLocal)
Take a look at this project it is very good implemented and has the needed documentation.
1. It the above project this is the only thing you need to validate the token and it is enough. Where token is the value of the Bearer into the request header.
try {
final Claims claims = Jwts.parser().setSigningKey("secretkey")
.parseClaimsJws(token).getBody();
request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
2. Stealing the token is not so easy but in my experience you can protect yourself by creating a Spring session manually for every successfull log in. Also mapping the session unique ID and the Bearer value(the token) into a Map (creating a Bean for example with API scope).
#Component
public class SessionMapBean {
private Map<String, String> jwtSessionMap;
private Map<String, Boolean> sessionsForInvalidation;
public SessionMapBean() {
this.jwtSessionMap = new HashMap<String, String>();
this.sessionsForInvalidation = new HashMap<String, Boolean>();
}
public Map<String, String> getJwtSessionMap() {
return jwtSessionMap;
}
public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
this.jwtSessionMap = jwtSessionMap;
}
public Map<String, Boolean> getSessionsForInvalidation() {
return sessionsForInvalidation;
}
public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
this.sessionsForInvalidation = sessionsForInvalidation;
}
}
This SessionMapBean will be available for all sessions. Now on every request you will not only verify the token but also you will check if he mathces the session (checking the request session id does matches the one stored into the SessionMapBean). Of course session ID can be also stolen so you need to secure the communication. Most common ways of stealing the session ID is Session Sniffing (or the Men in the middle) and Cross-site script attack. I will not go in more details about them you can read how to protect yourself from that kind of attacks.
3. You can see it into the project I linked. Most simply the filter will validated all /api/* and you will login into a /user/login for example.

Spring Session Redis and Spring Security how to update user session?

I am building a spring REST web application using spring boot, spring secuirity, and spring session (redis). I am building a cloud application following the gateway pattern using spring cloud and zuul proxy. Within this pattern I am using spring session to manage the HttpSesssion in redis and using that to authorize requests on my resource servers. When an operation is executed that alters the session's authorities, I would like to update that object so that the user does not have to log out to have the updates reflected. Does anyone have a solution for this?
To update the authorities you need to modify the authentication object in two places. One in the Security Context and the other in the Request Context. Your principal object will be org.springframework.security.core.userdetails.User or extend that class (if you have overridden UserDetailsService). This works for modifying the current user.
Authentication newAuth = new UsernamePasswordAuthenticationToken({YourPrincipalObject},null,List<? extends GrantedAuthority>)
SecurityContextHolder.getContext().setAuthentication(newAuth);
RequestContextHolder.currentRequestAttributes().setAttribute("SPRING_SECURITY_CONTEXT", newAuth, RequestAttributes.SCOPE_GLOBAL_SESSION);
To update the session using spring session for any logged in user requires a custom filter. The filter stores a set of sessions that have been modified by some process. A messaging system updates that value when new sessions need to be modified. When a request has a matching session key, the filter looks up the user in the database to fetch the updates. Then it updates the "SPRING_SECURITY_CONTEXT" property on the session and updates the Authentication in the SecurityContextHolder. The user does not need to log out. When specifying the order of your filter it is important that it comes after SpringSessionRepositoryFilter. That object has an #Order of -2147483598 so I just altered my filter by one to make sure it is the next one that is executed.
The workflow looks like:
Modify User A Authority
Send Message To Filter
Add User A Session Keys to Set (In the filter)
Next time User A passed through the filter, update their session
#Component
#Order(UpdateAuthFilter.ORDER_AFTER_SPRING_SESSION)
public class UpdateAuthFilter extends OncePerRequestFilter
{
public static final int ORDER_AFTER_SPRING_SESSION = -2147483597;
private Logger log = LoggerFactory.getLogger(this.getClass());
private Set<String> permissionsToUpdate = new HashSet<>();
#Autowired
private UserJPARepository userJPARepository;
private void modifySessionSet(String sessionKey, boolean add)
{
if (add) {
permissionsToUpdate.add(sessionKey);
} else {
permissionsToUpdate.remove(sessionKey);
}
}
public void addUserSessionsToSet(UpdateUserSessionMessage updateUserSessionMessage)
{
log.info("UPDATE_USER_SESSION - {} - received", updateUserSessionMessage.getUuid().toString());
updateUserSessionMessage.getSessionKeys().forEach(sessionKey -> modifySessionSet(sessionKey, true));
//clear keys for sessions not in redis
log.info("UPDATE_USER_SESSION - {} - success", updateUserSessionMessage.getUuid().toString());
}
#Override
public void destroy()
{
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
{
HttpSession session = httpServletRequest.getSession();
if (session != null)
{
String sessionId = session.getId();
if (permissionsToUpdate.contains(sessionId))
{
try
{
SecurityContextImpl securityContextImpl = (SecurityContextImpl) session.getAttribute("SPRING_SECURITY_CONTEXT");
if (securityContextImpl != null)
{
Authentication auth = securityContextImpl.getAuthentication();
Optional<User> user = auth != null
? userJPARepository.findByUsername(auth.getName())
: Optional.empty();
if (user.isPresent())
{
user.get().getAccessControls().forEach(ac -> ac.setUsers(null));
MyCustomUser myCustomUser = new MyCustomUser (user.get().getUsername(),
user.get().getPassword(),
user.get().getAccessControls(),
user.get().getOrganization().getId());
final Authentication newAuth = new UsernamePasswordAuthenticationToken(myCustomUser ,
null,
user.get().getAccessControls());
SecurityContextHolder.getContext().setAuthentication(newAuth);
session.setAttribute("SPRING_SECURITY_CONTEXT", newAuth);
}
else
{
//invalidate the session if the user could not be found
session.invalidate();
}
}
else
{
//invalidate the session if the user could not be found
session.invalidate();
}
}
finally
{
modifySessionSet(sessionId, false);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}

Resources