Merge pull request #8286 from eclipse/jetty-10.0.x-8216-openid-logout
Issue #8216 - OpenID Connect RP-Initiated Logout
This commit is contained in:
commit
b74da7ce3b
|
@ -68,6 +68,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";
|
||||
|
@ -82,6 +83,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;
|
||||
|
@ -103,11 +105,18 @@ 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
|
||||
|
@ -123,11 +132,15 @@ 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));
|
||||
}
|
||||
|
@ -166,6 +179,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)
|
||||
|
@ -218,6 +247,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
@Override
|
||||
public void logout(ServletRequest request)
|
||||
{
|
||||
attemptLogoutRedirect(request);
|
||||
super.logout(request);
|
||||
HttpServletRequest httpRequest = (HttpServletRequest)request;
|
||||
HttpSession session = httpRequest.getSession(false);
|
||||
|
@ -230,6 +260,64 @@ 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(ServletRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
|
||||
Response baseResponse = baseRequest.getResponse();
|
||||
String endSessionEndpoint = _openIdConfiguration.getEndSessionEndpoint();
|
||||
String redirectUri = null;
|
||||
if (_logoutRedirectPath != null)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(128);
|
||||
URIUtil.appendSchemeHostPort(sb, request.getScheme(), request.getServerName(), request.getServerPort());
|
||||
sb.append(baseRequest.getContextPath());
|
||||
sb.append(_logoutRedirectPath);
|
||||
redirectUri = sb.toString();
|
||||
}
|
||||
|
||||
HttpSession session = baseRequest.getSession(false);
|
||||
if (endSessionEndpoint == null || session == null)
|
||||
{
|
||||
if (redirectUri != null)
|
||||
baseResponse.sendRedirect(redirectUri, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Object openIdResponse = session.getAttribute(OpenIdAuthenticator.RESPONSE);
|
||||
if (!(openIdResponse instanceof Map))
|
||||
{
|
||||
if (redirectUri != null)
|
||||
baseResponse.sendRedirect(redirectUri, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
String idToken = (String)((Map)openIdResponse).get("id_token");
|
||||
baseResponse.sendRedirect(endSessionEndpoint +
|
||||
"?id_token_hint=" + UrlEncoded.encodeString(idToken, StandardCharsets.UTF_8) +
|
||||
((redirectUri == null) ? "" : "&post_logout_redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8)),
|
||||
true);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
LOG.warn("failed to redirect to end_session_endpoint", t);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
@ -213,6 +243,11 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
|||
{
|
||||
return tokenEndpoint;
|
||||
}
|
||||
|
||||
public String getEndSessionEndpoint()
|
||||
{
|
||||
return endSessionEndpoint;
|
||||
}
|
||||
|
||||
public String getAuthMethod()
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@ package org.eclipse.jetty.security.openid;
|
|||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
|
|||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
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;
|
||||
|
||||
|
@ -48,6 +49,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;
|
||||
|
@ -58,6 +60,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
|
||||
{
|
||||
|
@ -91,9 +94,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);
|
||||
|
@ -112,6 +116,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
|
||||
{
|
||||
|
@ -144,7 +153,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
|
||||
|
@ -252,7 +261,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
|
||||
|
@ -285,12 +294,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
|
||||
|
@ -299,6 +341,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);
|
||||
|
@ -336,5 +379,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue