This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 4d7d000d16 Refresh constraints after FORM auth if method changes
4d7d000d16 is described below

commit 4d7d000d164c526f75df1da43b09cd3583a4ab85
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Jul 30 14:41:30 2026 +0100

    Refresh constraints after FORM auth if method changes
---
 .../catalina/authenticator/AuthenticatorBase.java  | 149 ++++++++--
 .../catalina/authenticator/FormAuthenticator.java  |  60 ++--
 .../authenticator/TestFormAuthenticatorD.java      | 315 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   9 +
 4 files changed, 474 insertions(+), 59 deletions(-)

diff --git a/java/org/apache/catalina/authenticator/AuthenticatorBase.java 
b/java/org/apache/catalina/authenticator/AuthenticatorBase.java
index f42febc29a..c095bc4068 100644
--- a/java/org/apache/catalina/authenticator/AuthenticatorBase.java
+++ b/java/org/apache/catalina/authenticator/AuthenticatorBase.java
@@ -536,34 +536,14 @@ public abstract class AuthenticatorBase extends ValveBase 
implements Authenticat
             return;
         }
 
-        // Make sure that constrained resources are not cached by web proxies
-        // or browsers as caching can provide a security hole
-        if (constraints != null && disableProxyCaching && 
!Method.POST.equals(request.getMethod())) {
-            if (securePagesWithPragma) {
-                // Note: These can cause problems with downloading files with 
IE
-                response.setHeader("Pragma", "No-cache");
-                response.setHeader("Cache-Control", "no-cache");
-                response.setHeader("Expires", DATE_ONE);
-            } else {
-                response.setHeader("Cache-Control", "private");
-            }
-        }
+        /*
+         * Make sure that constrained resources are not cached by web proxies 
or browsers as caching can provide a
+         * security hole.
+         */
+        disableCaching(constraints, request, response);
 
-        if (constraints != null) {
-            // Enforce any user data constraint for this security constraint
-            if (log.isTraceEnabled()) {
-                log.trace("Calling hasUserDataPermission()");
-            }
-            if (!realm.hasUserDataPermission(request, response, constraints)) {
-                if (log.isDebugEnabled()) {
-                    
log.debug(sm.getString("authenticator.userDataPermissionFail"));
-                }
-                /*
-                 * ASSERT: Authenticator already set the appropriate HTTP 
status code, so we do not have to do anything
-                 * special
-                 */
-                return;
-            }
+        if (!checkUserDataConstraints(realm, constraints, request, response)) {
+            return;
         }
 
         // Since authenticate modifies the response on failure,
@@ -608,15 +588,29 @@ public abstract class AuthenticatorBase extends ValveBase 
implements Authenticat
                 log.trace("Calling authenticate()");
             }
 
