Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[jetty-users] IllegalStateException: !UserIdentity with custom login service and NullSessionCache

Hi jetty-users,

We are trying to replace an existing, working Basic authentication
scheme with the built in Form Jetty authentication coupled with
sessions to improve user experience.
I configured the JDBC session store and got things basically working,
except that sessions have really bad behavior when multiple service
instances are deployed.
I think since we have a load balancer without sticky sessions, the
DefaultSessionCache is interfering. I tried to disable it by calling
server.addBean(new NullSessionCacheFactory()) but this leads me to
users always hitting an exception:

java.lang.IllegalStateException: !UserIdentity
at org.eclipse.jetty.security.authentication.SessionAuthentication.getUserIdentity(SessionAuthentication.java:68)
at org.eclipse.jetty.security.authentication.FormAuthenticator.validateRequest(FormAuthenticator.java:331)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:537)

I'm not sure how we're triggering this !UserIdentity assertion. With
the default session cache, this exception does show up occasionally,
and I'm not sure exactly why / when. With the null cache it happens
seemingly always.

We're running embedded Jetty 9.4.43. I'm including my setup code
below, as well as the full stack trace. Thank you in advance for any
advice as to where to go next!

final var securityHandler = new ConstraintSecurityHandler();
securityHandler.setLoginService(loginService);
securityHandler.addRole("ws");

final var constraintMapping = new ConstraintMapping();
final var constraint = new Constraint(Constraint.__FORM_AUTH, "ws");
constraint.setAuthenticate(true);
constraintMapping.setConstraint(constraint);
constraintMapping.setPathSpec("/*");

securityHandler.addConstraintMapping(constraintMapping);

final var noAuth = new Constraint();
noAuth.setName(Constraint.NONE);
final String loginPath = "/login";
final String loginErrPath = loginPath + "/error";
for (final var exclude : new String[] { "/favicon.ico", "/health",
"/health/*", loginPath, loginErrPath }) {
    final var noAuthMapping = new ConstraintMapping();
    noAuthMapping.setConstraint(noAuth);
    noAuthMapping.setPathSpec(exclude);
    securityHandler.addConstraintMapping(noAuthMapping);
}
securityHandler.setHandler(/* webapp context handler */);

securityHandler.setAuthenticator(new FormAuthenticator(loginPath,
loginErrPath, false));

final var sessionHandler = new SessionHandler();
sessionHandler.setHandler(securityHandler);
final int duration = (int) Duration.ofDays(7).toSeconds();
sessionHandler.setMaxInactiveInterval(duration);
sessionHandler.new CookieConfig().setMaxAge(duration);

final var sessionSchema = new SessionTableSchema();
sessionSchema.setTableName("JettySessions_myapp");

final var dbAdapt = new DatabaseAdaptor();
dbAdapt.setDatasource(ds);

final var dataStoreFactory = new JDBCSessionDataStoreFactory();
dataStoreFactory.setGracePeriodSec((int) Duration.ofDays(1).toSeconds());
dataStoreFactory.setSessionTableSchema(sessionSchema);
dataStoreFactory.setDatabaseAdaptor(dbAdapt);
server.addBean(dataStoreFactory);

final var sessionIdMgr = new DefaultSessionIdManager(server);
sessionIdMgr.setWorkerName(kubernetesPodId);
server.setSessionIdManager(sessionIdMgr);

final var sessionCache = new NullSessionCacheFactory();
sessionCache.setSaveOnCreate(true);
sessionCache.setFlushOnResponseCommit(true);
server.addBean(sessionCache);

for (final var connector : server.getConnectors()) {
    if (connector instanceof HttpConfiguration.ConnectionFactory) {
        final var httpConf = ((HttpConfiguration.ConnectionFactory)
connector).getHttpConfiguration();
        httpConf.setSecurePort(443);
        httpConf.setSecureScheme("https");
    }
}

And this is our LoginService:

public class WSUser implements Principal {
    private final UserView userView;

    WSUser(final UserView userView) {
        this.userView = userView;
    }

    @Override
    public String getName() {
        return userView.user().name();
    }

    public UserView getUserView() {
        return userView;
    }
}

public class WsLoginService implements LoginService {
    private static final Logger LOG =
LoggerFactory.getLogger(WsLoginService.class);

    private final Cache<Entry<String, String>, UserIdentity>
loginCache = Caffeine
            .newBuilder()
            .expireAfterWrite(Duration.ofHours(1))
            .maximumSize(1000)
            .build();

    private final WSFlexLoginManager loginMgr;
    private IdentityService identityService = new DefaultIdentityService();

    @Inject
    WsLoginService(
            final WSFlexLoginManager loginMgr,
            final Provider<UserClient> user,
            final Provider<PermissionChecker> checker) {
        this.loginMgr = loginMgr;
    }

    @Override
    public String getName() {
        return REALM;
    }

    @Override
    public UserIdentity login(final String username, final Object
credentials, final ServletRequest request) {
        final var password = Objects.toString(credentials);
        return loginCache.get(
                Map.entry(username, password),
                x -> doLogin(username, password));
    }

    private UserIdentity doLogin(final String username, final String password) {
        try {
            final LoginResponse resp = loginMgr.login(username, password);
            if (!resp.mayLogin())
            {
                LOG.warn("login '{}' disallowed" username);
                return null;
            }
            final WSUser userPrincipal = new WSUser(resp.userView());
            final Subject subject = new Subject(true,
Set.of(userPrincipal), Set.of(), Set.of(resp));
            return getIdentityService().newUserIdentity(subject,
userPrincipal, new String[] { "ws" });
        } catch (final Exception e) {
            // don't show errors to unauthenticated users
            LOG.warn("login '{}' failed", username, e);
            return null;
        }
    }

    @Override
    public boolean validate(final UserIdentity identity) {
        if (! (identity.getUserPrincipal() instanceof WSUser)) {
            return false;
        }
        final Set<Object> pc = identity.getSubject().getPrivateCredentials();
        if (pc.size() != 1) {
            return false;
        }
        try {
            return loginMgr.validate((LoginResponse) pc.iterator().next());
        } catch (final Exception e) {
            LOG.warn("while trying to check session '{}'",
identity.getUserPrincipal().getName(), e);
            return false;
        }
    }

    @Override
    public IdentityService getIdentityService() {
        return identityService;
    }

    @Override
    public void setIdentityService(final IdentityService identityService) {
        this.identityService = identityService;
    }

    @Override
    public void logout(final UserIdentity identity) {
        // no-op
    }
}

And here's the full error:

HTTP ERROR 500 java.lang.IllegalStateException: !UserIdentity

URI:/
STATUS:500
MESSAGE:java.lang.IllegalStateException: !UserIdentity
SERVLET:org.eclipse.jetty.servlet.ServletHandler$Default404Servlet-5d7f1e59
CAUSED BY:java.lang.IllegalStateException: !UserIdentity

Caused by:

java.lang.IllegalStateException: !UserIdentity
at org.eclipse.jetty.security.authentication.SessionAuthentication.getUserIdentity(SessionAuthentication.java:68)
at org.eclipse.jetty.security.authentication.FormAuthenticator.validateRequest(FormAuthenticator.java:331)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:537)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:501)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1349)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
at com.paywholesail.components.server.metrics.OTInstrumentedHandler.handle(OTInstrumentedHandler.java:278)
at org.eclipse.jetty.server.handler.StatisticsHandler.handle(StatisticsHandler.java:179)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
at org.eclipse.jetty.server.Server.handle(Server.java:516)
at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:388)
at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:633)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:380)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:386)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034)
at java.base/java.lang.Thread.run(Thread.java:831)


Back to the top