Skip to content

Commit

Permalink
[ELY-2534] OIDC logout support
Browse files Browse the repository at this point in the history
  • Loading branch information
rsearls committed Dec 18, 2024
1 parent 0ba9eea commit ebdeafe
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 190 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2021 Red Hat, Inc., and individual contributors
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ public enum Reason {
INVALID_TOKEN,
STALE_TOKEN,
NO_AUTHORIZATION_HEADER,
NO_QUERY_PARAMETER_ACCESS_TOKEN
NO_QUERY_PARAMETER_ACCESS_TOKEN,
NO_SESSION_ID,
METHOD_NOT_ALLOWED
}

private Reason reason;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,12 @@ interface ElytronMessages extends BasicLogger {

@Message(id = 23070, value = "Authentication request format must be one of the following: oauth2, request, request_uri.")
RuntimeException invalidAuthenticationRequestFormat();

@Message(id = 23071, value = "%s is not a valid value for %s")
RuntimeException invalidLogoutPath(String pathValue, String pathName);

@Message(id = 23072, value = "The end substring of %s: %s can not be identical to %s: %s")
RuntimeException invalidLogoutCallbackPath(String callbackPathTitle, String callbacPathkValue,
String logoutPathTitle, String logoutPathValue);
}

Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,22 @@
import org.apache.http.client.utils.URIBuilder;
import org.jose4j.jwt.JwtClaims;
import org.wildfly.security.http.HttpConstants;
import org.wildfly.security.http.HttpScope;
import org.wildfly.security.http.Scope;
import org.wildfly.security.http.oidc.OidcHttpFacade.Request;

