Skip to content

Commit

Permalink
Merge pull request #8286 from eclipse/jetty-10.0.x-8216-openid-logout
Browse files Browse the repository at this point in the history
Issue #8216 - OpenID Connect RP-Initiated Logout
  • Loading branch information
lachlan-roberts authored Jul 20, 2022
2 parents 26a255e + c38b326 commit b74da7c
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -103,11 +105,18 @@ public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage)
}

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
Expand All @@ -123,11 +132,15 @@ public void setConfiguration(AuthConfiguration authConfig)

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));
}
Expand Down Expand Up @@ -166,6 +179,22 @@ else if (!redirectPath.startsWith("/"))
_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)
Expand Down Expand Up @@ -218,6 +247,7 @@ public UserIdentity login(String username, Object credentials, ServletRequest re
@Override
public void logout(ServletRequest request)
{
attemptLogoutRedirect(request);
super.logout(request);
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpSession session = httpRequest.getSession(false);
Expand All @@ -230,6 +260,64 @@ public void logout(ServletRequest request)
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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -97,14 +99,38 @@ public OpenIdConfiguration(@Name("issuer") String issuer,
@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");
Expand Down Expand Up @@ -140,6 +166,10 @@ protected void processMetadata(Map<String, Object> discoveryDocument)
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.");
Expand Down Expand Up @@ -213,6 +243,11 @@ public String getTokenEndpoint()
{
return tokenEndpoint;
}

public String getEndSessionEndpoint()
{
return endSessionEndpoint;
}

public String getAuthMethod()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand All @@ -35,6 +36,7 @@

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;

Expand Down Expand Up @@ -102,6 +104,7 @@ public void setup() throws Exception
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();
Expand Down Expand Up @@ -155,6 +158,11 @@ public void testLoginLogout() throws Exception
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
Expand All @@ -171,10 +179,9 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t
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();
}
}

Expand Down
Loading

0 comments on commit b74da7c

Please sign in to comment.