This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new 58123aa468 Refresh constraints after FORM auth if method changes
58123aa468 is described below
commit 58123aa468a20e2a079b7e0c68a4009e2475c098
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 b23fdbe87d..e53109b35e 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?
@@ -1455,4 +1509,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 a21891c9bb..3231f70b0e 100644
--- a/java/org/apache/catalina/authenticator/FormAuthenticator.java
+++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java
@@ -150,18 +150,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;
@@ -183,7 +175,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()) {
@@ -199,24 +191,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
@@ -241,7 +233,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);
@@ -253,10 +245,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
@@ -274,7 +266,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()) {
@@ -313,7 +305,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);
@@ -349,7 +341,14 @@ public class FormAuthenticator extends AuthenticatorBase {
internalResponse.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();
}
@@ -519,7 +518,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
*/
@@ -562,23 +561,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
@@ -643,6 +642,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
@@ -661,7 +661,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 2991e55d9d..ecde97da20 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -193,6 +193,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">
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.