https://bz.apache.org/bugzilla/show_bug.cgi?id=63336
Bug ID: 63336
Summary: Currently there is no way to know in form error page
that the user was not authenticated because it was
locked out
Product: Tomcat 8
Version: 8.5.x-trunk
Hardware: PC
Status: NEW
Severity: normal
Priority: P2
Component: Catalina
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ----
If a user is locked out from LockOutRealm or if there are some specific
exceptions in backend user realms like user is locked in the backend, user is
required to change it's password first etc (see JAAS exception like
AccountExpiredException, AccountLockedException, AccountNotFoundException or
CredentialExpiredException - in case the password is valid but because it was
requested that the user should change it's password because forgot password has
been requested)
So we need some way to inform the user of the web app that the account has been
locked up in the login error page instead of just saying the username/password
is invalid as it is confusing and users are going to request forgot password
flow which will change their password and they are going to still not be able
to login if LockOutRealm has triggered lockout for 5 min.
What I'm suggesting is to provide custom configurable HttpServletRequest
attribute for example "login.error.message" of type String that describes why
the user was not able to login along with the exception itself so that we could
pass additional information into the exception itself in an attribute
"login.error.exception" (again configurable request attribute name in
server.xml as it is not standard - please do not use standard servlet error
message and error attributes as some frameworks will clear those attributes and
the login error page will not be able to get the correct message/exception)
Because LockOutRealm do not have the HttpServletRequest passed into the user
realms we need to have a Valve that will put the HttpServletRequest/Response
into thread local variable so that user realms/JAAS modules could obtain the
HttpServletRequest and inject the user attribute to be used by the login error
page
Note that currently basic authenticator will report 401 error but it will not
put into the body the reason why it was rejected so it could be a good thing to
refactor that as well and if request have the attribute to pull the value and
when sending 401 Http error from basic authentication to also put the error
message in the response body.
Here is an example that I'm using for LockOutRealm in order to report to the
user that the account is locked up and not that the username/password is
incorrect and having the user wonder what's wrong:
import java.security.Principal;
import java.security.cert.X509Certificate;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.PolicyContextException;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.realm.LockOutRealm;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSName;
public class ExtendedLockOutRealm extends LockOutRealm {
public static final String REQUEST_ATTRIBUTE_LOGIN_ERROR_MESSAGE =
"login.error.message";
public static final String REQUEST_ATTRIBUTE_LOCKOUT_MESSAGE =
ExtendedLockOutRealm.class.getName() + ".REQUEST_ATTRIBUTE_LOCKOUT_MESSAGE";
public static final String REQUEST_ATTRIBUTE_LOCKOUT_USERNAME =
ExtendedLockOutRealm.class.getName() + ".REQUEST_ATTRIBUTE_LOCKOUT_USERNAME";
public static final String REQUEST_ATTRIBUTE_LOCKOUT_TIME =
ExtendedLockOutRealm.class.getName() + ".REQUEST_ATTRIBUTE_LOCKOUT_TIME";
private static final Log log = LogFactory.getLog(LockOutRealm.class);
@Override
public Principal authenticate(String username, String clientDigest,
String nonce, String nc, String cnonce, String qop,
String realmName, String md5a2) {
if (isLocked(username)) {
processLockOutUser(username, null);
return null;
}
Principal principal = super.authenticate(username, clientDigest, nonce,
nc, cnonce, qop, realmName, md5a2);
processLockOutUser(username, principal);
return principal;
}
@Override
public Principal authenticate(String username, String credentials) {
if (isLocked(username)) {
processLockOutUser(username, null);
return null;
}
Principal principal = super.authenticate(username, credentials);
processLockOutUser(username, principal);
return principal;
}
@Override
public Principal authenticate(X509Certificate[] certs) {
String username = null;
if (certs != null && certs.length >0) {
username = certs[0].getSubjectDN().getName();
}
if (isLocked(username)) {
processLockOutUser(username, null);
return null;
}
Principal principal = super.authenticate(certs);
processLockOutUser(username, principal);
return principal;
}
@Override
public Principal authenticate(GSSContext gssContext, boolean storeCreds) {
String username = null;
if (gssContext.isEstablished()) {
GSSName name = null;
try {
name = gssContext.getSrcName();
} catch (GSSException e) {
log.warn(sm.getString("realmBase.gssNameFail"), e);
return null;
}
username = name.toString();
if (isLocked(username)) {
processLockOutUser(username, null);
return null;
}
}
Principal principal = super.authenticate(gssContext, storeCreds);
processLockOutUser(username, principal);
return principal;
}
private void processLockOutUser(String username, Principal principal) {
if (principal == null && username != null && isLocked(username)) {
log.warn(sm.getString("lockOutRealm.authLockedUser", username));
if
(PolicyContext.getHandlerKeys().contains("javax.servlet.http.HttpServletRequest"))
{
try {
Object object =
PolicyContext.getContext("javax.servlet.http.HttpServletRequest");
if (object instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest =
(HttpServletRequest) object;
int lockoutMin = lockOutTime / 60;
String message = "Account has been locked for " +
(lockoutMin > 0 ? (lockoutMin + (lockoutMin > 1 ? " minutes" : " minute")) :
(lockOutTime + (lockOutTime > 1 ? " seconds" : " second")));
httpServletRequest.setAttribute(REQUEST_ATTRIBUTE_LOGIN_ERROR_MESSAGE,
message);
httpServletRequest.setAttribute(REQUEST_ATTRIBUTE_LOCKOUT_MESSAGE, message);
httpServletRequest.setAttribute(REQUEST_ATTRIBUTE_LOCKOUT_USERNAME, username);
httpServletRequest.setAttribute(REQUEST_ATTRIBUTE_LOCKOUT_TIME, lockOutTime);
}
} catch (PolicyContextException ignored) {
}
}
}
}
}
the override authenticate methods are because I want if the user is locked out
to return immediately and not invoke the inner user realms
basically adding the check:
if (isLocked(username)) {
}
before the actual super.authenticate method
I'm using javax.security.jacc.PolicyContext in order to store http
request/response so that security classes be able to obtain them
Here is the valve to register the request and reponse with PolicyContext:
import java.io.IOException;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.PolicyContextException;
import javax.servlet.ServletException;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
public class JACCPolicyContextValve extends ValveBase {
private static final String ALREADY_FILTERED_ATTRIBUTE =
JACCPolicyContextValve.class.getName() + ".FILTERED";
private boolean debug = false;
public void setDebug(final Boolean debug) {
if (debug != null) {
this.debug = debug;
}
}
@Override
protected void initInternal() throws LifecycleException {
if (debug) {
System.out.println("[JACCPolicyContextValve] initInternal
invoked.");
}
super.initInternal();
if
(!PolicyContext.getHandlerKeys().contains(HttpServletRequestPolicyContextHandler.KEY))
{
try {
PolicyContext.registerHandler(HttpServletRequestPolicyContextHandler.KEY, new
HttpServletRequestPolicyContextHandler(), false);
if (debug) {
System.out.println("[JACCPolicyContextValve] Register " +
HttpServletRequestPolicyContextHandler.class.getName() + " handler for key " +
HttpServletRequestPolicyContextHandler.KEY);
}
} catch (PolicyContextException ex) {
if (debug) {
ex.printStackTrace();
}
}
} else {
if (debug) {
System.out.println("[JACCPolicyContextValve] PolicyContext
already have " + HttpServletRequestPolicyContextHandler.KEY + " handler key.");
}
}
if
(!PolicyContext.getHandlerKeys().contains(HttpServletResponsePolicyContextHandler.KEY))
{
try {
PolicyContext.registerHandler(HttpServletResponsePolicyContextHandler.KEY, new
HttpServletResponsePolicyContextHandler(), false);
if (debug) {
System.out.println("[JACCPolicyContextValve] Register " +
HttpServletResponsePolicyContextHandler.class.getName() + " handler for key " +
HttpServletResponsePolicyContextHandler.KEY);
}
} catch (PolicyContextException ex) {
if (debug) {
ex.printStackTrace();
}
}
} else {
if (debug) {
System.out.println("[JACCPolicyContextValve] PolicyContext
already have " + HttpServletResponsePolicyContextHandler.KEY + " handler
key.");
}
}
}
@Override
public void invoke(Request request, Response response) throws IOException,
ServletException {
if (request.getAttribute(ALREADY_FILTERED_ATTRIBUTE) == null) {
request.setAttribute(ALREADY_FILTERED_ATTRIBUTE, Boolean.TRUE);
try {
if (debug) {
System.out.println("[JACCPolicyContextValve] Set request of
class: " + request.getClass().getName());
}
SecurityContextAssociationHandler.activeRequest.set(request);
if (debug) {
System.out.println("[JACCPolicyContextValve] Set response
of class: " + response.getClass().getName());
}
SecurityContextAssociationHandler.activeResponse.set(response);
getNext().invoke(request, response);
} finally {
if (debug) {
System.out.println("[JACCPolicyContextValve] Unset
request.");
}
SecurityContextAssociationHandler.activeRequest.set(null);
if (debug) {
System.out.println("[JACCPolicyContextValve] Unset
response.");
}
SecurityContextAssociationHandler.activeResponse.set(null);
}
} else {
getNext().invoke(request, response);
}
}
}
Now having request obtained from thread local will allow JAAS modules to put
information why it did not login so that even when the JAASRealm catches the
exceptions thrown from JAAS modules and just return null for the Principal to
still know why it failed.
Having catalina's standard way to obtain the request in security related
structure that do not have http request passed as parameter will allow security
providers to put security related information that is needed to inform the user
as to why he/she can't login
This issue is related to
https://bz.apache.org/bugzilla/show_bug.cgi?id=63334
--
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]