merge openid changes to ee10

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2022-07-20 15:11:02 +10:00
parent 7ad938753c
commit fe4ce1443b
4 changed files with 198 additions and 15 deletions

View File

@ -25,8 +25,8 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
import org.eclipse.jetty.ee10.servlet.ServletContextResponse;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.Authenticator;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
@ -69,6 +69,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public static final String RESPONSE = "org.eclipse.jetty.security.openid.response";
public static final String ISSUER = "org.eclipse.jetty.security.openid.issuer";
public static final String REDIRECT_PATH = "org.eclipse.jetty.security.openid.redirect_path";
public static final String LOGOUT_REDIRECT_PATH = "org.eclipse.jetty.security.openid.logout_redirect_path";
public static final String ERROR_PAGE = "org.eclipse.jetty.security.openid.error_page";
public static final String J_URI = "org.eclipse.jetty.security.openid.URI";
public static final String J_POST = "org.eclipse.jetty.security.openid.POST";
@ -83,6 +84,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
private final SecureRandom _secureRandom = new SecureRandom();
private OpenIdConfiguration _openIdConfiguration;
private String _redirectPath;
private String _logoutRedirectPath;
private String _errorPage;
private String _errorPath;
private String _errorQuery;
@ -104,15 +106,22 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
public OpenIdAuthenticator(OpenIdConfiguration configuration, String redirectPath, String errorPage)
{
this(configuration, redirectPath, errorPage, null);
}
public OpenIdAuthenticator(OpenIdConfiguration configuration, String redirectPath, String errorPage, String logoutRedirectPath)
{
_openIdConfiguration = configuration;
setRedirectPath(redirectPath);
if (errorPage != null)
setErrorPage(errorPage);
if (logoutRedirectPath != null)
setLogoutRedirectPath(logoutRedirectPath);
}
@Override
public void setConfiguration(Authenticator.AuthConfiguration authConfig)
public void setConfiguration(AuthConfiguration authConfig)
{
if (_openIdConfiguration == null)
{
@ -124,12 +133,16 @@ public class OpenIdAuthenticator extends LoginAuthenticator
String redirectPath = authConfig.getInitParameter(REDIRECT_PATH);
if (redirectPath != null)
_redirectPath = redirectPath;
setRedirectPath(redirectPath);
String error = authConfig.getInitParameter(ERROR_PAGE);
if (error != null)
setErrorPage(error);
String logout = authConfig.getInitParameter(LOGOUT_REDIRECT_PATH);
if (logout != null)
setLogoutRedirectPath(logout);
super.setConfiguration(new OpenIdAuthConfiguration(_openIdConfiguration, authConfig));
}
@ -167,6 +180,22 @@ public class OpenIdAuthenticator extends LoginAuthenticator
_redirectPath = redirectPath;
}
public void setLogoutRedirectPath(String logoutRedirectPath)
{
if (logoutRedirectPath == null)
{
LOG.warn("redirect path must not be null, defaulting to /");
logoutRedirectPath = "/";
}
else if (!logoutRedirectPath.startsWith("/"))
{
LOG.warn("redirect path must start with /");
logoutRedirectPath = "/" + logoutRedirectPath;
}
_logoutRedirectPath = logoutRedirectPath;
}
public void setErrorPage(String path)
{
if (path == null || path.trim().length() == 0)
@ -220,6 +249,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
@Override
public void logout(Request request)
{
attemptLogoutRedirect(request);
super.logout(request);
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
HttpServletRequest httpRequest = servletContextRequest.getHttpServletRequest();
@ -233,6 +263,65 @@ public class OpenIdAuthenticator extends LoginAuthenticator
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
session.removeAttribute(CLAIMS);
session.removeAttribute(RESPONSE);
session.removeAttribute(ISSUER);
}
}
/**
* This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.
*
* If end_session_endpoint is defined the request will be redirected to the end_session_endpoint, the optional
* post_logout_redirect_uri parameter will be set if {@link #REDIRECT_PATH} is non-null.
*
* If the end_session_endpoint is not defined then the request will be redirected to {@link #REDIRECT_PATH} if it is a
* non-null value, otherwise no redirection will be done.
*
* @param request the request to redirect.
*/
private void attemptLogoutRedirect(Request request)
{
try
{
ServletContextRequest baseRequest = Request.as(request, ServletContextRequest.class);
ServletContextResponse baseResponse = baseRequest.getResponse();
HttpServletRequest httpServletRequest = baseRequest.getHttpServletRequest();
HttpServletResponse httpServletResponse = baseResponse.getHttpServletResponse();
String endSessionEndpoint = _openIdConfiguration.getEndSessionEndpoint();
String redirectUri = null;
if (_logoutRedirectPath != null)
{
StringBuilder sb = new StringBuilder(128);
URIUtil.appendSchemeHostPort(sb, httpServletRequest.getScheme(), httpServletRequest.getServerName(), httpServletRequest.getServerPort());
sb.append(httpServletRequest.getContextPath());
sb.append(_logoutRedirectPath);
redirectUri = sb.toString();
}
HttpSession session = baseRequest.getHttpServletRequest().getSession(false);
if (endSessionEndpoint == null || session == null)
{
if (redirectUri != null)
httpServletResponse.sendRedirect(redirectUri);
return;
}
Object openIdResponse = session.getAttribute(OpenIdAuthenticator.RESPONSE);
if (!(openIdResponse instanceof Map))
{
if (redirectUri != null)
httpServletResponse.sendRedirect(redirectUri);
return;
}
@SuppressWarnings("rawtypes")
String idToken = (String)((Map)openIdResponse).get("id_token");
httpServletResponse.sendRedirect(endSessionEndpoint +
"?id_token_hint=" + UrlEncoded.encodeString(idToken, StandardCharsets.UTF_8) +
((redirectUri == null) ? "" : "&post_logout_redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8)));
}
catch (Throwable t)
{
LOG.warn("failed to redirect to end_session_endpoint", t);
}
}

View File

@ -42,6 +42,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
private static final String TOKEN_ENDPOINT = "token_endpoint";
private static final String END_SESSION_ENDPOINT = "end_session_endpoint";
private static final String ISSUER = "issuer";
private final HttpClient httpClient;
@ -52,6 +53,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
private final String authMethod;
private String authEndpoint;
private String tokenEndpoint;
private String endSessionEndpoint;
private boolean authenticateNewUsers = false;
/**
@ -97,14 +99,38 @@ public class OpenIdConfiguration extends ContainerLifeCycle
@Name("clientSecret") String clientSecret,
@Name("authMethod") String authMethod,
@Name("httpClient") HttpClient httpClient)
{
this(issuer, authorizationEndpoint, tokenEndpoint, null, clientId, clientSecret, authMethod, httpClient);
}
/**
* Create an OpenID configuration for a specific OIDC provider.
* @param issuer The URL of the OpenID provider.
* @param authorizationEndpoint the URL of the OpenID provider's authorization endpoint if configured.
* @param tokenEndpoint the URL of the OpenID provider's token endpoint if configured.
* @param endSessionEndpoint the URL of the OpdnID provider's end session endpoint if configured.
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
* @param clientSecret The client secret known only by the Client and the Authorization Server.
* @param authMethod Authentication method to use with the Token Endpoint.
* @param httpClient The {@link HttpClient} instance to use.
*/
public OpenIdConfiguration(@Name("issuer") String issuer,
@Name("authorizationEndpoint") String authorizationEndpoint,
@Name("tokenEndpoint") String tokenEndpoint,
@Name("endSessionEndpoint") String endSessionEndpoint,
@Name("clientId") String clientId,
@Name("clientSecret") String clientSecret,
@Name("authMethod") String authMethod,
@Name("httpClient") HttpClient httpClient)
{
this.issuer = issuer;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.authEndpoint = authorizationEndpoint;
this.endSessionEndpoint = endSessionEndpoint;
this.tokenEndpoint = tokenEndpoint;
this.httpClient = httpClient != null ? httpClient : newHttpClient();
this.authMethod = authMethod;
this.authMethod = authMethod == null ? "client_secret_post" : authMethod;
if (this.issuer == null)
throw new IllegalArgumentException("Issuer was not configured");
@ -140,6 +166,10 @@ public class OpenIdConfiguration extends ContainerLifeCycle
if (tokenEndpoint == null)
throw new IllegalStateException(TOKEN_ENDPOINT);
// End session endpoint is optional.
if (endSessionEndpoint == null)
endSessionEndpoint = (String)discoveryDocument.get(END_SESSION_ENDPOINT);
// We are lenient and not throw here as some major OIDC providers do not conform to this.
if (!Objects.equals(discoveryDocument.get(ISSUER), issuer))
LOG.warn("The issuer in the metadata is not correct.");
@ -166,8 +196,8 @@ public class OpenIdConfiguration extends ContainerLifeCycle
{
Map<?, ?> rawResult = (Map<?, ?>)parsedResult;
result = rawResult.entrySet().stream()
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(it -> it.getKey().toString(), Map.Entry::getValue));
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(it -> it.getKey().toString(), Map.Entry::getValue));
if (LOG.isDebugEnabled())
LOG.debug("discovery document {}", result);
return result;
@ -214,6 +244,11 @@ public class OpenIdConfiguration extends ContainerLifeCycle
return tokenEndpoint;
}
public String getEndSessionEndpoint()
{
return endSessionEndpoint;
}
public String getAuthMethod()
{
return authMethod;

View File

@ -17,6 +17,7 @@ import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -35,6 +36,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
@ -102,6 +104,7 @@ public class OpenIdAuthenticationTest
server.addBean(new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET));
securityHandler.setInitParameter(OpenIdAuthenticator.REDIRECT_PATH, "/redirect_path");
securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
securityHandler.setInitParameter(OpenIdAuthenticator.LOGOUT_REDIRECT_PATH, "/");
context.setSecurityHandler(securityHandler);
server.start();
@ -155,6 +158,11 @@ public class OpenIdAuthenticationTest
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));
// Test that the user was logged out successfully on the openid provider.
assertThat(openIdProvider.getLoggedInUsers().getCurrent(), equalTo(0L));
assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}
public static class LoginPage extends HttpServlet
@ -171,10 +179,9 @@ public class OpenIdAuthenticationTest
public static class LogoutPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
request.getSession().invalidate();
response.sendRedirect("/");
request.logout();
}
}

