we are currently migrating from legacy security subsystem to Elytron and have a Struts2 based web application deployed in JBoss EAP 7.3.6 which should support multiple "flavors" of authentication.
The standard way of logging in should be that a user manually provides credentials in a login form (j_security_check
) and clicks the corresponding button. This works well with Elytron in our setup.
The second possibility is, that the GET request to protected content of the web application can contain a custom cookie that contains a JWT token. This cookie is intercepted by a io.undertow.server.HttpHandler
which deals with the incoming request in its io.undertow.server.HttpHandler#handleRequest
method. This handler is registered by io.undertow.servlet.api.DeploymentInfo#addSecurityWrapper
with a DeploymentInfo
which is provided by an implementation of io.undertow.servlet.ServletExtension
. The ServletExtension
is registered as a service provider in META-INF/services/io.undertow.servlet.ServletExtension
.
The request handling in our implementation of io.undertow.server.HttpHandler#handleRequest
extracts the JWT token from the cookie, pre-validates it and determines the contained username. This username and the token as a password are used as inputs for a call to javax.servlet.http.HttpServletRequest#login
.
With the legacy security subsystem, the behavior of the server was, that this call to login triggered the authentication against the configured legacy security domain AND created a session in Undertow so that the HTTP 200 response for the previous GET request contained a Set-Cookie
header with a fresh JSESSIONID
cookie.
With Elytron, javax.servlet.http.HttpServletRequest#login
doesn't do anything, neither an authentication against an Elytron security domain and security realm nor the creation of a session is triggered. The browser simply shows the login form which should get skipped by the described interception process.
I debugged the implementation of javax.servlet.http.HttpServletRequest#login
that comes with JBoss. We start in io.undertow.servlet.spec.HttpServletRequestImpl#login
which calls login = sc.login(username, password)
. This SecurityContext
, when using Elytron, is org.wildfly.elytron.web.undertow.server.SecurityContextImpl
. org.wildfly.elytron.web.undertow.server.SecurityContextImpl#login
first checks if (httpAuthenticator == null)
. The httpAuthenticator
is only set in org.wildfly.elytron.web.undertow.server.SecurityContextImpl#authenticate
which gets called by a call to javax.servlet.http.HttpServletRequest#authenticate
.
This explains, why a plain call to io.undertow.servlet.spec.HttpServletRequestImpl#login
was doing nothing. I tried to call javax.servlet.http.HttpServletRequest#authenticate
first, to instantiate that httpAuthenticator
internally, and then javax.servlet.http.HttpServletRequest#login
. This at least finally triggered the authentication and authorization against the configured Elytron security domain and security realm. Authentication/authorization were successful but Undertow still didn't issue a new JSESSIONID
cookie and the browser again showed the login form instead of proceeding to the protected resources.
I'm currently out of ideas, how to proceed with this issue und how to achieve the same behavior as with the legacy security subsystem. Why does the Elytron implementation of io.undertow.security.api.SecurityContext
behave so differently compared to the one for legacy security (io.undertow.security.impl.SecurityContextImpl
)? How am I supposed to log in programatically in a FORM based web application using Elytron with javax.servlet.http.HttpServletRequest#login
and/or javax.servlet.http.HttpServletRequest#authenticate
?
The relevant JBoss configuration for all this looks like this:
Undertow:
<application-security-domains>
<application-security-domain name="my_app_security_domain" http-authentication-factory="MyHttpAuthFactory"/>
</application-security-domains>
Elytron:
<security-domains>
<security-domain name="MySecurityDomain" default-realm="MyCachingRealm" permission-mapper="default-permission-mapper">
<realm name="MyCachingRealm" role-decoder="FromRolesAttributeDecoder"/>
</security-domain>
</security-domains>
<security-realms>
<custom-realm name="MyCustomRealm" module="module name redacted" class-name="class name redacted"/>
<caching-realm name="MyCachingRealm" realm="MyCustomRealm" maximum-age="300000"/>
<identity-realm name="local" identity="$local"/>
</security-realms>
<mappers>
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
<permission-mapping>
<principal name="anonymous"/>
<permission-set name="default-permissions"/>
</permission-mapping>
<permission-mapping match-all="true">
<permission-set name="login-permission"/>
<permission-set name="default-permissions"/>
</permission-mapping>
</simple-permission-mapper>
<constant-realm-mapper name="local" realm-name="local"/>
<constant-realm-mapper name="MyRealmMapper" realm-name="MyCachingRealm"/>
<simple-role-decoder name="FromRolesAttributeDecoder" attribute="Roles"/>
</mappers>
<http>
<http-authentication-factory name="MyHttpAuthFactory" security-domain="MySecurityDomain" http-server-mechanism-factory="global">
<mechanism-configuration>
<mechanism mechanism-name="FORM" realm-mapper="MyRealmMapper">
<mechanism-realm realm-name="MyRealm"/>
</mechanism>
</mechanism-configuration>
</http-authentication-factory>
<provider-http-server-mechanism-factory name="global"/>
</http>
This was a bug in JBoss EAP which has been fixed in EAP 7.3.8 and 7.4.1. See https://issues.redhat.com/browse/JBEAP-21737 and https://issues.redhat.com/browse/JBEAP-21738 for details.