diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java index 95ab3576476..38422ed6e4b 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java @@ -68,21 +68,25 @@ public class FormAuthenticator extends LoginAuthenticator private String _formErrorPath; private String _formLoginPage; private String _formLoginPath; - private boolean _dispatch; private boolean _alwaysSaveUri; public FormAuthenticator() { } + @Deprecated public FormAuthenticator(String login, String error, boolean dispatch) + { + this(login, error); + } + + public FormAuthenticator(String login, String error) { this(); if (login != null) setLoginPage(login); if (error != null) setErrorPage(error); - _dispatch = dispatch; } /** @@ -113,8 +117,6 @@ public class FormAuthenticator extends LoginAuthenticator String error = configuration.getParameter(FormAuthenticator.__FORM_ERROR_PAGE); if (error != null) setErrorPage(error); - String dispatch = configuration.getParameter(FormAuthenticator.__FORM_DISPATCH); - _dispatch = dispatch == null ? _dispatch : Boolean.parseBoolean(dispatch); } @Override @@ -136,6 +138,11 @@ public class FormAuthenticator extends LoginAuthenticator _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?')); } + public String getLoginPage() + { + return _formLoginPage; + } + private void setErrorPage(String path) { if (path == null || path.trim().length() == 0) @@ -158,6 +165,11 @@ public class FormAuthenticator extends LoginAuthenticator } } + public String getErrorPage() + { + return _formErrorPage; + } + @Override public UserIdentity login(String username, Object password, Request request, Response response) { @@ -270,7 +282,8 @@ public class FormAuthenticator extends LoginAuthenticator final String password = parameters.getValue(__J_PASSWORD); UserIdentity user = login(username, password, request, response); - LOG.debug("jsecuritycheck {} {}", username, user); + if (LOG.isDebugEnabled()) + LOG.debug("jsecuritycheck {} {}", username, user); if (user != null) { // Redirect to original request @@ -285,11 +298,9 @@ public class FormAuthenticator extends LoginAuthenticator } // not authenticated - if (_formErrorPage == null) - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403); - else - Response.sendRedirect(request, response, callback, encodeURL(URIUtil.addPaths(request.getContext().getContextPath(), _formErrorPage), request), true); - + if (LOG.isDebugEnabled()) + LOG.debug("auth failed {}=={}", username, _formErrorPage); + sendError(request, response, callback); return AuthenticationState.SEND_FAILURE; } @@ -313,7 +324,8 @@ public class FormAuthenticator extends LoginAuthenticator // if we can't send challenge if (response.isCommitted()) { - LOG.debug("auth deferred {}", session == null ? null : session.getId()); + if (LOG.isDebugEnabled()) + LOG.debug("auth deferred {}", session == null ? null : session.getId()); return null; } @@ -352,10 +364,23 @@ public class FormAuthenticator extends LoginAuthenticator // send the challenge if (LOG.isDebugEnabled()) LOG.debug("challenge {}->{}", session.getId(), _formLoginPage); - Response.sendRedirect(request, response, callback, encodeURL(URIUtil.addPaths(request.getContext().getContextPath(), _formLoginPage), request), true); + sendChallenge(request, response, callback); return AuthenticationState.CHALLENGE; } + protected void sendError(Request request, Response response, Callback callback) + { + if (_formErrorPage == null) + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403); + else + Response.sendRedirect(request, response, callback, encodeURL(URIUtil.addPaths(request.getContext().getContextPath(), _formErrorPage), request), true); + } + + protected void sendChallenge(Request request, Response response, Callback callback) + { + Response.sendRedirect(request, response, callback, encodeURL(URIUtil.addPaths(request.getContext().getContextPath(), _formLoginPage), request), true); + } + public boolean isJSecurityCheck(String uri) { int jsc = uri.indexOf(__J_SECURITY_CHECK); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java index e1a6a780c3a..3db5cea24c8 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java @@ -445,6 +445,20 @@ public class ServletChannel _written = 0; } + + public void dispatched(Dispatchable dispatchable) throws Exception + { + if (LOG.isDebugEnabled()) + LOG.debug("handle {} {} ", _servletContextRequest.getHttpURI(), this); + + Action action = _state.handling(); + if (action != Action.DISPATCH) + throw new IllegalStateException(action.name()); + + dispatchable.dispatch(); + + } + /** * Handle the servlet request. This is called on the initial dispatch and then again on any asynchronous events. * @return True if the channel is ready to continue handling (ie it is not suspended) @@ -889,7 +903,7 @@ public class ServletChannel } } - interface Dispatchable + public interface Dispatchable { void dispatch() throws Exception; } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/FormAuthenticator.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/FormAuthenticator.java new file mode 100644 index 00000000000..4e0132490bc --- /dev/null +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/FormAuthenticator.java @@ -0,0 +1,199 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee10.servlet.security; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import org.eclipse.jetty.ee10.servlet.ServletChannel; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.security.authentication.SessionAuthentication; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * FORM Authenticator. + * + *

This authenticator implements form authentication will use dispatchers to + * the login page if the {@link #__FORM_DISPATCH} init parameter is set to true. + * Otherwise it will redirect.

+ * + *

The form authenticator redirects unauthenticated requests to a log page + * which should use a form to gather username/password from the user and send them + * to the /j_security_check URI within the context. FormAuthentication uses + * {@link SessionAuthentication} to wrap Authentication results so that they + * are associated with the session.

+ */ +public class FormAuthenticator extends org.eclipse.jetty.security.authentication.FormAuthenticator +{ + public static final String __FORM_DISPATCH = "org.eclipse.jetty.security.dispatch"; + + private boolean _dispatch; + + public FormAuthenticator() + { + } + + public FormAuthenticator(String login, String error, boolean dispatch) + { + super(login, error, dispatch); + _dispatch = dispatch; + } + + @Override + public void setConfiguration(Configuration configuration) + { + super.setConfiguration(configuration); + + String dispatch = configuration.getParameter(FormAuthenticator.__FORM_DISPATCH); + _dispatch = dispatch == null ? _dispatch : Boolean.parseBoolean(dispatch); + } + + @Override + protected void sendError(Request request, Response response, Callback callback) + { + if (_dispatch && getErrorPage() != null) + dispatch(getErrorPage(), request, response, callback); + else + super.sendError(request, response, callback); + } + + @Override + protected void sendChallenge(Request request, Response response, Callback callback) + { + if (_dispatch) + dispatch(getLoginPage(), request, response, callback); + else + super.sendChallenge(request, response, callback); + } + + private void dispatch(String path, Request request, Response response, Callback callback) + { + try + { + // We are before the ServletHandler, so we must do the association. + ServletChannel servletChannel = Request.get(request, ServletContextRequest.class, ServletContextRequest::getServletChannel); + servletChannel.associate(request, response, callback); + + response.getHeaders().put(HttpHeader.CACHE_CONTROL.asString(), HttpHeaderValue.NO_CACHE.asString()); + response.getHeaders().putDate(HttpHeader.EXPIRES.asString(), 1); + + ServletContextRequest contextRequest = Request.as(request, ServletContextRequest.class); + RequestDispatcher dispatcher = contextRequest.getServletApiRequest().getRequestDispatcher(path); + dispatcher.forward(new FormRequest(contextRequest.getServletApiRequest()), new FormResponse(contextRequest.getHttpServletResponse())); + + // TODO: we need to run the ServletChannel. + callback.succeeded(); + } + catch (Throwable t) + { + Response.writeError(request, response, callback, t); + } + } + + protected static class FormRequest extends HttpServletRequestWrapper + { + public FormRequest(HttpServletRequest request) + { + super(request); + } + + @Override + public long getDateHeader(String name) + { + if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) + return -1; + return super.getDateHeader(name); + } + + @Override + public String getHeader(String name) + { + if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) + return null; + return super.getHeader(name); + } + + @Override + public Enumeration getHeaderNames() + { + return Collections.enumeration(Collections.list(super.getHeaderNames())); + } + + @Override + public Enumeration getHeaders(String name) + { + if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) + return Collections.enumeration(Collections.emptyList()); + return super.getHeaders(name); + } + } + + protected static class FormResponse extends HttpServletResponseWrapper + { + public FormResponse(HttpServletResponse response) + { + super(response); + } + + @Override + public void addDateHeader(String name, long date) + { + if (notIgnored(name)) + super.addDateHeader(name, date); + } + + @Override + public void addHeader(String name, String value) + { + if (notIgnored(name)) + super.addHeader(name, value); + } + + @Override + public void setDateHeader(String name, long date) + { + if (notIgnored(name)) + super.setDateHeader(name, date); + } + + @Override + public void setHeader(String name, String value) + { + if (notIgnored(name)) + super.setHeader(name, value); + } + + private boolean notIgnored(String name) + { + if (HttpHeader.CACHE_CONTROL.is(name) || + HttpHeader.PRAGMA.is(name) || + HttpHeader.ETAG.is(name) || + HttpHeader.EXPIRES.is(name) || + HttpHeader.LAST_MODIFIED.is(name) || + HttpHeader.AGE.is(name)) + return false; + return true; + } + } +} diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/security/DefaultUserIdentity.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/security/DefaultUserIdentity.java new file mode 100644 index 00000000000..5d61c41232b --- /dev/null +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/security/DefaultUserIdentity.java @@ -0,0 +1,72 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee10.servlet.security; + +import java.security.Principal; +import javax.security.auth.Subject; + +import org.eclipse.jetty.security.DefaultIdentityService; +import org.eclipse.jetty.security.UserIdentity; + +/** + * The default implementation of UserIdentity. + */ +public class DefaultUserIdentity implements UserIdentity +{ + private final Subject _subject; + private final Principal _userPrincipal; + private final String[] _roles; + + public DefaultUserIdentity(Subject subject, Principal userPrincipal, String[] roles) + { + _subject = subject; + _userPrincipal = userPrincipal; + _roles = roles; + } + + @Override + public Subject getSubject() + { + return _subject; + } + + @Override + public Principal getUserPrincipal() + { + return _userPrincipal; + } + + @Override + public boolean isUserInRole(String role) + { + if (role == null) + return false; + + if (DefaultIdentityService.isRoleAssociated(role)) + return true; + + for (String r : _roles) + { + if (r.equals(role)) + return true; + } + return false; + } + + @Override + public String toString() + { + return DefaultUserIdentity.class.getSimpleName() + "('" + _userPrincipal + "')"; + } +} diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/security/FormAuthenticatorTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/security/FormAuthenticatorTest.java new file mode 100644 index 00000000000..887010ec3e3 --- /dev/null +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/security/FormAuthenticatorTest.java @@ -0,0 +1,100 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee10.servlet.security; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.security.Constraint; +import org.eclipse.jetty.security.EmptyLoginService; +import org.eclipse.jetty.security.SecurityHandler; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.URIUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +public class FormAuthenticatorTest +{ + private Server _server; + private LocalConnector _connector; + + @BeforeEach + public void configureServer() throws Exception + { + _server = new Server(); + _connector = new LocalConnector(_server); + _server.addConnector(_connector); + + ServletContextHandler contextHandler = new ServletContextHandler("/ctx", ServletContextHandler.SESSIONS); + _server.setHandler(contextHandler); + contextHandler.addServlet(new AuthenticationTestServlet() , "/"); + + SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped(); + securityHandler.setLoginService(new EmptyLoginService()); + contextHandler.insertHandler(securityHandler); + securityHandler.put("/any/*", Constraint.ANY_USER); + securityHandler.put("/known/*", Constraint.KNOWN_ROLE); + securityHandler.put("/admin/*", Constraint.from("admin")); + securityHandler.setAuthenticator(new FormAuthenticator("/login", "/error", true)); + + _server.start(); + } + + public static class AuthenticationTestServlet extends HttpServlet + { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + PrintWriter writer = resp.getWriter(); + writer.println("path: " + URIUtil.addPaths(req.getContextPath(), req.getServletPath())); + writer.println("dispatcherType: " + req.getDispatcherType()); + } + } + + @AfterEach + public void stopServer() throws Exception + { + if (_server.isRunning()) + { + _server.stop(); + _server.join(); + } + } + + @Test + public void testLoginDispatch() throws Exception + { + String response = _connector.getResponse("GET /ctx/admin/user HTTP/1.0\r\nHost:host:8888\r\n\r\n"); + assertThat(response, containsString("HTTP/1.1 200 OK")); + assertThat(response, containsString("dispatcherType: FORWARD")); + } + + @Test + public void testError() throws Exception + { + String response = _connector.getResponse("GET /ctx/j_security_check?j_username=user&j_password=wrong HTTP/1.0\r\nHost:host:8888\r\n\r\n"); + assertThat(response, containsString("path: /ctx/error")); + assertThat(response, containsString("dispatcherType: FORWARD")); + } +}