Skip to content

Commit

Permalink
KNOX-2961 - Knox SSO cookie Invalidation - Phase I
Browse files Browse the repository at this point in the history
  • Loading branch information
smolnar82 committed Oct 2, 2023
1 parent 3af43b7 commit 800bb03
Show file tree
Hide file tree
Showing 27 changed files with 507 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@
<script src="libs/bower/jquery/js/jquery-3.5.1.min.js" ></script>

<script type="text/javascript" src="js/knoxauth.js"></script>

<%
final boolean autoGlobalLogout = "1".equals(request.getParameter("autoGlobalLogout"));
if (autoGlobalLogout) {%>
<script type="text/javascript">
window.onload=function() {
window.setTimeout(document.getElementById("globalLogoutForm").submit(), 10);
};
</script>
<%}%>

<%
String originalUrl = request.getParameter("originalUrl");
Topology topology = (Topology)request.getSession().getServletContext().getAttribute("org.apache.knox.gateway.topology");
Expand Down Expand Up @@ -123,8 +134,6 @@
response.setHeader("Location", globalLogoutPageURL);
return;
}
%>

<!-- Helper function to delete cookie -->
Expand Down Expand Up @@ -177,18 +186,20 @@
<%
if (globalLogoutPageURL != null && !globalLogoutPageURL.isEmpty()) {
%>
<p style="color: white;display: block">
<form method="POST" action="#" id="globalLogoutForm">
<div>
If you would like to logout of the Knox SSO session, you need to do so from
the configured SSO provider. Subsequently, authentication will be required to access
any SSO protected resources. Note that this may or may not invalidate any previously
established application sessions. Application sessions are subject to their application
specific session cookies and timeouts.
<a href="<%= request.getRequestURI() %>?globalLogout=1" >Global Logout</a>
</p>
<input type="hidden" name="globalLogout" value="1" id="globalLogoutUrl"/>
<button type="submit" style="background: none!important; border: none; padding: 0!important; color: #06A; text-decoration: none; cursor: pointer;">Global Logout</button>
</form>
</div>
<%
}
}
}
else {
%>
<div style="background: gray;text-color: white;text-align:center;">
Expand Down
4 changes: 4 additions & 0 deletions gateway-provider-security-jwt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,9 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public interface JWTMessages {
@Message( level = MessageLevel.DEBUG, text = "Sending redirect to: {0}" )
void sendRedirectToLoginURL(String loginURL);

@Message( level = MessageLevel.INFO, text = "Sending redirect to global logout URL: {0}" )
void sendRedirectToLogoutURL(String logoutURL);

@Message( level = MessageLevel.WARN, text = "Configuration for authentication provider URL is missing - will derive default URL." )
void missingAuthenticationProviderUrlConfiguration();

Expand Down Expand Up @@ -98,4 +101,7 @@ public interface JWTMessages {

@Message( level = MessageLevel.INFO, text = "Unexpected Issuer for token {0} ({1})." )
void unexpectedTokenIssuer(String tokenDisplayText, String tokenId);

@Message( level = MessageLevel.WARN, text = "Invalid SSO cookie found! Cleaning up..." )
void invalidSsoCookie();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
*/
package org.apache.knox.gateway.provider.federation.jwt.filter;

import org.apache.http.HttpHeaders;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import org.apache.knox.gateway.session.SessionInvalidators;
import org.apache.knox.gateway.util.AuthFilterUtils;
import org.apache.knox.gateway.util.CertificateUtils;
import org.apache.knox.gateway.util.CookieUtils;
import org.apache.knox.gateway.util.Urls;
import org.eclipse.jetty.http.MimeTypes;

import javax.security.auth.Subject;
Expand Down Expand Up @@ -71,7 +74,7 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter {
private String cookieName;
private String authenticationProviderUrl;
private String gatewayPath;
private Set<String> unAuthenticatedPaths = new HashSet(20);
private Set<String> unAuthenticatedPaths = new HashSet<>(20);

@Override
public void init( FilterConfig filterConfig ) throws ServletException {
Expand Down Expand Up @@ -175,7 +178,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha

// There were no valid cookies found so redirect to login url
if(res != null && !res.isCommitted()) {
sendRedirectToLoginURL(req, res);
// only if the Location header is not set already by a session invalidator
if (res.getHeader(HttpHeaders.LOCATION) == null) {
sendRedirectToLoginURL(req, res);
}
}
}
}
Expand All @@ -188,8 +194,25 @@ private void sendRedirectToLoginURL(HttpServletRequest request, HttpServletRespo
}

@Override
protected void handleValidationError(HttpServletRequest request, HttpServletResponse response,
int status, String error) throws IOException {
protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status, String error) throws IOException {
if (error != null && error.startsWith("Token") && error.endsWith("disabled")) {
LOGGER.invalidSsoCookie();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
removeAuthenticationToken(request, response);
SessionInvalidators.KNOX_SSO_INVALIDATOR.getSessionInvalidators().forEach(sessionInvalidator -> {
sessionInvalidator.onAuthenticationError(request, response);
});
final boolean doGlobalLogout = request.getAttribute("doGlobalLogout") == null ? false
: Boolean.parseBoolean((String) request.getAttribute("doGlobalLogout"));
if (doGlobalLogout) {
final String redirectTo = constructGlobalLogoutUrl(request);
LOGGER.sendRedirectToLogoutURL(redirectTo);
response.setHeader(HttpHeaders.LOCATION, redirectTo);
response.sendRedirect(redirectTo);
return;
}
}

/* We don't need redirect if this is a XHR request */
if (request.getHeader(XHR_HEADER) != null &&
request.getHeader(XHR_HEADER).equalsIgnoreCase(XHR_VALUE)) {
Expand All @@ -206,6 +229,12 @@ protected void handleValidationError(HttpServletRequest request, HttpServletResp
}
}

private String constructGlobalLogoutUrl(HttpServletRequest request) {
final StringBuilder logoutUrlBuilder = new StringBuilder(deriveDefaultAuthenticationProviderUrl(request, true));
logoutUrlBuilder.append('&').append(ORIGINAL_URL_QUERY_PARAM).append(deriveDefaultAuthenticationProviderUrl(request, false)); //orignalUrl=WebSSO login
return logoutUrlBuilder.toString();
}

/**
* Create the URL to be used for authentication of the user in the absence of
* a JWT token within the incoming request.
Expand All @@ -230,13 +259,17 @@ protected String constructLoginURL(HttpServletRequest request) {
+ request.getRequestURL().append(getOriginalQueryString(request));
}

public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request) {
return deriveDefaultAuthenticationProviderUrl(request, false);
}

/**
* Derive a provider URL from the request assuming that the
* KnoxSSO endpoint is local to the endpoint serving this request.
* @param request origin request
* @return url that is based on KnoxSSO endpoint
*/
public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request) {
public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request, boolean logout) {
String providerURL = null;
String scheme;
String host;
Expand All @@ -252,7 +285,7 @@ public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request)
if (!host.contains(":") && port != -1) {
sb.append(':').append(port);
}
sb.append('/').append(gatewayPath).append("/knoxsso/api/v1/websso");
sb.append('/').append(gatewayPath).append(logout ? "/knoxsso/knoxauth/logout.jsp?autoGlobalLogout=1" : "/knoxsso/api/v1/websso");
providerURL = sb.toString();
} catch (MalformedURLException e) {
LOGGER.failedToDeriveAuthenticationProviderUrl(e);
Expand All @@ -265,4 +298,22 @@ private String getOriginalQueryString(HttpServletRequest request) {
String originalQueryString = request.getQueryString();
return (originalQueryString == null) ? "" : "?" + originalQueryString;
}

private void removeAuthenticationToken(HttpServletRequest request, HttpServletResponse response) {
final Cookie c = new Cookie(cookieName, null);
c.setMaxAge(0);
c.setPath("/");
try {
String domainName = Urls.getDomainName(request.getRequestURL().toString(), null);
if(domainName != null) {
c.setDomain(domainName);
}
} catch (MalformedURLException e) {
//log.problemWithCookieDomainUsingDefault();
// we are probably not going to be able to
// remove the cookie due to this error but it
// isn't necessarily not going to work.
}
response.addCookie(c);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.knox.gateway.pac4j.filter;

import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.pac4j.Pac4jMessages;
import org.apache.knox.gateway.pac4j.config.ClientConfigurationDecorator;
Expand All @@ -32,6 +33,8 @@
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.services.security.MasterService;
import org.apache.knox.gateway.session.SessionInvalidator;
import org.apache.knox.gateway.session.SessionInvalidators;
import org.pac4j.config.client.PropertiesConfigFactory;
import org.pac4j.config.client.PropertiesConstants;
import org.pac4j.core.client.Client;
Expand All @@ -54,6 +57,8 @@
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
Expand All @@ -73,7 +78,7 @@
*
* @since 0.8.0
*/
public class Pac4jDispatcherFilter implements Filter {
public class Pac4jDispatcherFilter implements Filter, SessionInvalidator {
private static final String ALIAS_PREFIX = "${ALIAS=";
private static Pac4jMessages log = MessagesFactory.get(Pac4jMessages.class);

Expand Down Expand Up @@ -234,6 +239,8 @@ public void init( FilterConfig filterConfig ) throws ServletException {

config.setSessionStore(sessionStore);

SessionInvalidators.KNOX_SSO_INVALIDATOR.registerSessionInvalidator(this);

}

/**
Expand Down Expand Up @@ -321,6 +328,15 @@ public void doFilter( ServletRequest servletRequest, ServletResponse servletResp
}
}

@Override
public void onAuthenticationError(HttpServletRequest request, HttpServletResponse response) {
final GatewayConfig gatewayConfig = (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
if (gatewayConfig != null && gatewayConfig.getGlobalLogoutPageUrl() != null) {
request.setAttribute("doGlobalLogout", "true");
}
}

@Override
public void destroy() { }

}
5 changes: 5 additions & 0 deletions gateway-release/home/conf/topologies/homepage.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
<role>federation</role>
<name>SSOCookieProvider</name>
<enabled>true</enabled>
<param>
<!-- since 2.1.0: KnoxSSO cookie validation -->
<name>knox.token.exp.server-managed</name>
<value>false</value>
</param>
</provider>
<provider>
<role>identity-assertion</role>
Expand Down
5 changes: 5 additions & 0 deletions gateway-release/home/conf/topologies/knoxsso.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@
<name>knoxsso.token.ttl</name>
<value>86400000</value>
</param>
<param>
<!-- since 2.1.0: KnoxSSO cookie validation -->
<name>knox.token.exp.server-managed</name>
<value>false</value>
</param>
</service>

</topology>
5 changes: 5 additions & 0 deletions gateway-release/home/conf/topologies/manager.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<role>federation</role>
<name>SSOCookieProvider</name>
<enabled>true</enabled>
<param>
<!-- since 2.1.0: KnoxSSO cookie validation -->
<name>knox.token.exp.server-managed</name>
<value>false</value>
</param>
</provider>
<provider>
<role>identity-assertion</role>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,12 @@ private String getTimestampDisplay(long timestamp) {
}

/**
* Method that deletes expired tokens based on the token timestamp.
* Method that deletes expired tokens based on the token timestamp as well as disabled KnoxSSO cookies.
*/
protected void evictExpiredTokens() {
if (readyForEviction()) {
final Set<String> tokensToEvict = getExpiredTokens();
tokensToEvict.addAll(getDisabledKnoxSsoCookies());

if (!tokensToEvict.isEmpty()) {
removeTokens(tokensToEvict);
Expand Down Expand Up @@ -383,6 +384,22 @@ protected Set<String> getExpiredTokens() {
return expiredTokens;
}

protected Set<String> getDisabledKnoxSsoCookies() {
final Set<String> disbaledKnoxSsoCookies = new HashSet<>();
getTokenIds().forEach(tokenId -> {
TokenMetadata metadata = null;
try {
metadata = getTokenMetadata(tokenId);
} catch (UnknownTokenException e) {
// NOP
}
if (metadata != null && metadata.isKnoxSsoCookie() && !metadata.isEnabled()) {
disbaledKnoxSsoCookies.add(tokenId);
}
});
return disbaledKnoxSsoCookies;
}

/**
* Method that checks if a token's state is a candidate for eviction.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,20 @@ protected void evictExpiredTokens() {
} catch (SQLException e) {
log.errorRemovingTokensFromDatabase(e.getMessage(), e);
}

//removing disabled KnoxSSO cookies since they are no longer needed
try {
final Set<String> disabledKnoxSsoCookies = tokenDatabase.getDisabledKnoxSsoCookies();
if (!disabledKnoxSsoCookies.isEmpty()) {
log.removingDisabledKnoxSsoCookiesFromDatabase(disabledKnoxSsoCookies.size(),
String.join(", ", disabledKnoxSsoCookies.stream().map(tokenId -> Tokens.getTokenIDDisplayText(tokenId)).collect(Collectors.toSet())));
for (String tokenId : disabledKnoxSsoCookies) {
tokenDatabase.removeToken(tokenId);
}
}
} catch (SQLException e) {
log.errorRemovingDisabledKnoxSsoCookiesFromDatabase(e.getMessage(), e);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class TokenStateDatabase {
private static final String GET_TOKENS_CREATED_BY_USER_NAME_SQL = "SELECT kt.token_id, kt.issue_time, kt.expiration, kt.max_lifetime, ktm.md_name, ktm.md_value FROM " + TOKENS_TABLE_NAME
+ " kt, " + TOKEN_METADATA_TABLE_NAME + " ktm WHERE kt.token_id = ktm.token_id AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.CREATED_BY + "' AND md_value = ? )"
+ " ORDER BY kt.issue_time";
private static final String GET_DISABLED_SSO_COOKIE_TOKEN_IDS = "SELECT kt.token_id from knox_tokens kt, knox_token_metadata meta "
+ "WHERE kt.token_id = meta.token_id AND meta.md_name = 'knoxSSOCookie' AND meta.md_value = 'true' "
+ "AND meta.token_id IN (SELECT token_id FROM knox_token_metadata WHERE md_name = 'enabled' AND md_value = 'false')";

private final DataSource dataSource;

Expand Down Expand Up @@ -177,6 +180,18 @@ TokenMetadata getTokenMetadata(String tokenId) throws SQLException {
}
}

Set<String> getDisabledKnoxSsoCookies() throws SQLException {
final Set<String> disabledKnoxSsoCookies = new HashSet<>();
try (Connection connection = dataSource.getConnection(); PreparedStatement getExpiredTokenIdsStatement = connection.prepareStatement(GET_DISABLED_SSO_COOKIE_TOKEN_IDS)) {
try (ResultSet rs = getExpiredTokenIdsStatement.executeQuery()) {
while(rs.next()) {
disabledKnoxSsoCookies.add(rs.getString(1));
}
return disabledKnoxSsoCookies;
}
}
}

private static String decodeMetadata(String metadataName, String metadataValue) {
return metadataName.equals(TokenMetadata.PASSCODE) ? new String(Base64.decodeBase64(metadataValue.getBytes(UTF_8)), UTF_8) : metadataValue;
}
Expand Down
Loading

0 comments on commit 800bb03

Please sign in to comment.