/**
* @author <a href="mailto:[email protected]">Pedro Igor</a>
*/
final class LogoutHandler {

private static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri";
private static final String ID_TOKEN_HINT_PARAM = "id_token_hint";
public static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri";
public static final String ID_TOKEN_HINT_PARAM = "id_token_hint";
private static final String LOGOUT_TOKEN_PARAM = "logout_token";
private static final String LOGOUT_TOKEN_TYPE = "Logout";
private static final String SID = "sid";
private static final String ISS = "iss";
private static final String CLIENT_ID_SID_SEPARATOR = "-";
public static final String SID = "sid";
public static final String ISS = "iss";

/**
* A bounded map to store sessions marked for invalidation after receiving logout requests through the back-channel
Expand All @@ -60,27 +63,24 @@ protected boolean removeEldestEntry(Map.Entry<String, OidcClientConfiguration> e
});

boolean tryLogout(OidcHttpFacade facade) {
log.trace("tryLogout entered");
RefreshableOidcSecurityContext securityContext = getSecurityContext(facade);

if (securityContext == null) {
// no active session
log.trace("tryLogout securityContext == null");
return false;
}

if (isSessionMarkedForInvalidation(facade)) {
// session marked for invalidation, invalidate it
log.debug("Invalidating pending logout session");
facade.getTokenStore().logout(false);
return true;
}

if (isRpInitiatedLogoutUri(facade)) {
if (isRpInitiatedLogoutPath(facade)) {
log.trace("isRpInitiatedLogoutPath");
redirectEndSessionEndpoint(facade);
return true;
}

if (isLogoutCallbackUri(facade)) {
if (isLogoutCallbackPath(facade)) {
log.trace("isLogoutCallbackPath");
if (isFrontChannel(facade)) {
log.trace("isFrontChannel");
handleFrontChannelLogoutRequest(facade);
return true;
} else {
Expand All @@ -89,50 +89,41 @@ boolean tryLogout(OidcHttpFacade facade) {
facade.authenticationFailed();
}
}

return false;
}

boolean tryBackChannelLogout(OidcHttpFacade facade) {
if (isLogoutCallbackUri(facade)) {
if (isBackChannel(facade)) {
handleBackChannelLogoutRequest(facade);
return true;
} else {
// no active session, should have received a POST logout request
facade.getResponse().setStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
facade.authenticationFailed();
}
boolean isSessionMarkedForInvalidation(OidcHttpFacade facade) {
HttpScope session = facade.getScope(Scope.SESSION);
if (session == null || ! session.exists()) return false;
RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) session.getAttachment(OidcSecurityContext.class.getName());
if (securityContext == null) {
return false;
}
return false;
}

private boolean isSessionMarkedForInvalidation(OidcHttpFacade facade) {
RefreshableOidcSecurityContext securityContext = getSecurityContext(facade);
IDToken idToken = securityContext.getIDToken();

if (idToken == null) {
return false;
}

return sessionsMarkedForInvalidation.remove(idToken.getSid()) != null;
return sessionsMarkedForInvalidation.remove(getSessionKey(facade, idToken.getSid())) != null;
}

private void redirectEndSessionEndpoint(OidcHttpFacade facade) {
RefreshableOidcSecurityContext securityContext = getSecurityContext(facade);
OidcClientConfiguration clientConfiguration = securityContext.getOidcClientConfiguration();

String logoutUri;

try {
URIBuilder redirectUriBuilder = new URIBuilder(clientConfiguration.getEndSessionEndpointUrl())
.addParameter(ID_TOKEN_HINT_PARAM, securityContext.getIDTokenString());
String postLogoutUri = clientConfiguration.getPostLogoutUri();

if (postLogoutUri != null) {
redirectUriBuilder.addParameter(POST_LOGOUT_REDIRECT_URI_PARAM, getRedirectUri(facade) + postLogoutUri);
String postLogoutPath = clientConfiguration.getPostLogoutPath();
if (postLogoutPath != null) {
redirectUriBuilder.addParameter(POST_LOGOUT_REDIRECT_URI_PARAM,
getRedirectUri(facade) + postLogoutPath);
}

logoutUri = redirectUriBuilder.build().toString();
log.trace("redirectEndSessionEndpoint path: " + redirectUriBuilder.toString());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
Expand All @@ -142,6 +133,19 @@ private void redirectEndSessionEndpoint(OidcHttpFacade facade) {
facade.getResponse().setHeader(HttpConstants.LOCATION, logoutUri);
}

boolean tryBackChannelLogout(OidcHttpFacade facade) {
log.trace("tryBackChannelLogout entered");
if (isLogoutCallbackPath(facade)) {
log.trace("isLogoutCallbackPath");
if (isBackChannel(facade)) {
log.trace("isBackChannel");
handleBackChannelLogoutRequest(facade);
return true;
}
}
return false;
}

private void handleBackChannelLogoutRequest(OidcHttpFacade facade) {
String logoutToken = facade.getRequest().getFirstParam(LOGOUT_TOKEN_PARAM);
TokenValidator tokenValidator = TokenValidator.builder(facade.getOidcClientConfiguration())
Expand Down Expand Up @@ -175,7 +179,11 @@ private void handleBackChannelLogoutRequest(OidcHttpFacade facade) {
}

log.debug("Marking session for invalidation during back-channel logout");
sessionsMarkedForInvalidation.put(sessionId, facade.getOidcClientConfiguration());
sessionsMarkedForInvalidation.put(getSessionKey(facade, sessionId), facade.getOidcClientConfiguration());
}

private String getSessionKey(OidcHttpFacade facade, String sessionId) {
return facade.getOidcClientConfiguration().getClientId() + CLIENT_ID_SID_SEPARATOR + sessionId;
}

private void handleFrontChannelLogoutRequest(OidcHttpFacade facade) {
Expand Down Expand Up @@ -210,8 +218,7 @@ private String getRedirectUri(OidcHttpFacade facade) {
if (uri.indexOf('?') != -1) {
uri = uri.substring(0, uri.indexOf('?'));
}

int logoutPathIndex = uri.indexOf(getLogoutUri(facade));
int logoutPathIndex = uri.indexOf(getLogoutPath(facade));

if (logoutPathIndex != -1) {
uri = uri.substring(0, logoutPathIndex);
Expand All @@ -220,14 +227,14 @@ private String getRedirectUri(OidcHttpFacade facade) {
return uri;
}

private boolean isLogoutCallbackUri(OidcHttpFacade facade) {
private boolean isLogoutCallbackPath(OidcHttpFacade facade) {
String path = facade.getRequest().getRelativePath();
return path.endsWith(getLogoutCallbackUri(facade));
return path.endsWith(getLogoutCallbackPath(facade));
}

private boolean isRpInitiatedLogoutUri(OidcHttpFacade facade) {
private boolean isRpInitiatedLogoutPath(OidcHttpFacade facade) {
String path = facade.getRequest().getRelativePath();
return path.endsWith(getLogoutUri(facade));
return path.endsWith(getLogoutPath(facade));
}

private boolean isSessionRequiredOnLogout(OidcHttpFacade facade) {
Expand All @@ -246,12 +253,11 @@ private RefreshableOidcSecurityContext getSecurityContext(OidcHttpFacade facade)
return securityContext;
}

private String getLogoutUri(OidcHttpFacade facade) {
return facade.getOidcClientConfiguration().getLogoutUrl();
private String getLogoutPath(OidcHttpFacade facade) {
return facade.getOidcClientConfiguration().getLogoutPath();
}

private String getLogoutCallbackUri(OidcHttpFacade facade) {
return facade.getOidcClientConfiguration().getLogoutCallbackUrl();
private String getLogoutCallbackPath(OidcHttpFacade facade) {
return facade.getOidcClientConfiguration().getLogoutCallbackPath();
}

private boolean isBackChannel(OidcHttpFacade facade) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ public class Oidc {
public static final String CONFIDENTIAL_PORT = "confidential-port";
public static final String ENABLE_BASIC_AUTH = "enable-basic-auth";
public static final String PROVIDER_URL = "provider-url";
public static final String LOGOUT_PATH = "logout-path";
public static final String LOGOUT_CALLBACK_PATH = "logout-callback-path";
public static final String POST_LOGOUT_PATH = "post-logout-path";
public static final String LOGOUT_SESSION_REQUIRED = "logout-session-required";

/**
* Bearer token pattern.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
final class OidcAuthenticationMechanism implements HttpServerAuthenticationMechanism {

private static LogoutHandler logoutHandler = new LogoutHandler();

private final Map<String, ?> properties;
private final CallbackHandler callbackHandler;
private final OidcClientContext oidcClientContext;
Expand All @@ -59,6 +60,7 @@ public String getMechanismName() {

@Override
public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException {
log.debug("evaluateRequest uri: " + request.getRequestURI().toString());
OidcClientContext oidcClientContext = getOidcClientContext(request);
if (oidcClientContext == null) {
log.debugf("Ignoring request for path [%s] from mechanism [%s]. No client configuration context found.", request.getRequestURI(), getMechanismName());
Expand All @@ -74,6 +76,11 @@ public void evaluateRequest(HttpServerRequest request) throws HttpAuthentication
}

RequestAuthenticator authenticator = createRequestAuthenticator(httpFacade, oidcClientConfiguration);
if (logoutHandler.isSessionMarkedForInvalidation(httpFacade)) {
// session marked for invalidation, invalidate it
log.debug("Invalidating pending logout session");
httpFacade.getTokenStore().logout(false);
}
httpFacade.getTokenStore().checkCurrentToken();
if ((oidcClientConfiguration.getAuthServerBaseUrl() != null && keycloakPreActions(httpFacade, oidcClientConfiguration))
|| preflightCors(httpFacade, oidcClientConfiguration)) {
Expand All @@ -84,7 +91,8 @@ public void evaluateRequest(HttpServerRequest request) throws HttpAuthentication

AuthOutcome outcome = authenticator.authenticate();
if (AuthOutcome.AUTHENTICATED.equals(outcome)) {
if (new AuthenticatedActionsHandler(oidcClientConfiguration, httpFacade).handledRequest() || logoutHandler.tryLogout(httpFacade)) {
if (new AuthenticatedActionsHandler(oidcClientConfiguration, httpFacade).handledRequest()
|| logoutHandler.tryLogout(httpFacade)) {
httpFacade.authenticationInProgress();
} else {
httpFacade.authenticationComplete();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,12 @@ public enum RelativeUrlsUsed {
protected String requestObjectSigningKeyAlias;
protected String requestObjectSigningKeyStoreType;
protected JWKEncPublicKeyLocator encryptionPublicKeyLocator;
private boolean logoutSessionRequired = true;

private String postLogoutUri;

private String postLogoutPath;
private boolean sessionRequiredOnLogout = true;

private String logoutUrl = "/logout";

private String logoutCallbackUrl = "/logout/callback";
private String logoutPath = "/logout";
private String logoutCallbackPath = "/logout/callback";

private int logoutSessionWaitingLimit = 100;

Expand Down Expand Up @@ -338,6 +336,10 @@ public String getEndSessionEndpointUrl() {
return endSessionEndpointUrl;
}

public String getLogoutPath() {
return logoutPath;
}

public String getAccountUrl() {
resolveUrls();
return accountUrl;
Expand Down Expand Up @@ -702,14 +704,6 @@ public String getTokenSignatureAlgorithm() {
return tokenSignatureAlgorithm;
}

public void setPostLogoutUri(String postLogoutUri) {
this.postLogoutUri = postLogoutUri;
}

public String getPostLogoutUri() {
return postLogoutUri;
}

public boolean isSessionRequiredOnLogout() {
return sessionRequiredOnLogout;
}
Expand All @@ -718,21 +712,6 @@ public void setSessionRequiredOnLogout(boolean sessionRequiredOnLogout) {
this.sessionRequiredOnLogout = sessionRequiredOnLogout;
}

public String getLogoutUrl() {
return logoutUrl;
}

public void setLogoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
}

public String getLogoutCallbackUrl() {
return logoutCallbackUrl;
}

public void setLogoutCallbackUrl(String logoutCallbackUrl) {
this.logoutCallbackUrl = logoutCallbackUrl;
}
public int getLogoutSessionWaitingLimit() {
return logoutSessionWaitingLimit;
}
Expand Down Expand Up @@ -828,4 +807,32 @@ public void setEncryptionPublicKeyLocator(JWKEncPublicKeyLocator publicKeySetExt
public JWKEncPublicKeyLocator getEncryptionPublicKeyLocator() {
return this.encryptionPublicKeyLocator;
}

public void setPostLogoutPath(String postLogoutPath) {
this.postLogoutPath = postLogoutPath;
}

public String getPostLogoutPath() {
return postLogoutPath;
}

public boolean isLogoutSessionRequired() {
return logoutSessionRequired;
}

public void setLogoutSessionRequired(boolean logoutSessionRequired) {
this.logoutSessionRequired = logoutSessionRequired;
}

public void setLogoutPath(String logoutPath) {
this.logoutPath = logoutPath;
}

public String getLogoutCallbackPath() {
return logoutCallbackPath;
}

public void setLogoutCallbackPath(String logoutCallbackPath) {
this.logoutCallbackPath = logoutCallbackPath;
}
}
Loading

0 comments on commit ebdeafe

Please sign in to comment.