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 Sep 29, 2023
1 parent 3af43b7 commit dd218da
Show file tree
Hide file tree
Showing 23 changed files with 474 additions and 192 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.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() { }

}
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
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ public interface TokenStateServiceMessages {
@Message(level = MessageLevel.ERROR, text = "An error occurred while removing expired tokens from the database : {0}")
void errorRemovingTokensFromDatabase(String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.INFO, text = "Removing {0} disabled KnoxSSO cookie(s) from the database: {1}")
void removingDisabledKnoxSsoCookiesFromDatabase(int size, String disabledKnoxSsoCookieList);

@Message(level = MessageLevel.ERROR, text = "An error occurred while removing disabled KnoxSSO cookies from the database : {0}")
void errorRemovingDisabledKnoxSsoCookiesFromDatabase(String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.DEBUG, text = "Fetched issue time for {0} from the database : {1}")
void fetchedIssueTimeFromDatabase(String tokenId, long issueTime);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.HmacAlgorithms;
Expand Down Expand Up @@ -240,6 +241,24 @@ public void testEvictExpiredTokens() throws Exception {
assertEquals(0, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
}

@Test
public void testEvictDisabledKnoxSsoCookies() throws Exception {
truncateDatabase();
final long now = System.currentTimeMillis();
final int tokenCount = 500;
for (int i = 0; i < tokenCount; i++) {
final String tokenId = UUID.randomUUID().toString();
jdbcTokenStateService.addToken(tokenId, now, now + TimeUnit.MINUTES.toMillis(5), TimeUnit.MINUTES.toMillis(60));
final TokenMetadata tokenMetadata = new TokenMetadata("user");
tokenMetadata.setKnoxSsoCookie(i % 2 == 0); // half of them are KnoxSSO cookie
tokenMetadata.setEnabled(i % 4 == 0); // quarter of them are disabled
jdbcTokenStateService.addMetadata(tokenId, tokenMetadata);
}
assertEquals(tokenCount, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
jdbcTokenStateService.evictExpiredTokens(); //500 tokens were generated, 125 are disabled KnoxSSO cookies -> 375 should be in the DB
assertEquals(375, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
}

private long getLongTokenAttributeFromDatabase(String tokenId, String sql) throws SQLException {
try (Connection conn = getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) {
if (tokenId != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ public interface KnoxSSOMessages {
@Message( level = MessageLevel.ERROR, text = "The original URL: {0} for redirecting back after authentication is " +
"not valid according to the configured whitelist: {1}. See documentation for KnoxSSO Whitelisting.")
void whiteListMatchFail(String original, String whitelist);

@Message( level = MessageLevel.INFO, text = "Knox Token service ({0}) stored state for token {1} ({2})")
void storedToken(String topologyName, String tokenDisplayText, String tokenId);
}
Loading

0 comments on commit dd218da

Please sign in to comment.