View File

@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import jakarta.servlet.ServletException;
@ -34,6 +35,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.statistic.CounterStatistic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -44,6 +46,7 @@ public class OpenIdProvider extends ContainerLifeCycle
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private static final String AUTH_PATH = "/auth";
private static final String TOKEN_PATH = "/token";
private static final String END_SESSION_PATH = "/end_session";
private final Map<String, User> issuedAuthCodes = new HashMap<>();
protected final String clientId;
@ -54,6 +57,7 @@ public class OpenIdProvider extends ContainerLifeCycle
private int port = 0;
private String provider;
private User preAuthedUser;
private final CounterStatistic loggedInUsers = new CounterStatistic();
public static void main(String[] args) throws Exception
{
@ -87,9 +91,10 @@ public class OpenIdProvider extends ContainerLifeCycle
ServletContextHandler contextHandler = new ServletContextHandler();
contextHandler.setContextPath("/");
contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH);
contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH);
contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH);
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
server.setHandler(contextHandler);
addBean(server);
@ -108,6 +113,11 @@ public class OpenIdProvider extends ContainerLifeCycle
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
}
public CounterStatistic getLoggedInUsers()
{
return loggedInUsers;
}
@Override
protected void doStart() throws Exception
{
@ -140,7 +150,7 @@ public class OpenIdProvider extends ContainerLifeCycle
redirectUris.add(uri);
}
public class OpenIdAuthEndpoint extends HttpServlet
public class AuthEndpoint extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
@ -244,7 +254,7 @@ public class OpenIdProvider extends ContainerLifeCycle
}
}
public class OpenIdTokenEndpoint extends HttpServlet
private class TokenEndpoint extends HttpServlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
@ -277,12 +287,45 @@ public class OpenIdProvider extends ContainerLifeCycle
"\"token_type\": \"Bearer\"" +
"}";
loggedInUsers.increment();
resp.setContentType("text/plain");
resp.getWriter().print(response);
}
}
public class OpenIdConfigServlet extends HttpServlet
private class EndSessionEndpoint extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String idToken = req.getParameter("id_token_hint");
if (idToken == null)
{
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
return;
}
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
if (logoutRedirect == null)
{
resp.setStatus(HttpServletResponse.SC_OK);
resp.getWriter().println("logout success on end_session_endpoint");
return;
}
loggedInUsers.decrement();
resp.setContentType("text/plain");
resp.sendRedirect(logoutRedirect);
}
}
private class ConfigServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
@ -291,6 +334,7 @@ public class OpenIdProvider extends ContainerLifeCycle
"\"issuer\": \"" + provider + "\"," +
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
"}";
resp.getWriter().write(discoveryDocument);
@ -328,5 +372,13 @@ public class OpenIdProvider extends ContainerLifeCycle
long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof User))
return false;
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
}
}
}