This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-9.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 773bb8c313db6aeb02e8ea5f2ec96bc73f6540fc Author: Richard Zowalla <[email protected]> AuthorDate: Tue Aug 29 11:05:18 2023 +0200 TOMEE-4239 - Adds unpatched version from 10.0.27 --- .../catalina/authenticator/FormAuthenticator.java | 733 +++++++++++++++++++++ 1 file changed, 733 insertions(+) diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/authenticator/FormAuthenticator.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/authenticator/FormAuthenticator.java new file mode 100644 index 0000000000..a412db0071 --- /dev/null +++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/authenticator/FormAuthenticator.java @@ -0,0 +1,733 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.authenticator; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Principal; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Locale; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.coyote.ActionCode; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.http.MimeHeaders; + +/** + * An <b>Authenticator</b> and <b>Valve</b> implementation of FORM BASED + * Authentication, as described in the Servlet API Specification. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class FormAuthenticator + extends AuthenticatorBase { + + private final Log log = LogFactory.getLog(FormAuthenticator.class); // must not be static + + + // ----------------------------------------------------- Instance Variables + + /** + * Character encoding to use to read the username and password parameters + * from the request. If not set, the encoding of the request body will be + * used. + */ + protected String characterEncoding = null; + + /** + * Landing page to use if a user tries to access the login page directly or + * if the session times out during login. If not set, error responses will + * be sent instead. + */ + protected String landingPage = null; + + + // ------------------------------------------------------------- Properties + + /** + * Return the character encoding to use to read the user name and password. + * + * @return The name of the character encoding + */ + public String getCharacterEncoding() { + return characterEncoding; + } + + + /** + * Set the character encoding to be used to read the user name and password. + * + * @param encoding The name of the encoding to use + */ + public void setCharacterEncoding(String encoding) { + characterEncoding = encoding; + } + + + /** + * Return the landing page to use when FORM auth is mis-used. + * + * @return The path to the landing page relative to the web application root + */ + public String getLandingPage() { + return landingPage; + } + + + /** + * Set the landing page to use when the FORM auth is mis-used. + * + * @param landingPage The path to the landing page relative to the web + * application root + */ + public void setLandingPage(String landingPage) { + this.landingPage = landingPage; + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Authenticate the user making this request, based on the specified + * login configuration. Return <code>true</code> if any specified + * constraint has been satisfied, or <code>false</code> if we have + * created a response challenge already. + * + * @param request Request we are processing + * @param response Response we are creating + * + * @exception IOException if an input/output error occurs + */ + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) + throws IOException { + + // References to objects we will need later + Session session = null; + Principal principal = null; + + // Have we authenticated this user before but have caching disabled? + if (!cache) { + session = request.getSessionInternal(true); + if (log.isDebugEnabled()) { + log.debug("Checking for reauthenticate in session " + session); + } + String username = (String) session.getNote(Constants.SESS_USERNAME_NOTE); + String password = (String) session.getNote(Constants.SESS_PASSWORD_NOTE); + if (username != null && password != null) { + if (log.isDebugEnabled()) { + log.debug("Reauthenticating username '" + username + "'"); + } + principal = context.getRealm().authenticate(username, password); + if (principal != null) { + register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password); + if (!matchRequest(request)) { + return true; + } + } + if (log.isDebugEnabled()) { + log.debug("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); + if (log.isDebugEnabled()) { + log.debug("Restore request from session '" + session.getIdInternal() + "'"); + } + if (restoreRequest(request, session)) { + if (log.isDebugEnabled()) { + log.debug("Proceed to restored request"); + } + return true; + } else { + if (log.isDebugEnabled()) { + log.debug("Restore of original request failed"); + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return false; + } + } + + // This check has to be after the previous check for a matching request + // because that matching request may also include a cached Principal. + if (checkForCachedAuthentication(request, response, true)) { + return true; + } + + // Acquire references to objects we will need to evaluate + 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); + + LoginConfig config = context.getLoginConfig(); + + // No -- Save this request and redirect to the form login page + if (!loginAction) { + // If this request was to the root of the context without a trailing + // '/', need to redirect to add it else the submit of the login form + // may not go to the correct web application + if (request.getServletPath().length() == 0 && request.getPathInfo() == null) { + StringBuilder location = new StringBuilder(requestURI); + location.append('/'); + if (request.getQueryString() != null) { + location.append('?'); + location.append(request.getQueryString()); + } + response.sendRedirect(response.encodeRedirectURL(location.toString())); + return false; + } + + session = request.getSessionInternal(true); + if (log.isDebugEnabled()) { + log.debug("Save request in session '" + session.getIdInternal() + "'"); + } + try { + saveRequest(request, session); + } catch (IOException ioe) { + log.debug("Request body too big to save during authentication"); + response.sendError(HttpServletResponse.SC_FORBIDDEN, sm.getString("authenticator.requestBodyTooBig")); + return false; + } + forwardToLoginPage(request, response, config); + return false; + } + + // Yes -- Acknowledge the request, validate the specified credentials + // and redirect to the error page if they are not correct + request.getResponse().sendAcknowledgement(ContinueResponseTiming.ALWAYS); + Realm realm = context.getRealm(); + if (characterEncoding != null) { + request.setCharacterEncoding(characterEncoding); + } + String username = request.getParameter(Constants.FORM_USERNAME); + String password = request.getParameter(Constants.FORM_PASSWORD); + if (log.isDebugEnabled()) { + log.debug("Authenticating username '" + username + "'"); + } + principal = realm.authenticate(username, password); + if (principal == null) { + forwardToErrorPage(request, response, config); + return false; + } + + if (log.isDebugEnabled()) { + log.debug("Authentication of '" + username + "' was successful"); + } + + if (session == null) { + session = request.getSessionInternal(false); + } + if (session != null && getChangeSessionIdOnAuthentication()) { + // Does session id match? + String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE); + if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.sessionIdMismatch", session.getId(), expectedSessionId)); + } + session.expire(); + session = null; + } + } + if (session == null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug("User took so long to log on the session expired"); + } + if (landingPage == null) { + response.sendError( + HttpServletResponse.SC_REQUEST_TIMEOUT, sm.getString("authenticator.sessionExpired")); + } else { + // Make the authenticator think the user originally requested + // the landing page + String uri = request.getContextPath() + landingPage; + SavedRequest saved = new SavedRequest(); + saved.setMethod("GET"); + saved.setRequestURI(uri); + saved.setDecodedRequestURI(uri); + request.getSessionInternal(true).setNote(Constants.FORM_REQUEST_NOTE, saved); + response.sendRedirect(response.encodeRedirectURL(uri)); + } + return false; + } + + register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password); + + // Redirect the user to the original request URI (which will cause + // the original request to be restored) + requestURI = savedRequestURL(session); + if (log.isDebugEnabled()) { + log.debug("Redirecting to original '" + requestURI + "'"); + } + if (requestURI == null) { + if (landingPage == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, sm.getString("authenticator.formlogin")); + } else { + // Make the authenticator think the user originally requested + // the landing page + String uri = request.getContextPath() + landingPage; + SavedRequest saved = new SavedRequest(); + saved.setMethod("GET"); + saved.setRequestURI(uri); + saved.setDecodedRequestURI(uri); + session.setNote(Constants.FORM_REQUEST_NOTE, saved); + response.sendRedirect(response.encodeRedirectURL(uri)); + } + } else { + // Until the Servlet API allows specifying the type of redirect to + // use. + Response internalResponse = request.getResponse(); + String location = response.encodeRedirectURL(requestURI); + if ("HTTP/1.1".equals(request.getProtocol())) { + internalResponse.sendRedirect(location, HttpServletResponse.SC_SEE_OTHER); + } else { + internalResponse.sendRedirect(location, HttpServletResponse.SC_FOUND); + } + } + return false; + } + + + @Override + protected boolean isContinuationRequired(Request request) { + // Special handling for form-based logins to deal with the case + // where the login form (and therefore the "j_security_check" URI + // to which it submits) might be outside the secured area + String contextPath = this.context.getPath(); + String decodedRequestURI = request.getDecodedRequestURI(); + if (decodedRequestURI.startsWith(contextPath) && + decodedRequestURI.endsWith(Constants.FORM_ACTION)) { + return true; + } + + // Special handling for form-based logins to deal with the case where + // a resource is protected for some HTTP methods but not protected for + // GET which is used after authentication when redirecting to the + // protected resource. + // TODO: This is similar to the FormAuthenticator.matchRequest() logic + // Is there a way to remove the duplication? + Session session = request.getSessionInternal(false); + if (session != null) { + SavedRequest savedRequest = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); + if (savedRequest != null && + decodedRequestURI.equals(savedRequest.getDecodedRequestURI())) { + return true; + } + } + + return false; + } + + + @Override + protected String getAuthMethod() { + return HttpServletRequest.FORM_AUTH; + } + + + @Override + protected void register(Request request, HttpServletResponse response, + Principal principal, String authType, String username, + String password, boolean alwaysUseSession, boolean cache) { + + super.register(request, response, principal, authType, username, password, alwaysUseSession, cache); + + // If caching an authenticated Principal is turned off, + // store username and password as session notes to use them for re-authentication. + if (!cache) { + Session session = request.getSessionInternal(false); + if (session != null) { + if (username != null) { + session.setNote(Constants.SESS_USERNAME_NOTE, username); + } else { + session.removeNote(Constants.SESS_USERNAME_NOTE); + } + if (password != null) { + session.setNote(Constants.SESS_PASSWORD_NOTE, password); + } else { + session.removeNote(Constants.SESS_PASSWORD_NOTE); + } + } + } + } + + + /** + * Called to forward to the login page + * + * @param request Request we are processing + * @param response Response we are populating + * @param config Login configuration describing how authentication + * should be performed + * @throws IOException If the forward to the login page fails and the call + * to {@link HttpServletResponse#sendError(int, String)} + * throws an {@link IOException} + */ + protected void forwardToLoginPage(Request request, + HttpServletResponse response, LoginConfig config) + throws IOException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.forwardLogin", + request.getRequestURI(), request.getMethod(), + config.getLoginPage(), context.getName())); + } + + String loginPage = config.getLoginPage(); + if (loginPage == null || loginPage.length() == 0) { + String msg = sm.getString("formAuthenticator.noLoginPage", + context.getName()); + log.warn(msg); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + msg); + return; + } + + if (getChangeSessionIdOnAuthentication()) { + Session session = request.getSessionInternal(false); + if (session != null) { + String oldSessionId = session.getId(); + String newSessionId = changeSessionID(request, session); + session.setNote(Constants.SESSION_ID_NOTE, newSessionId); + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.changeSessionIdLogin", oldSessionId, newSessionId)); + } + } + } + + // Always use GET for the login page, regardless of the method used + String oldMethod = request.getMethod(); + request.getCoyoteRequest().method().setString("GET"); + + RequestDispatcher disp = + context.getServletContext().getRequestDispatcher(loginPage); + try { + if (context.fireRequestInitEvent(request.getRequest())) { + disp.forward(request.getRequest(), response); + context.fireRequestDestroyEvent(request.getRequest()); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + String msg = sm.getString("formAuthenticator.forwardLoginFail"); + log.warn(msg, t); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + msg); + } finally { + // Restore original method so that it is written into access log + request.getCoyoteRequest().method().setString(oldMethod); + } + } + + + /** + * Called to forward to the error page + * + * @param request Request we are processing + * @param response Response we are populating + * @param config Login configuration describing how authentication + * should be performed + * @throws IOException If the forward to the error page fails and the call + * to {@link HttpServletResponse#sendError(int, String)} + * throws an {@link IOException} + */ + protected void forwardToErrorPage(Request request, + HttpServletResponse response, LoginConfig config) + throws IOException { + + String errorPage = config.getErrorPage(); + if (errorPage == null || errorPage.length() == 0) { + String msg = sm.getString("formAuthenticator.noErrorPage", + context.getName()); + log.warn(msg); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + msg); + return; + } + + RequestDispatcher disp = + context.getServletContext().getRequestDispatcher(config.getErrorPage()); + try { + if (context.fireRequestInitEvent(request.getRequest())) { + disp.forward(request.getRequest(), response); + context.fireRequestDestroyEvent(request.getRequest()); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + String msg = sm.getString("formAuthenticator.forwardErrorFail"); + log.warn(msg, t); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + msg); + } + } + + + /** + * Does this request match the saved one (so that it must be the redirect + * we signaled after successful authentication? + * + * @param request The request to be verified + * @return <code>true</code> if the requests matched the saved one + */ + 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 (cache && session.getPrincipal() == null || !cache && request.getPrincipal() == null) { + return false; + } + + // Does session id match? + if (getChangeSessionIdOnAuthentication()) { + String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE); + if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) { + return false; + } + } + + // Does the request URI match? + String decodedRequestURI = request.getDecodedRequestURI(); + if (decodedRequestURI == null) { + return false; + } + return decodedRequestURI.equals(sreq.getDecodedRequestURI()); + } + + + /** + * Restore 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</code>; otherwise, return + * <code>true</code>. + * + * @param request The request to be restored + * @param session The session containing the saved information + * @return <code>true</code> if the request was successfully restored + * @throws IOException if an IO error occurred during the process + */ + protected boolean restoreRequest(Request request, Session session) + throws IOException { + + // 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.SESSION_ID_NOTE); + if (saved == null) { + return false; + } + + // Swallow any request body since we will be replacing it + // Need to do this before headers are restored as AJP connector uses + // content length header to determine how much data needs to be read for + // request body + byte[] buffer = new byte[4096]; + InputStream is = request.createInputStream(); + while (is.read(buffer) >= 0) { + // Ignore request body + } + + // Modify our current request to reflect the original one + request.clearCookies(); + Iterator<Cookie> cookies = saved.getCookies(); + while (cookies.hasNext()) { + request.addCookie(cookies.next()); + } + + String method = saved.getMethod(); + MimeHeaders rmh = request.getCoyoteRequest().getMimeHeaders(); + rmh.recycle(); + boolean cacheable = "GET".equalsIgnoreCase(method) || + "HEAD".equalsIgnoreCase(method); + Iterator<String> names = saved.getHeaderNames(); + while (names.hasNext()) { + String name = names.next(); + // The browser isn't expecting this conditional response now. + // Assuming that it can quietly recover from an unexpected 412. + // BZ 43687 + if(!("If-Modified-Since".equalsIgnoreCase(name) || + (cacheable && "If-None-Match".equalsIgnoreCase(name)))) { + Iterator<String> values = saved.getHeaderValues(name); + while (values.hasNext()) { + rmh.addValue(name).setString(values.next()); + } + } + } + + request.clearLocales(); + Iterator<Locale> locales = saved.getLocales(); + while (locales.hasNext()) { + request.addLocale(locales.next()); + } + + request.getCoyoteRequest().getParameters().recycle(); + + ByteChunk body = saved.getBody(); + + if (body != null) { + request.getCoyoteRequest().action + (ActionCode.REQ_SET_BODY_REPLAY, body); + + // Set content type + MessageBytes contentType = MessageBytes.newInstance(); + + // If no content type specified, use default for POST + String savedContentType = saved.getContentType(); + if (savedContentType == null && "POST".equalsIgnoreCase(method)) { + savedContentType = "application/x-www-form-urlencoded"; + } + + contentType.setString(savedContentType); + request.getCoyoteRequest().setContentType(contentType); + } + + request.getCoyoteRequest().method().setString(method); + // The method, URI, queryString and protocol are normally stored as + // bytes in the HttpInputBuffer and converted lazily to String. At this + // point, the method has already been set as String in the line above + // but the URI, queryString and protocol are still in byte form in the + // HttpInputBuffer. Processing the saved request body will overwrite + // these bytes. Configuring the HttpInputBuffer to retain these bytes as + // it would in a normal request would require some invasive API changes. + // Therefore force the conversion to String now so the correct values + // are presented if the application requests them. + request.getRequestURI(); + request.getQueryString(); + request.getProtocol(); + + return true; + } + + + /** + * Save the original request information into our session. + * + * @param request The request to be saved + * @param session The session to contain the saved information + * @throws IOException if an IO error occurred during the process + */ + 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<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + Enumeration<String> values = request.getHeaders(name); + while (values.hasMoreElements()) { + String value = values.nextElement(); + saved.addHeader(name, value); + } + } + Enumeration<Locale> locales = request.getLocales(); + while (locales.hasMoreElements()) { + Locale locale = locales.nextElement(); + saved.addLocale(locale); + } + + // May need to acknowledge a 100-continue expectation + request.getResponse().sendAcknowledgement(ContinueResponseTiming.ALWAYS); + + int maxSavePostSize = request.getConnector().getMaxSavePostSize(); + if (maxSavePostSize != 0) { + ByteChunk body = new ByteChunk(); + body.setLimit(maxSavePostSize); + + byte[] buffer = new byte[4096]; + int bytesRead; + InputStream is = request.getInputStream(); + + while ( (bytesRead = is.read(buffer) ) >= 0) { + body.append(buffer, 0, bytesRead); + } + + // Only save the request body if there is something to save + if (body.getLength() > 0) { + saved.setContentType(request.getContentType()); + saved.setBody(body); + } + } + + saved.setMethod(request.getMethod()); + saved.setQueryString(request.getQueryString()); + saved.setRequestURI(request.getRequestURI()); + saved.setDecodedRequestURI(request.getDecodedRequestURI()); + + // Stash the SavedRequest in our session for later use + session.setNote(Constants.FORM_REQUEST_NOTE, saved); + } + + + /** + * Return the request URI (with the corresponding query string, if any) + * from the saved request so that we can redirect to it. + * + * @param session Our current session + * @return the original request URL + */ + 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(); + } +} \ No newline at end of file
