2

There are several scenarios, where I want to update the user/principal data such that the changes are reflected while the user stays logged in (I do not want to force re-authentication)

From "within" the session this is not a Problem:

    @PostMapping("/updateInfo")
    fun updateMyData(
            @AuthenticationPrincipal user: AppUser,
            @Valid @RequestBody newInfo: UpdateDataRequest
    ): ResponseEntity<TestUserInfo> {
        val testInfo = TestUserInfo(user, newInfo)
        user.info = testInfo

        val updatedUser = users.save(user)
        return ResponseEntity.ok(updatedUser.info!!)
    }

When I allow the user to for example change the their own data, I can easily access and change the @AuthenticationPrincipal - in successive requests i can observe that the data is updated.

This is different when I need to change the user data from 'outside' the session.

use cases

There are 2 use cases for this:

a). an administrator changes user-data
b). the user confirms his email address

Now a). clearly happens from within another http-session where the principal is a user with some admin privileges. For b). you might ask, why this doesn't happen within a session: I want a simple one-time confirmation link, i.e. a get request. I cannot assume, that the user is logged in via a session on the device the confirmation link is opened. It wouldn't feel right to me, to do a separate preauthentication provider or something to get the user authenticated - then there will an unnecessary session opened on a browser that is never used again.

So in both cases, when I fetch the user via a JPArepository, update data, and save it back, the change is up to date in the databse - but the logged-in users don't know of that change, because their user data is stored in the http session and doesn't know that it needs to be updated.

Note that I am not using redis/spring-session anything - this is just a plain http session, so from my understanding I can not use FindByIndexNameSessionRepository.

What I have tried

  1. In spring-security issue #3849 it was suggested by rwinch to override SecurityContextRepository - however, there is no further information on how to do that exactly - I tried to understand the interface but couldn't get much further there.

  2. I tried to get through the responses tothe followinf SO post: How to reload authorities on user update with Spring Security (ignoring answers using redis.)

  • the most upvoted answer by leo doesn't help, as mentioned in the comments there
  • Aure77 suggests using SessionRegistry, which I tried to use also following bealdung - but to no avail: I cannot the right session, getallprincipals() is always empty when there is an active session for a logged in user. In case I had the right session I'm still not even sure how to move on from there, as Aure just suggests using expireNow() which forces reauthentication - I want to avoid that.
  • alexkasko suggests something similar - from his I am thinking that maybe spring boot uses a thread-local securityContextRepository by default, and thats why i have no principals. He the suggests something that i haven'T yet understood - also the answers are quite old (2012) and I don'T feel very secure about trying to understand and apply that
  • TwiN suggests using a HandlerInterceptor. Hasler Choo suggests a modified version with a hashset that seems to be more close to what i need. As described below - it has its problems though.

HandlerInterceptor based approach

This is the only solution so far that I could successfully implement - but it doesn't seem very flexible. My implementation so far will only cover user-role changes.
Configuration:

@Configuration
class WebMvcConfig : WebMvcConfigurer {
    @Autowired
    private lateinit var updateUserDataInterceptor : UpdateUserDataInterceptor

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(updateUserDataInterceptor)
    }
}

The HandlerInterceptor:

@Component
class UpdateUserDataInterceptor(
        @Autowired
        private val users: AppUserRepository
) : HandlerInterceptor {
    private val usersToUpdate = ConcurrentHashMap.newKeySet<Long>()

    fun markUpdate(user: AppUser) = usersToUpdate.add(user.id)

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        val auth = SecurityContextHolder.getContext().authentication
        (auth.principal as? AppUser)?.apply {
            synchronized(usersToUpdate) {
                if (id in usersToUpdate) {
                    role = users.findById(id).get().role
                    usersToUpdate.remove(id)
                }
            }
        }

        return true
    }
}

Instead of just updating the role, what I would rather like, is just replace the entire principle - but the principal is final in the Authentication object. So whenever a would wnat something else than the role updated, this has to specifically be mentioned here.

Remaining questions:

  • Are there other solutions than the HandlerInterceptor?
  • Is there a HandlerInterceptor based solution, that allows me to fully update the principal object
IARI
  • 1,217
  • 1
  • 18
  • 35
  • @KavithakaranKanapathippillai I added direct-links to the individual answers - all options below 2. are answers to the same SO post. – IARI Jun 24 '20 at 13:23
  • thanks for the answer - I believe the next request should be fine, i'll double check on that tomorrow. – IARI Jun 25 '20 at 16:22

1 Answers1

1

I am not considering single instance applications

1. Three factors in play

  • How quickly you want the changes reflected ( current session and current request vs current session and next request vs next session)
  • Do you have to keep the response time minimally affected by using distributed memory or cache?
  • Do you want to cut the cost (cannot use distributed memory) at the expense of response time?

Now you can you choose one option from first factor. But with second and third factors, you optimise one factor at the expensive of other one. Or you try to find a balance like your attempt to keep a list of affected users in memory and then hit the database for those affected.

( Unfortunately your optimisation to keep list of affected users in UpdateUserDataInterceptor as it is not stored in distributed memory won't work unless it is a single instance application)

2. Now based on my understanding of your question, I am making the following answers to the three factors in play.

  • current session next request
  • reduced cost (no distributed memory)
  • performance hit with database calls

( I will later update my thoughts on other possible paths and possible implementations for those paths)

3. Implementation options for the selected path - next-request-with-db-calls-and-no-distributed-memory

Any component that is part of request filter chain with the ability to call the database can achieve this by updating the SecurityContext. If you do this in the SecurityContextRepository, you are doing it at the earliest opportunity and you may even have the opportunity to restore the SecurityContext with updated principle instead of updating the already created SecurityContext. But any other filter or Interceptor can achieve this too by updating the SecurityContext.

4. Detailed look into each Implementation

  • SecurityContextRepository Option :

Looking at the HttpSessionSecurityContextRepository, it seems straight forward to extend it.

public class HttpSessionSecurityContextRepository 
            implements SecurityContextRepository {
    .....
    public SecurityContext loadContext(HttpRequestResponseHolder reqRespHolder) {
        HttpServletRequest request = reqRespHolder.getRequest();
        HttpServletResponse response = reqRespHolder.getResponse();
        HttpSession httpSession = request.getSession(false);

        SecurityContext context = readSecurityContextFromSession(httpSession);
        ........

        //retrieve the user details from db
        //and update the principal. 
        .......
        return context;
    }

}
  • SecurityContextHolderStrategy Option

Looking at the ThreadLocalSecurityContextHolderStrategy, it also looks straightforward

final class ThreadLocalSecurityContextHolderStrategy 
            implements SecurityContextHolderStrategy {
    

    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
    ....


    public void setContext(SecurityContext context) {

        // you can intercept this call here, manipulate the SecurityContext and set it 

        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    }

    .....
}
  • Another filter or HandlerInterceptor //TODO WILL UPDATE

Note:

  • You mentioned principal is final in authentication object and you want to replace it. You can achieve this by creating a mutable wrapper of UserDetails, extending your current UserDetailsService and returning that wrapper. Then you can update the principal,
YourWrapper principalWrapper =(YourWrapper) securityContext
                       .getAuthentication().getPrincipal();
principalWrapper.setPrincipal(updated);