/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.web.security.jaspi.modules;
import org.apache.catalina.Context;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.authenticator.SavedRequest;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.coyote.ActionCode;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.CharChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.jboss.as.web.WebLogger;
import org.jboss.web.CatalinaMessages;
import javax.security.auth.Subject;
import javax.security.auth.message.AuthException;
import javax.security.auth.message.AuthStatus;
import javax.security.auth.message.MessageInfo;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Locale;
/**
* <p>
* This class implements a JASPI {@code ServerAuthModule} that handles HTTP FORM authentication.
* </p>
*
* @author <a href="mailto:Anil.Saldhana@redhat.com">Anil Saldhana</a>
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@SuppressWarnings("unused")
public class HTTPFormServerAuthModule extends WebServerAuthModule {
protected Context context;
protected boolean cache = false;
protected String delegatingLoginContextName = null;
/**
* <p>
* Creates an instance of {@code HTTPFormServerAuthModule}.
* </p>
*/
public HTTPFormServerAuthModule() {
}
/**
* <p>
* Creates an instance of {@code HTTPFormServerAuthModule} with the specified delegating login context name.
* </p>
*
* @param delegatingLoginContextName the name of the login context configuration that contains the JAAS modules that
* are to be called by this module.
*/
public HTTPFormServerAuthModule(String delegatingLoginContextName) {
super();
this.delegatingLoginContextName = delegatingLoginContextName;
}
@Override
public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject)
throws AuthException {
// Do nothing, just return SUCCESS.
return AuthStatus.SUCCESS;
}
@Override
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject)
throws AuthException {
Request request = (Request) messageInfo.getRequestMessage();
Response response = (Response) messageInfo.getResponseMessage();
Principal principal;
context = request.getContext();
LoginConfig config = context.getLoginConfig();
// references to objects we will need later.
Session session = null;
// lets find out if the cache is enabled or not.
cache = Boolean.valueOf((String) messageInfo.getMap().get("CACHE"));
// have we authenticated this user before but have caching disabled?
if (!cache) {
session = request.getSessionInternal(true);
WebLogger.WEB_SECURITY_LOGGER.debugf("Checking for reauthenticate in session %s", session.getIdInternal());
String username = (String) session.getNote(Constants.SESS_USERNAME_NOTE);
String password = (String) session.getNote(Constants.SESS_PASSWORD_NOTE);
if ((username != null) && (password != null)) {
WebLogger.WEB_SECURITY_LOGGER.debugf("Reauthenticating username '%s'", username);
principal = context.getRealm().authenticate(username, password);
if (principal != null) {
session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal);
if (!matchRequest(request)) {
registerWithCallbackHandler(principal, username, password);
return AuthStatus.SUCCESS;
}
}
WebLogger.WEB_SECURITY_LOGGER.tracef("Reauthentication failed, proceed normally");
}
}
// is this the re-submit of the original request URI after successful authentication? If so, forward the *original* request instead.
if (matchRequest(request)) {
session = request.getSessionInternal(true);
WebLogger.WEB_SECURITY_LOGGER.tracef("Restore request from session '%s'", session.getIdInternal());
principal = (Principal) session.getNote(Constants.FORM_PRINCIPAL_NOTE);
registerWithCallbackHandler(principal,
(String) session.getNote(Constants.SESS_USERNAME_NOTE),
(String) session.getNote(Constants.SESS_PASSWORD_NOTE));
// if we're caching principals we no longer need the username and password in the session, so remove them.
if (cache) {
session.removeNote(Constants.SESS_USERNAME_NOTE);
session.removeNote(Constants.SESS_PASSWORD_NOTE);
}
if (restoreRequest(request, session)) {
WebLogger.WEB_SECURITY_LOGGER.tracef("Proceed to restored request");
return (AuthStatus.SUCCESS);
} else {
WebLogger.WEB_SECURITY_LOGGER.tracef("Restore of original request failed");
try {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
} catch (IOException e) {
// Ignore IOException here (client disconnect)
}
return AuthStatus.FAILURE;
}
}
// acquire references to objects we will need to evaluate.
MessageBytes uriMB = MessageBytes.newInstance();
CharChunk uriCC = uriMB.getCharChunk();
uriCC.setLimit(-1);
String contextPath = request.getContextPath();
String requestURI = request.getDecodedRequestURI();
// is this the action request from the login page?
boolean loginAction = requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION);
// no - save this request and redirect to the form login page.
if (!loginAction) {
session = request.getSessionInternal(true);
WebLogger.WEB_SECURITY_LOGGER.tracef("Save request in session '%s'", session.getIdInternal());
try {
saveRequest(request, session);
} catch (IOException ioe) {
WebLogger.WEB_SECURITY_LOGGER.tracef("Request body too big to save during authentication");
try {
response.sendError(HttpServletResponse.SC_FORBIDDEN, CatalinaMessages.MESSAGES.requestBodyTooLarge());
} catch (IOException e) {
// Ignore IOException here (client disconnect)
throw new AuthException(e.getLocalizedMessage());
}
return (AuthStatus.FAILURE);
}
forwardToLoginPage(request, response, config);
return (AuthStatus.SEND_CONTINUE);
}
// yes - validate the specified credentials and redirect to the error page if they are not correct
Realm realm = context.getRealm();
String characterEncoding = request.getCharacterEncoding();
if (characterEncoding != null) {
try {
request.setCharacterEncoding(characterEncoding);
} catch (UnsupportedEncodingException e) {
WebLogger.WEB_SECURITY_LOGGER.unsupportedEncoding(e.getLocalizedMessage());
}
}
String username = request.getParameter(Constants.FORM_USERNAME);
String password = request.getParameter(Constants.FORM_PASSWORD);
WebLogger.WEB_SECURITY_LOGGER.tracef("Authenticating username '%s'", username);
principal = realm.authenticate(username, password);
if (principal == null) {
forwardToErrorPage(request, response, config);
return (AuthStatus.FAILURE);
}
WebLogger.WEB_SECURITY_LOGGER.tracef("Authentication of '%s' was successful", username);
if (session == null)
session = request.getSessionInternal(false);
if (session == null) {
WebLogger.WEB_SECURITY_LOGGER.tracef("User took so long to log on the session expired");
try {
response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, CatalinaMessages.MESSAGES.sessionTimeoutDuringAuthentication());
} catch (IOException e) {
// Ignore IOException here (client disconnect)
}
return (AuthStatus.FAILURE);
}
// save the authenticated Principal in our session.
session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal);
// save the username and password as well.
session.setNote(Constants.SESS_USERNAME_NOTE, username);
session.setNote(Constants.SESS_PASSWORD_NOTE, password);
// redirect the user to the original request URI (which will cause the original request to be restored).
requestURI = savedRequestURL(session);
WebLogger.WEB_SECURITY_LOGGER.tracef("Redirecting to original '%s'", requestURI);
try {
if (requestURI == null)
response.sendError(HttpServletResponse.SC_BAD_REQUEST, CatalinaMessages.MESSAGES.invalidFormLoginDirectReference());
else
response.sendRedirect(response.encodeRedirectURL(requestURI));
} catch (IOException ioe) {
// Ignore IOException here (client disconnect)
}
return (AuthStatus.FAILURE);
}
/**
* <p>
* This method verifies if the specified {@code Request} matches a previously saved request or not.
* </p>
*
* @param request The {@code Request} object to be verified.
* @return {@code true} if the request matches the saved one; {@code false} otherwise.
*/
protected boolean matchRequest(Request request) {
// has a session been created?
Session session = request.getSessionInternal(false);
if (session == null)
return (false);
// is there a saved request?
SavedRequest sreq = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
if (sreq == null)
return (false);
// is there a saved principal?
if (session.getNote(Constants.FORM_PRINCIPAL_NOTE) == null)
return (false);
// does the request URI match?
String requestURI = request.getRequestURI();
if (requestURI == null)
return (false);
return (requestURI.equals(sreq.getRequestURI()));
}
/**
* <p>
* Restores the original request from information stored in our session. If the original request is no longer present
* (because the session timed out), return {@code false}; otherwise, return {@code true}.
* </p>
*
* @param request the {@code Request} instance that will be populated with the saved request data.
* @param session the {@code Session} containing the saved information.
* @return {@code true} if the request could be restored; {@code false} otherwise.
*/
@SuppressWarnings("unchecked")
protected boolean restoreRequest(Request request, Session session) {
// retrieve and remove the SavedRequest object from our session.
SavedRequest saved = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
session.removeNote(Constants.FORM_REQUEST_NOTE);
session.removeNote(Constants.FORM_PRINCIPAL_NOTE);
if (saved == null)
return (false);
// modify our current request to reflect the original one
request.clearCookies();
Iterator cookies = saved.getCookies();
while (cookies.hasNext()) {
request.addCookie((Cookie) cookies.next());
}
MimeHeaders rmh = request.getCoyoteRequest().getMimeHeaders();
rmh.recycle();
boolean cachable = "GET".equalsIgnoreCase(saved.getMethod()) || "HEAD".equalsIgnoreCase(saved.getMethod());
Iterator names = saved.getHeaderNames();
while (names.hasNext()) {
String name = (String) names.next();
// the browser isn't expecting this conditional response now. Assuming that it can quietly recover from an unexpected 412.
if (!("If-Modified-Since".equalsIgnoreCase(name) || (cachable && "If-None-Match".equalsIgnoreCase(name)))) {
Iterator values = saved.getHeaderValues(name);
while (values.hasNext()) {
rmh.addValue(name).setString((String) values.next());
}
}
}
request.clearLocales();
Iterator locales = saved.getLocales();
while (locales.hasNext()) {
request.addLocale((Locale) locales.next());
}
request.getCoyoteRequest().getParameters().recycle();
if ("POST".equalsIgnoreCase(saved.getMethod())) {
ByteChunk body = saved.getBody();
if (body != null) {
request.getCoyoteRequest().action(ActionCode.ACTION_REQ_SET_BODY_REPLAY, body);
// set the MIME content type.
MessageBytes contentType = MessageBytes.newInstance();
// if no content type specified, use default for POST.
String savedContentType = saved.getContentType();
if (savedContentType == null) {
savedContentType = "application/x-www-form-urlencoded";
}
contentType.setString(savedContentType);
request.getCoyoteRequest().setContentType(contentType);
}
}
request.getCoyoteRequest().method().setString(saved.getMethod());
request.getCoyoteRequest().queryString().setString(saved.getQueryString());
request.getCoyoteRequest().requestURI().setString(saved.getRequestURI());
return true;
}
/**
* <p>
* Saves the original request information into our session.
* </p>
*
* @param request the {@code Request} instance to be saved
* @param session the {@code Session} where the request is to be saved.
* @throws IOException if an error occurs while saving the request.
*/
@SuppressWarnings("unchecked")
protected void saveRequest(Request request, Session session) throws IOException {
// create and populate a SavedRequest object for this request.
SavedRequest saved = new SavedRequest();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies)
saved.addCookie(cookie);
}
Enumeration names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
Enumeration values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = (String) values.nextElement();
saved.addHeader(name, value);
}
}
Enumeration locales = request.getLocales();
while (locales.hasMoreElements()) {
Locale locale = (Locale) locales.nextElement();
saved.addLocale(locale);
}
if ("POST".equalsIgnoreCase(request.getMethod())) {
ByteChunk body = new ByteChunk();
body.setLimit(request.getConnector().getMaxSavePostSize());
byte[] buffer = new byte[4096];
int bytesRead;
InputStream is = request.getInputStream();
while ((bytesRead = is.read(buffer)) >= 0) {
body.append(buffer, 0, bytesRead);
}
saved.setBody(body);
saved.setContentType(request.getContentType());
}
saved.setMethod(request.getMethod());
saved.setQueryString(request.getQueryString());
saved.setRequestURI(request.getRequestURI());
// stash the SavedRequest in our session for later use.
session.setNote(Constants.FORM_REQUEST_NOTE, saved);
}
/**
* <p>
* Returns the request URI (with the corresponding query string, if any) from the saved request so that we can
* redirect to it.
* </p>
*
* @param session the current {@code Session}.
* @return a {@code String} containing the request URI.
*/
protected String savedRequestURL(Session session) {
SavedRequest saved = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
if (saved == null)
return (null);
StringBuilder sb = new StringBuilder(saved.getRequestURI());
if (saved.getQueryString() != null) {
sb.append('?');
sb.append(saved.getQueryString());
}
return (sb.toString());
}
/**
* <p>
* Forwards the current request to the login page.
* </p>
*
* @param request the {@code Request} instance being processed.
* @param response the {@code Response} instance being processed.
* @param config the login configuration describing how authentication should be performed
*/
protected void forwardToLoginPage(Request request, Response response, LoginConfig config) {
RequestDispatcher disp = this.context.getServletContext().getRequestDispatcher(config.getLoginPage());
try {
disp.forward(request.getRequest(), response.getResponse());
response.finishResponse();
} catch (Throwable t) {
WebLogger.WEB_SECURITY_LOGGER.errorForwardingToLoginPage(t.getLocalizedMessage());
}
}
/**
* <p>
* Forwards the current request to the error page.
* </p>
*
* @param request the {@code Request} instance being processed.
* @param response the {@code Response} instance being processed.
* @param config the login configuration describing how authentication should be performed
*/
protected void forwardToErrorPage(Request request, Response response, LoginConfig config) {
RequestDispatcher disp = this.context.getServletContext().getRequestDispatcher(config.getErrorPage());
try {
disp.forward(request.getRequest(), response.getResponse());
} catch (Throwable t) {
WebLogger.WEB_SECURITY_LOGGER.errorForwardingToErrorPage(t.getLocalizedMessage());
}
}
}