-            if (jaspicProvider != null) {
+            boolean authenticated;
+            if (jaspicProvider == null) {
+                AuthenticationResult authenticationResult = 
doAuthenticateExtended(request, response);
+                if (authenticationResult == 
AuthenticationResult.PASSED_CONSTRAINTS_NEED_REFRESH) {
+                    // Recalculate constraints since the request has changed
+                    constraints = realm.findSecurityConstraints(request, 
this.context);
+                    // Re-check if caching needs to be disabled since the 
request (and maybe constraints) have changed
+                    disableCaching(constraints, request, response);
+                    // Re-check user data constraints as constraints may have 
changed
+                    if (!checkUserDataConstraints(realm, constraints, request, 
response)) {
+                        return;
+                    }
+                }
+                authenticated = authenticationResult.getAuthenticated();
+            } else {
                 jaspicState = getJaspicState(jaspicProvider, request, 
response, hasAuthConstraint);
                 if (jaspicState == null) {
                     return;
                 }
+                authenticated = authenticateJaspic(request, response, 
jaspicState, false);
             }
 
-            if (jaspicProvider == null && !doAuthenticate(request, response) ||
-                    jaspicProvider != null && !authenticateJaspic(request, 
response, jaspicState, false)) {
+            if (!authenticated) {
                 if (log.isDebugEnabled()) {
                     
log.debug(sm.getString("authenticator.authenticationFail"));
                 }
@@ -658,6 +652,43 @@ public abstract class AuthenticatorBase extends ValveBase 
implements Authenticat
     }
 
 
+    private void disableCaching(SecurityConstraint[] constraints, Request 
request, Response response) {
+        // Only disable caching where necessary
+        if (constraints != null && disableProxyCaching && 
!Method.POST.equals(request.getMethod())) {
+            if (securePagesWithPragma) {
+                // Note: These can cause problems with downloading files with 
IE
+                response.setHeader("Pragma", "No-cache");
+                response.setHeader("Cache-Control", "no-cache");
+                response.setHeader("Expires", DATE_ONE);
+            } else {
+                response.setHeader("Cache-Control", "private");
+            }
+        }
+    }
+
+
+    private boolean checkUserDataConstraints(Realm realm, SecurityConstraint[] 
constraints, Request request,
+            Response response) throws IOException {
+        if (constraints != null) {
+            // Enforce any user data constraint for this security constraint
+            if (log.isTraceEnabled()) {
+                log.trace("Calling hasUserDataPermission()");
+            }
+            if (!realm.hasUserDataPermission(request, response, constraints)) {
+                if (log.isDebugEnabled()) {
+                    
log.debug(sm.getString("authenticator.userDataPermissionFail"));
+                }
+                /*
+                 * ASSERT: Authenticator already set the appropriate HTTP 
status code, so we do not have to do anything
+                 * special
+                 */
+                return false;
+            }
+        }
+        return true;
+    }
+
+
     /**
      * Determines whether a CORS preflight request should bypass 
authentication.
      *
@@ -723,7 +754,8 @@ public abstract class AuthenticatorBase extends ValveBase 
implements Authenticat
         AuthConfigProvider jaspicProvider = getJaspicProvider();
 
         if (jaspicProvider == null) {
-            return doAuthenticate(request, httpResponse);
+            // Just authenticating so no requirement to refresh constraints
+            return doAuthenticateExtended(request, 
httpResponse).getAuthenticated();
         } else {
             Response response = request.getResponse();
             JaspicState jaspicState = getJaspicState(jaspicProvider, request, 
response, true);
@@ -827,6 +859,28 @@ public abstract class AuthenticatorBase extends ValveBase 
implements Authenticat
     protected abstract boolean doAuthenticate(Request request, 
HttpServletResponse response) throws IOException;
 
 
+    /**
+     * Extended mechanism for sub-class authentication that adds the option to 
trigger a refresh of the security
+     * constraints as required with FORM authentication if the method changes. 
Most sub-classes will just implement
+     * {@link #doAuthenticate(Request, HttpServletResponse)}.
+     *
+     * @param request  The request that triggered the authentication
+     * @param response The response associated with the request
+     *
+     * @return the result of the authentication
+     *
+     * @throws IOException If an I/O problem occurred during the 
authentication process
+     */
+    protected AuthenticationResult doAuthenticateExtended(Request request, 
HttpServletResponse response)
+            throws IOException {
+        if (doAuthenticate(request, response)) {
+            return AuthenticationResult.PASSED;
+        } else {
+            return AuthenticationResult.FAILED;
+        }
+    }
+
+
     /**
      * Does this authenticator require that {@link #authenticate(Request, 
HttpServletResponse)} is called to continue an
      * authentication process that started in a previous request?
@@ -1456,4 +1510,37 @@ public abstract class AuthenticatorBase extends 
ValveBase implements Authenticat
          */
         FULL
     }
+
+
+    /**
+     * Used to pass authentication results that are more complex than a simple 
pass/fail.
+     */
+    protected enum AuthenticationResult {
+
+        /**
+         * The authentication failed.
+         */
+        FAILED(false),
+
+        /**
+         * The authentication was successful but before proceeding the 
constraints need to be refreshed because one or
+         * more relevant properties of the request (method, URI) have changed.
+         */
+        PASSED_CONSTRAINTS_NEED_REFRESH(true),
+
+        /**
+         * The authentication was successful.
+         */
+        PASSED(true);
+
+        private final boolean authenticated;
+
+        AuthenticationResult(boolean authenticated) {
+            this.authenticated = authenticated;
+        }
+
+        public boolean getAuthenticated() {
+            return authenticated;
+        }
+    }
 }
