Issue #10295 - create an EE10 FormAuthenticator
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
c66f9809ba
commit
e9ab7498a9
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + "')";
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue