Issue #10295 - create an EE10 FormAuthenticator

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2023-08-14 14:41:01 +10:00
parent c66f9809ba
commit e9ab7498a9
5 changed files with 423 additions and 13 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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.
*
* <p>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.</p>
*
* <p>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.</p>
*/
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<String> getHeaderNames()
{
return Collections.enumeration(Collections.list(super.getHeaderNames()));
}
@Override
public Enumeration<String> getHeaders(String name)
{
if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
return Collections.<String>enumeration(Collections.<String>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;
}
}
}

View File

@ -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 + "')";
}
}

View File

@ -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"));
}
}