diff --git a/java/org/apache/catalina/authenticator/FormAuthenticator.java 
b/java/org/apache/catalina/authenticator/FormAuthenticator.java
index c5be82a058..eca8ce4296 100644
--- a/java/org/apache/catalina/authenticator/FormAuthenticator.java
+++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java
@@ -148,18 +148,10 @@ public class FormAuthenticator extends AuthenticatorBase {
 
     // ------------------------------------------------------ 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 {
+    protected AuthenticationResult doAuthenticateExtended(Request request, 
HttpServletResponse response)
+            throws IOException {
 
         // References to objects we will need later
         Session session = null;
@@ -181,7 +173,7 @@ public class FormAuthenticator extends AuthenticatorBase {
                 if (principal != null) {
                     register(request, response, principal, 
HttpServletRequest.FORM_AUTH, username, password);
                     if (!matchRequest(request)) {
-                        return true;
+                        return AuthenticationResult.PASSED;
                     }
                 }
                 if (log.isDebugEnabled()) {
@@ -197,24 +189,24 @@ public class FormAuthenticator extends AuthenticatorBase {
             if (log.isTraceEnabled()) {
                 log.trace("Restore request from session '" + 
session.getIdInternal() + "'");
             }
-            if (restoreRequest(request, session)) {
+            AuthenticationResult result = restoreRequest(request, session);
+            if (result.getAuthenticated()) {
                 if (log.isTraceEnabled()) {
                     log.trace("Proceed to restored request");
                 }
-                return true;
             } else {
                 if (log.isDebugEnabled()) {
                     log.debug(sm.getString("formAuthenticator.restoreFailed"));
                 }
                 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-                return false;
             }
+            return result;
         }
 
         // 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;
+            return AuthenticationResult.PASSED;
         }
 
         // Acquire references to objects we will need to evaluate
@@ -239,7 +231,7 @@ public class FormAuthenticator extends AuthenticatorBase {
                     location.append(request.getQueryString());
                 }
                 
response.sendRedirect(response.encodeRedirectURL(location.toString()));
-                return false;
+                return AuthenticationResult.FAILED;
             }
 
             session = request.getSessionInternal(true);
@@ -251,10 +243,10 @@ public class FormAuthenticator extends AuthenticatorBase {
             } catch (IOException ioe) {
                 log.debug(sm.getString("authenticator.requestBodyTooBig"), 
ioe);
                 response.sendError(HttpServletResponse.SC_FORBIDDEN, 
sm.getString("authenticator.requestBodyTooBig"));
-                return false;
+                return AuthenticationResult.FAILED;
             }
             forwardToLoginPage(request, response, config);
-            return false;
+            return AuthenticationResult.FAILED;
         }
 
         // Yes -- Acknowledge the request, validate the specified credentials
@@ -272,7 +264,7 @@ public class FormAuthenticator extends AuthenticatorBase {
         principal = realm.authenticate(username, password);
         if (principal == null) {
             forwardToErrorPage(request, response, config);
-            return false;
+            return AuthenticationResult.FAILED;
         }
 
         if (log.isTraceEnabled()) {
@@ -311,7 +303,7 @@ public class FormAuthenticator extends AuthenticatorBase {
                 
request.getSessionInternal(true).setNote(Constants.FORM_REQUEST_NOTE, saved);
                 response.sendRedirect(response.encodeRedirectURL(uri));
             }
-            return false;
+            return AuthenticationResult.FAILED;
         }
 
         register(request, response, principal, HttpServletRequest.FORM_AUTH, 
username, password);
@@ -344,7 +336,14 @@ public class FormAuthenticator extends AuthenticatorBase {
                 response.sendRedirect(location, HttpServletResponse.SC_FOUND);
             }
         }
-        return false;
+        return AuthenticationResult.FAILED;
+    }
+
+
+    @Override
+    protected boolean doAuthenticate(Request request, HttpServletResponse 
response) throws IOException {
+        // This method should never be called
+        throw new UnsupportedOperationException();
     }
 
 
@@ -514,7 +513,7 @@ public class FormAuthenticator extends AuthenticatorBase {
      *
      * @param request The request to be verified
      * @param strict  <code>true</code> to check for a valid Principal and 
valid Session ID, <code>false</code> to only
-     * check for a valid saved request and matching URI
+     *                    check for a valid saved request and matching URI
      *
      * @return <code>true</code> if the requests matched the saved one
      */
@@ -557,23 +556,23 @@ public class FormAuthenticator extends AuthenticatorBase {
 
     /**
      * 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>.
+     * (because the session timed out), it will be treated as a failure to 
restore the request.
      *
      * @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
+     * @return the status of the FORM authentication process based on whether 
the original request could be restored
      *
      * @throws IOException if an IO error occurred during the process
      */
-    protected boolean restoreRequest(Request request, Session session) throws 
IOException {
+    protected AuthenticationResult 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;
+            return AuthenticationResult.FAILED;
         }
 
         // Swallow any request body since we will be replacing it
@@ -638,6 +637,7 @@ public class FormAuthenticator extends AuthenticatorBase {
             request.getCoyoteRequest().setContentType(contentType);
         }
 
+        boolean methodChanged = 
!request.getCoyoteRequest().getMethod().equals(method);
         request.getCoyoteRequest().setMethod(method);
         // The method, URI, queryString and protocol are normally stored as
         // bytes in the HttpInputBuffer and converted lazily to String. At this
@@ -656,7 +656,11 @@ public class FormAuthenticator extends AuthenticatorBase {
             
session.setMaxInactiveInterval(saved.getOriginalMaxInactiveIntervalOptional().intValue());
         }
 
-        return true;
+        if (methodChanged) {
+            return AuthenticationResult.PASSED_CONSTRAINTS_NEED_REFRESH;
+        } else {
+            return AuthenticationResult.PASSED;
+        }
     }
 
 
diff --git a/test/org/apache/catalina/authenticator/TestFormAuthenticatorD.java 
b/test/org/apache/catalina/authenticator/TestFormAuthenticatorD.java
new file mode 100644
index 0000000000..cdc9d6977c
--- /dev/null
+++ b/test/org/apache/catalina/authenticator/TestFormAuthenticatorD.java
@@ -0,0 +1,315 @@
+/*
+ *  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.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.startup.SimpleHttpClient;
+import org.apache.catalina.startup.TesterMapRealm;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.descriptor.web.LoginConfig;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
+import org.apache.tomcat.util.http.Method;
+
+public class TestFormAuthenticatorD extends TomcatBaseTest {
+
+    private static final String CRLF = SimpleHttpClient.CRLF;
+
+    private static final String STANDARD_USER = "userA";
+    private static final String ADMIN_USER = "userB";
+
+
+    @Test
+    public void testWithStandardUser() throws Exception {
+        SimpleHttpClient client = setupTest();
+
+        // Standard user requesting GET /standard
+        // Initial request - receive login form
+        requestStandard(client, null, true);
+        // Submit login form - receive redirect
+        submitFormAuth(client, STANDARD_USER);
+        // Request original page
+        requestStandard(client, STANDARD_USER, false);
+
+        // As authenticated standard user, request GET /admin
+        requestAdmin(client, STANDARD_USER, Method.GET, Method.GET, false);
+
+        // As authenticated standard user, request POST /admin
+        requestAdmin(client, STANDARD_USER, Method.POST, Method.POST, false);
+
+        // Clear authentication
+        client.setSessionId(null);
+
+        // As unauthenticated user, request GET /admin
+        requestAdmin(client, null, Method.GET, Method.GET, false);
+
+        // As unauthenticated user, request POST /admin
+        requestAdmin(client, null, Method.POST, Method.POST, true);
+        // Submit login form - receive redirect
+        submitFormAuth(client, STANDARD_USER);
+        // As standard user, request POST /admin
+        requestAdmin(client, STANDARD_USER, Method.GET, Method.POST, false);
+    }
+
+
+    @Test
+    public void testWithAdminUser() throws Exception {
+        SimpleHttpClient client = setupTest();
+
+        // Standard user requesting GET /standard
+        // Initial request - receive login form
+        requestStandard(client, null, true);
+        // Submit login form - receive redirect
+        submitFormAuth(client, ADMIN_USER);
+        // Request original page
+        requestStandard(client, ADMIN_USER, false);
+
+        // As authenticated standard user, request GET /admin
+        requestAdmin(client, ADMIN_USER, Method.GET, Method.GET, false);
+
+        // As authenticated standard user, request POST /admin
+        requestAdmin(client, ADMIN_USER, Method.POST, Method.POST, false);
+
+        // Clear authentication
+        client.setSessionId(null);
+
+        // As unauthenticated user, request GET /admin
+        requestAdmin(client, null, Method.GET, Method.GET, false);
+
+        // As unauthenticated user, request POST /admin
+        requestAdmin(client, null, Method.POST, Method.POST, true);
+        // Submit login form - receive redirect
+        submitFormAuth(client, ADMIN_USER);
+        // As standard user, request POST /admin
+        requestAdmin(client, ADMIN_USER, Method.GET, Method.POST, false);
+    }
+
+
+    private SimpleHttpClient setupTest() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        // Add Servlets
+        Tomcat.addServlet(ctx, "login", new LoginServlet());
+        Tomcat.addServlet(ctx, "error", new ErrorServlet());
+        Tomcat.addServlet(ctx, "target", new TargetServlet());
+        // Map Servlets (target gets mapped twice)
+        ctx.addServletMapping("/login", "login");
+        ctx.addServletMapping("/error", "error");
+        ctx.addServletMapping("/standard", "target");
+        ctx.addServletMapping("/admin", "target");
+
+        // Configure the Realm
+        TesterMapRealm realm = new TesterMapRealm();
+        realm.addUser(STANDARD_USER, STANDARD_USER);
+        realm.addUserRole(STANDARD_USER, "standard");
+        realm.addUser(ADMIN_USER, ADMIN_USER);
+        realm.addUserRole(ADMIN_USER, "standard");
+        realm.addUserRole(ADMIN_USER, "admin");
+        ctx.setRealm(realm);
+
+        // Configure the security constraints
+        // /standard is protected for all methods
+        SecurityConstraint constraintStandard = new SecurityConstraint();
+        SecurityCollection collectionStandard = new SecurityCollection();
+        collectionStandard.setName("Protect standard");
+        collectionStandard.addPattern("/standard");
+        constraintStandard.addCollection(collectionStandard);
+        constraintStandard.addAuthRole("standard");
+        ctx.addConstraint(constraintStandard);
+        // /admin is only protected for POST
+        SecurityConstraint constraintAdmin = new SecurityConstraint();
+        SecurityCollection collectionAdmin = new SecurityCollection();
+        collectionAdmin.addMethod("POST");
+        collectionAdmin.setName("Protect admin");
+        collectionAdmin.addPattern("/admin");
+        constraintAdmin.addCollection(collectionAdmin);
+        constraintAdmin.addAuthRole("admin");
+        ctx.addConstraint(constraintAdmin);
+
+        // Configure authentication
+        LoginConfig lc = new LoginConfig();
+        lc.setAuthMethod("FORM");
+        lc.setLoginPage("/login");
+        lc.setErrorPage("/error");
+        ctx.setLoginConfig(lc);
+        ctx.getPipeline().addValve(new FormAuthenticator());
+
+        tomcat.start();
+
+        TestHttpClient client = new TestHttpClient();
+        client.setPort(getPort());
+        client.setUseContentLength(true);
+        client.setUseCookies(true);
+        client.setRequestPause(0);
+        client.connect();
+
+        return client;
+    }
+
+    private void submitFormAuth(SimpleHttpClient client, String user) throws 
Exception {
+        client.setRequest(new String[] {
+                "POST /j_security_check HTTP/1.1" + CRLF,
+                "Host: localhost:" + getPort() + CRLF,
+                "Cookie: JSESSIONID=" + client.getSessionId() + CRLF,
+                "Content-Type: application/x-www-form-urlencoded" + CRLF,
+                "Content-Length: " + (23 + user.length() * 2) + CRLF,
+                CRLF,
+                "j_username=" + user + "&j_password=" + user });
+        client.processRequest();
+        Assert.assertEquals(303, client.getStatusCode());
+        client.resetResponse();
+    }
+
+
+    private void requestStandard(SimpleHttpClient client, String user, boolean 
authRedirectExpected) throws Exception {
+        String sessionID = client.getSessionId();
+
+        client.setRequest(new String[] {
+                "GET /standard HTTP/1.1" + CRLF,
+                "Host: localhost:" + getPort() + CRLF,
+                "Cookie: " + (sessionID == null ? "a=b" : "JSESSIONID=" + 
sessionID) + CRLF,
+                CRLF });
+        client.processRequest();
+        Assert.assertEquals(200, client.getStatusCode());
+        if (authRedirectExpected) {
+            
Assert.assertTrue(client.getResponseBody().contains("j_security_check"));
+        } else {
+            Assert.assertEquals("GET" + System.lineSeparator() + user + 
System.lineSeparator() + ADMIN_USER.equals(user) +
+                    System.lineSeparator() + "null" + System.lineSeparator(), 
client.getResponseBody());
+        }
+        client.resetResponse();
+    }
+
+
+    private void requestAdmin(SimpleHttpClient client, String user, String 
requestMethod, String resultMethod,
+            boolean authRedirectExpected) throws Exception {
+        String sessionID = client.getSessionId();
+
+        client.setRequest(
+                new String[] {
+                        requestMethod + " /admin HTTP/1.1" + CRLF,
+                        "Host: localhost:" + getPort() + CRLF,
+                        "Cookie: " + (sessionID == null ? "a=b" : 
"JSESSIONID=" + sessionID) + CRLF,
+                        CRLF });
+        client.processRequest();
+        String body = client.getResponseBody();
+        if (authRedirectExpected) {
+            Assert.assertEquals(body, 200, client.getStatusCode());
+            Assert.assertTrue(body, body.contains("j_security_check"));
+        } else {
+            if (Method.POST.equals(resultMethod)) {
+                if (ADMIN_USER.equals(user)) {
+                    Assert.assertEquals(body, 200, client.getStatusCode());
+                    Assert.assertEquals(body, resultMethod + 
System.lineSeparator() + user + System.lineSeparator() +
+                            "true" + System.lineSeparator() + "null" + 
System.lineSeparator(),
+                            client.getResponseBody());
+                } else {
+                    Assert.assertEquals(body, 403, client.getStatusCode());
+                }
+            } else {
+                Assert.assertEquals(body, 200, client.getStatusCode());
+                Assert.assertEquals(body, resultMethod + 
System.lineSeparator() + user + System.lineSeparator() +
+                        ADMIN_USER.equals(user) + System.lineSeparator() + 
"null" + System.lineSeparator(),
+                        client.getResponseBody());
+            }
+        }
+        client.resetResponse();
+    }
+
+
+    private static class TestHttpClient extends SimpleHttpClient {
+
+        @Override
+        public boolean isResponseBodyOK() {
+            return true;
+        }
+    }
+
+
+    private static class LoginServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            resp.setContentType("text/html");
+            resp.setCharacterEncoding(StandardCharsets.UTF_8);
+            PrintWriter pw = resp.getWriter();
+            pw.println("<html>");
+            pw.println("<body>");
+            pw.println("<form method=\"post\" action=\"j_security_check\">");
+            pw.println("<input name=\"j_username\">");
+            pw.println("<input name=\"j_password\" type=\"password\">");
+            pw.println("<button type=\"submit\">Login</button>");
+            pw.println("</form>");
+            pw.println("</body>");
+            pw.println("</html>");
+        }
+    }
+
+
+    private static class ErrorServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            resp.setContentType("text/plain");
+            resp.setCharacterEncoding(StandardCharsets.UTF_8);
+            PrintWriter pw = resp.getWriter();
+            pw.println("Login failed");
+        }
+    }
+
+
+    private static class TargetServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            resp.setContentType("text/plain");
+            resp.setCharacterEncoding(StandardCharsets.UTF_8);
+            PrintWriter pw = resp.getWriter();
+            pw.println(req.getMethod());
+            pw.println(req.getRemoteUser());
+            pw.println(req.isUserInRole("admin"));
+            pw.println(req.getParameter("action"));
+        }
+
+        @Override
+        protected void doPost(HttpServletRequest req, HttpServletResponse 
resp) throws ServletException, IOException {
+            doGet(req, resp);
+        }
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index c37ae9230d..6879998e1d 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -310,6 +310,15 @@
         Ensure the security constraint with the longest matching path is
         selected when more than one constraint matches the request path. 
(markt)
       </fix>
+      <fix>
+        If the request saved by FORM authentication uses a method other than
+        GET, ensure that the security constraints are re-assessed after the
+        saved request is restored and before it is processed. Custom
+        <code>Authenticator</code> implementations that extend
+        <code>FormAuthenticator</code> and override
+        <code>doAuthenticate()</code> and/or <code>restoreRequest()</code> will
+        require modification. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to