Exception in service when invoked from `AuthenticationEventListener`

I have a service that works just fine when invoked from screen controllers, etc, but when invoked from an AuthenticationEventListener it is throwing a strange exception that I can’t diagnose:

09:09:37.038 INFO  c.h.cuba.core.sys.ServiceInterceptor    - Exception in LicenseInfoService.getGoodTill(): com.haulmont.cuba.security.global.NoUserSessionException: User session not found: 23dce942-d13f-11df-88cd-b3d32fd1e595
09:09:37.057 ERROR c.h.c.s.a.AuthenticationServiceBean     - Login error
com.haulmont.cuba.core.global.RemoteException: User session not found: 23dce942-d13f-11df-88cd-b3d32fd1e595
	at com.haulmont.cuba.core.sys.ServiceInterceptor.aroundInvoke(ServiceInterceptor.java:96) ~[cuba-core-7.2.7.jar:7.2.7]
	at jdk.internal.reflect.GeneratedMethodAccessor132.invoke(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) ~[spring-aop-5.2.3.RELEASE.jar:5.2.3.RELEASE]
	at com.sun.proxy.$Proxy320.getGoodTill(Unknown Source) ~[na:na]

The method in the service is simply this:

    public Date getGoodTill() {
        if (needRefresh()) {
            refreshData();
        } else {
            DebugLog.info(this.getClass(), "  Using cached data.");
        }
        return this.goodTill;
    }

refreshData() in turn simply gets the values from a config interface - this is test code, really - later it will come from an external database or perhaps a web service.

Hi,
Can you show your AuthenticationEventListener code where you call this service? Which event, authentication success or fail or “before”?

Calling a middleware service method requires the code to be authenticated, either with some user or with anonymous session (it’s used on the login screen).

Generally “User session not found” means that calling code is not authenticated properly. It can be mistake in your code, or a bug in the platform.

It is in OnBeforeLogin.

It is designed to prevent login if a client’s license count is exceeded or time has run out.

Any ideas on this one @AlexBudarov?

Hi,

Sorry, but we need a source code example, or small sample project where the problem is reproduced.

Ok - no problem -

    private boolean licenseExpired() {
        return new Date().after(licenseInfoService.getGoodTill());
    }

    private boolean wouldBeTooManyUsers() {
        return getNonSystemSessionCount() >= licenseInfoService.getMaxUsers();
    }


    @EventListener
    private void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
        if (event.getCredentials() instanceof AbstractClientCredentials) {
            DebugLog.info(this.getClass(), "onBeforeLogin (ACC)> all: {} non-system: {}", getAllSessionCount(), getNonSystemSessionCount());
            if (licenseExpired()) {
                throw new LoginException(String.format("Your license expired on %s", dateFormat.format(licenseInfoService.getGoodTill())));
            }

            if (wouldBeTooManyUsers()) {
                throw new LoginException(String.format("Your license only permits %s users (%s currently logged in)", licenseInfoService.getMaxUsers(), getNonSystemSessionCount()));
            }
        } else {
            DebugLog.info(this.getClass(), "onBeforeLogin (!ACC)> all: {} non-system: {}", getAllSessionCount(), getNonSystemSessionCount());
        }
    }

There is the code in the AuthenticationEventListener.

The code in the service (licenseInfoService) simply gets the values from a normal config interface - this is test code though; later it will get the values from a separate DB or perhaps a web service. I am just trying to get a methodology in place whereby license details can be grabbed using a service rather than a config interface, since storing license details in a client’s DB is way too easily hacked.

Or to go a step further, here’s a totally minimal example project that will show the issue. Just run the project and login as the default admin user. You will get the exception mentioned in the OP.

I need a way to call service methods from a BeforeLoginEvent handler, or the handler of another event, that will allow me to throw a LoginException to reject the login if the results of said service methods determine that I should.

servicefromaeltest.zip (80.9 KB)

Hi,
Thanks for sample project, it saves time.
I’ve created the ticket: NoUserSessionException when calling middleware service from BeforeLogin event listener · Issue #3055 · cuba-platform/cuba · GitHub

You have two workarounds for now:

  1. Preferred one.
    Create a Spring bean instead of licenseInfoService and put logic there. Call this bean from BeforeLoginEvent listener.
    Thus you will avoid ServiceInterceptor throwing the exception.

  2. Another way is to create BeforeLoginEvent listener on the web module instead of core module, with the same logic, then you’ll be able to call middleware services.

But checking for AbstractClientCredentials is too broad, it even triggers when anonymous user session is obtained for login screen. You probably need this logic at most for LoginPasswordCredentials, RememberMeCredentials and ExternalUserCredentials.

A couple questions on this approach…

  1. Is there a link as to how to create and call Spring beans? I haven’t needed to do that yet, so I haven’t read up on how it all works.

  2. Will the bean be usable from the other places one might use a service? Other parts of the app will also query this license info, including screen controllers and such.

  3. Will the bean be able to use other CUBA features like config interfaces and the like?

Is there an example of this I can refer to? I have only seen these listeners done on the core module.

Hmmm, ok - I was just following examples for AuthenticationEventListeners and they all have the pattern of testing for AbstractClientCredentials.

Nevermind most of that last message; some Googling got me what I needed and I have the Spring bean implemented and working, and it does work from both the AuthenticationEventListener as well as screen controllers. Excellent!

Some remaining questions though -

How does one decide between using a service or a Spring bean? They seem to have nearly the same capabilities? (And services are actually Spring beans themselves, are they not?) The difference and when to use one over the other isn’t very clear.

All of the examples of AuthenticationEventListeners out there show checking for AbstractClientCredentials - whilst in your last message, you indicated it might be better to check for 3 more specific types. Is that a new recommendation, or are there times when the AbstractClientCredentials is the correct choice?

Creating a Spring bean is mentioned in user guides:
https://doc.cuba-platform.com/manual-7.2/managed_beans_creation.html
https://doc.cuba-platform.com/studio/#middleware_beans

Diffference between service and bean is the following:

Bean can be created in global, web or core module.
Global bean is instantiated twice in web and core Spring contexts, and it’s available for using separately in all modules.

Bean created in web module can be used only “locally” in web module. The same for core-module bean.

Middleware services are different. They are always implemented in core module, but can be used in all modules (core, web or even called via REST API).

Take a look to documentation and user guides:
https://doc.cuba-platform.com/manual-7.2/services.html
https://www.cuba-platform.com/guides/create-business-logic-in-cuba#middleware_services

It’s just my personal observation. If you check for AbstractClientCredentials - then automatic login of anonymous user (which is system action and is essential for login screen functioning) is also counted in your logic as regular user session login.