flow
flow copied to clipboard
Support authentication event notifications within the context of the associated VaadinSession
Describe your motivation
I'm trying to use Vaadin flow, Vaadin's LoginForm
, and spring-security. The login process works fine.
But I have Spring beans that want to be notified when a user logs in. Ideally, they would use the existing mechanism for doing this as documented here in the spring-security docs:
@EventListener
public void onSuccess(AuthenticationSuccessEvent success) { ... }
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent failures) { ... }
Examples of the function of such a listener:
- Update the display somehow, e.g., show "Logged in as fred" or "Not logged in".
- Update the session idle timeout to be long when there is a logged-in user, but short otherwise (e.g., when some random Internet bot has successfully loaded the login screen and created a new, useless Vaadin session).
But I can't figure out how to do this, because those spring-securuty events are delivered with no VaadinSession
context.
In other words, there's no way to recover the user's VaadinSession
from those events, so there's no way to update his UI, change his session idle timeout, etc.
This seems like a fundamental omission in the spring-security integration provided by vaadin-spring. Or maybe I'm missing something?
Describe the solution you'd like
Provide a simple way for me to listen for authentication events with the associated session.
This could be a new event listener registration, or perhaps Vaadin could make the events delivered implement some new interface HasVaadinSession
that I could cast to, or whatever.
Describe alternatives you've considered
Nothing comes to mind just yet.
Additional context
Vaadin web security is fundamentally session-based; there's no such thing as a Vaadin "login" without an associated session. So the Vaadin integration with Spring security should reflect this. The session should be closely tied to everything that happens.
FYI, here's an exception showing the gory details of why using a plain @EventListener
doesn't work.
The problem being reported is that the target bean with the @EventListener
method is a @VaadinSessionScope
bean, but it can't be found when the event is being delivered because there's no VaadinSession
associated with the current thread and therefore no vaadin-session
scope in which to look it up:
SEVERE: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
org.springframework.beans.factory.support.ScopeNotActiveException: Error creating bean with name 'mybean':
Scope 'vaadin-session' is not active for the current thread; consider defining a scoped proxy for this bean if you
intend to refer to it from a singleton
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1231)
at org.springframework.context.event.ApplicationListenerMethodAdapter.getTargetBean(ApplicationListenerMethodAdapter.java:392)
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:354)
at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:237)
at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:168)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:178)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:171)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:149)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:451)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:384)
at org.springframework.security.authentication.DefaultAuthenticationEventPublisher.publishAuthenticationSuccess(DefaultAuthenticationEventPublisher.java:99)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:226)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201)
at org.springframework.security.authentication.ObservationAuthenticationManager.lambda$authenticate$1(ObservationAuthenticationManager.java:54)
at io.micrometer.observation.Observation.observe(Observation.java:565)
at org.springframework.security.authentication.ObservationAuthenticationManager.authenticate(ObservationAuthenticationManager.java:53)
at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:85)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:231)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
...
Two immediate thoughts on this topic:
- Vaadin integration for Spring Security is request-based by default, see
AccessAnnotationChecker
codes:
return hasAccess(cls, request.getUserPrincipal(),
request::isUserInRole);
- The stack-trace above shows no calls to Vaadin. Thus, open question is how Vaadin can intercept the event listener and how properly wrap or get the session.
Updates on this...
First, when the login event is fired, the VaadinSession
doesn't exist yet, and when the logout event is fired, the VaadinSession
is already being closed, so what I thought was needed (adjusting idle time) is not really needed. Not sure if this behavior is documented or not but if not it should be.
Secondly, for the more general problem of how to track down a VaadinSession
when you're within a HTTP request that has a session associated with it, but outside of the Vaadin servlet, I've come up with this:
import com.google.common.base.Preconditions;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.VaadinSessionState;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
public final class VaadinSessionFinder {
private VaadinSessionFinder() {
}
/**
* Find the {@link VaadinSession} associated with the current HTTP request.
*
* <p>
* The session is found by directly inspecting the current HTTP session, so this will work
* even if the current thread is not executing within the Vaadin servlet.
*
* <p>
* This method relies on Spring's {@link RequestContextHolder} to locate the current HTTP request.
*
* @return the {@link VaadinSession} associated with the current HTTP request, if any
*/
public static Optional<VaadinSession> find() {
// Get the current HTTP request
final HttpServletRequest request = (HttpServletRequest)RequestContextHolder.currentRequestAttributes()
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// Find the VaadinSession in the HTTP session (this logic follows VaadinService.java)
final String servletName = request.getHttpServletMapping().getServletName();
final String attributeName = String.format("%s.%s", VaadinSession.class.getName(), servletName);
return Optional.ofNullable(request.getSession(false))
.map(session -> session.getAttribute(attributeName))
.map(VaadinSession.class::cast);
}
/**
* Invoke the given action in the context of the {@link VaadinSession} associated with the current HTTP request.
*
* @param action the action to perform
* @return true if successfully dispatched, false if {@code session} is not in state {@link VaadinSessionState#OPEN}
* @throws IllegalStateException if there is no current HTTP request or {@link VaadinSession} associated with it
* @throws IllegalArgumentException if {@code action} is null
*/
public static boolean access(Runnable action) {
Preconditions.checkArgument(action != null, "null action");
final VaadinSession session = VaadinSessionFinder.find()
.orElseThrow(() -> new IllegalStateException("no VaadinSession found"));
if (!VaadinSessionState.OPEN.equals(session.getState()))
return false;
session.access(action::run);
return true;
}
}