From 5282ca37ef7a81789b3c1d0a41854259898bce96 Mon Sep 17 00:00:00 2001 From: keller-j Date: Wed, 29 Jun 2022 18:01:00 +0200 Subject: [PATCH] Issue #8216 - provide logout for OpenID via OpenidAuthenticator Signed-off-by: Johannes Keller --- .../security/openid/OpenIdAuthenticator.java | 105 +++++++++++++++++- .../security/openid/OpenIdConfiguration.java | 55 ++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java index 7a987feaa91..cd4804eecdd 100644 --- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.security.openid; import java.io.IOException; import java.io.Serializable; import java.math.BigInteger; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.LinkedHashMap; @@ -75,6 +76,8 @@ public class OpenIdAuthenticator extends LoginAuthenticator public static final String J_SECURITY_CHECK = "/j_security_check"; public static final String ERROR_PARAMETER = "error_description_jetty"; private static final String CSRF_MAP = "org.eclipse.jetty.security.openid.csrf_map"; + public static final String LOGOUT_PAGE = "org.eclipse.jetty.security.openid.logout_page"; + public static final String REDIRECT_PATH_AFTER_LOGOUT = "org.eclipse.jetty.security.openid.redirect_after_logout"; @Deprecated public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token"; @@ -86,10 +89,12 @@ public class OpenIdAuthenticator extends LoginAuthenticator private String _errorPath; private String _errorQuery; private boolean _alwaysSaveUri; + private String _logoutPage; + private String _redirectPathAfterLogout; public OpenIdAuthenticator() { - this(null, J_SECURITY_CHECK, null); + this(null, J_SECURITY_CHECK, (String) null); } public OpenIdAuthenticator(OpenIdConfiguration configuration) @@ -101,13 +106,32 @@ public class OpenIdAuthenticator extends LoginAuthenticator { this(configuration, J_SECURITY_CHECK, errorPage); } + + public OpenIdAuthenticator(String errorPage, String logoutPage, OpenIdConfiguration configuration) + { + this(configuration, J_SECURITY_CHECK, errorPage, logoutPage, null); + } + + public OpenIdAuthenticator(String errorPage, String logoutPage, String redirectAfterLogout, OpenIdConfiguration configuration) + { + this(configuration, J_SECURITY_CHECK, errorPage, logoutPage, redirectAfterLogout); + } public OpenIdAuthenticator(OpenIdConfiguration configuration, String redirectPath, String errorPage) + { + this(configuration, redirectPath, errorPage, null, null); + } + + public OpenIdAuthenticator(OpenIdConfiguration configuration, String redirectPath, String errorPage, + String logoutPage, String redirectPathAfterLogout) { _openIdConfiguration = configuration; setRedirectPath(redirectPath); if (errorPage != null) setErrorPage(errorPage); + + _logoutPage = logoutPage; + _redirectPathAfterLogout = redirectPathAfterLogout; } @Override @@ -128,6 +152,14 @@ public class OpenIdAuthenticator extends LoginAuthenticator String error = authConfig.getInitParameter(ERROR_PAGE); if (error != null) setErrorPage(error); + + String logout = authConfig.getInitParameter(LOGOUT_PAGE); + if (logout != null) + _logoutPage = logout; + + String redirectAfterLogoutPath = authConfig.getInitParameter(REDIRECT_PATH_AFTER_LOGOUT); + if (redirectAfterLogoutPath != null) + _redirectPathAfterLogout = redirectAfterLogoutPath; super.setConfiguration(new OpenIdAuthConfiguration(_openIdConfiguration, authConfig)); } @@ -414,6 +446,10 @@ public class OpenIdAuthenticator extends LoginAuthenticator } if (LOG.isDebugEnabled()) LOG.debug("auth {}", authentication); + + if (isLogoutPage(request.getRequestURI()) && isAuthenticatedUser(authentication)) + return this.logout(request, response); + return authentication; } @@ -538,6 +574,73 @@ public class OpenIdAuthenticator extends LoginAuthenticator { return req.isSecure(); } + + public String getEndSessionEndpoint() + { + return _openIdConfiguration.getEndSessionEndpoint(); + } + + public boolean isLogoutPage(String contextPath) + { + return _logoutPage != null && (_logoutPage.equals(contextPath)); + } + + protected boolean isAuthenticatedUser(Authentication authentication) { + return authentication != null && authentication instanceof Authentication.User + && ((Authentication.User) authentication).getUserIdentity() != null; + } + + protected Authentication logout(HttpServletRequest request, HttpServletResponse response) throws ServerAuthException { + try + { + if (_redirectPathAfterLogout == null || (_redirectPathAfterLogout != null && _redirectPathAfterLogout.isEmpty())) + { + final Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request)); + final Response baseResponse = baseRequest.getResponse(); + // simple redirect without id_token_hint + baseResponse.sendRedirect(getEndSessionEndpoint(), true); + } else + { + // redirect with id_token_hint + OpenIdAuthenticator.logout(request, response, getEndSessionEndpoint(), _redirectPathAfterLogout); + } + + return Authentication.SEND_CONTINUE; + } catch (IOException e) + { + throw new ServerAuthException(e); + } + } + + public static void logout(HttpServletRequest request, HttpServletResponse response, String endSessionEndpoint, String redirectPath) throws IOException { + HttpSession session = request != null? request.getSession(false): null; + + if (request != null && response != null && endSessionEndpoint != null) + { + final Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request)); + final Response baseResponse = baseRequest.getResponse(); + + StringBuffer redirectUri = new StringBuffer(); + if (redirectPath != null && !redirectPath.isEmpty()) + { + URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(), request.getServerName(), request.getServerPort()); + redirectUri.append(redirectPath); + } + + if (session != null) + { + Object openIdResponse = session.getAttribute(OpenIdAuthenticator.RESPONSE); + if (openIdResponse != null && openIdResponse instanceof Map) + { + Map responseMap = (Map) openIdResponse; + String idToken = (String) responseMap.get("id_token"); + + baseResponse.sendRedirect(endSessionEndpoint + "?id_token_hint=" + URLEncoder.encode(idToken, StandardCharsets.UTF_8.toString()) + + (redirectPath != null? ("&post_logout_redirect_uri=" + URLEncoder.encode(redirectUri.toString(), StandardCharsets.UTF_8.toString())): "" ), true); + } + } + } + } private UriRedirectInfo removeAndClearCsrfMap(HttpSession session, String csrf) { diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java index e9fdf8ccf45..dc988b3347e 100644 --- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java @@ -49,6 +49,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle private final String authMethod; private String authEndpoint; private String tokenEndpoint; + private String endSessionEndpoint; private boolean authenticateNewUsers = false; /** @@ -74,9 +75,26 @@ public class OpenIdConfiguration extends ContainerLifeCycle public OpenIdConfiguration(String issuer, String authorizationEndpoint, String tokenEndpoint, String clientId, String clientSecret, HttpClient httpClient) { - this(issuer, authorizationEndpoint, tokenEndpoint, clientId, clientSecret, "client_secret_post", httpClient); + this(issuer, authorizationEndpoint, tokenEndpoint, null, clientId, clientSecret, "client_secret_post", 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 httpClient The {@link HttpClient} instance to use. + * @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. + */ + public OpenIdConfiguration(String issuer, String authorizationEndpoint, String tokenEndpoint, String endSesseionEndpoint, + HttpClient httpClient, String clientId, String clientSecret) + { + this(issuer, authorizationEndpoint, tokenEndpoint, endSesseionEndpoint, clientId, clientSecret, "client_secret_post", + httpClient); + } + /** * Create an OpenID configuration for a specific OIDC provider. * @param issuer The URL of the OpenID provider. @@ -94,11 +112,35 @@ 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; @@ -130,6 +172,10 @@ public class OpenIdConfiguration extends ContainerLifeCycle tokenEndpoint = (String)discoveryDocument.get("token_endpoint"); if (tokenEndpoint == null) throw new IllegalArgumentException("token_endpoint"); + + endSessionEndpoint = (String)discoveryDocument.get("end_session_endpoint"); + if (endSessionEndpoint == null) + throw new IllegalArgumentException("end_session_endpoint"); if (!Objects.equals(discoveryDocument.get("issuer"), issuer)) LOG.warn("The issuer in the metadata is not correct."); @@ -198,6 +244,11 @@ public class OpenIdConfiguration extends ContainerLifeCycle { return tokenEndpoint; } + + public String getEndSessionEndpoint() + { + return endSessionEndpoint; + } public String getAuthMethod() {