KEYCLOAK-7207 Check session expiration for SAML session
This commit is contained in:
parent
bf33cb0cf9
commit
4b18c6a117
19 changed files with 440 additions and 44 deletions
|
@ -19,6 +19,7 @@ package org.keycloak.adapters.saml;
|
|||
|
||||
import org.keycloak.adapters.spi.KeycloakAccount;
|
||||
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -30,14 +31,16 @@ public class SamlSession implements Serializable, KeycloakAccount {
|
|||
private SamlPrincipal principal;
|
||||
private Set<String> roles;
|
||||
private String sessionIndex;
|
||||
private XMLGregorianCalendar sessionNotOnOrAfter;
|
||||
|
||||
public SamlSession() {
|
||||
}
|
||||
|
||||
public SamlSession(SamlPrincipal principal, Set<String> roles, String sessionIndex) {
|
||||
public SamlSession(SamlPrincipal principal, Set<String> roles, String sessionIndex, XMLGregorianCalendar sessionNotOnOrAfter) {
|
||||
this.principal = principal;
|
||||
this.roles = roles;
|
||||
this.sessionIndex = sessionIndex;
|
||||
this.sessionNotOnOrAfter = sessionNotOnOrAfter;
|
||||
}
|
||||
|
||||
public SamlPrincipal getPrincipal() {
|
||||
|
@ -52,6 +55,10 @@ public class SamlSession implements Serializable, KeycloakAccount {
|
|||
return sessionIndex;
|
||||
}
|
||||
|
||||
public XMLGregorianCalendar getSessionNotOnOrAfter() {
|
||||
return sessionNotOnOrAfter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other)
|
||||
|
|
|
@ -17,13 +17,17 @@
|
|||
|
||||
package org.keycloak.adapters.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.xml.datatype.DatatypeConstants;
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
@ -31,6 +35,9 @@ import java.io.IOException;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SamlUtil {
|
||||
|
||||
protected static Logger log = Logger.getLogger(SamlUtil.class);
|
||||
|
||||
public static void sendSaml(boolean asRequest, HttpFacade httpFacade, String actionUrl,
|
||||
BaseSAML2BindingBuilder binding, Document document,
|
||||
SamlDeployment.Binding samlBinding) throws ProcessingException, ConfigurationException, IOException {
|
||||
|
@ -87,4 +94,31 @@ public class SamlUtil {
|
|||
redirectTo = baseUri + "/" + redirectTo;
|
||||
return redirectTo;
|
||||
}
|
||||
|
||||
public static SamlSession validateSamlSession(Object potentialSamlSession, SamlDeployment deployment) {
|
||||
if (potentialSamlSession == null) {
|
||||
log.debug("SamlSession was not found in the session");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(potentialSamlSession instanceof SamlSession)) {
|
||||
log.debug("Provided samlSession was not SamlSession type");
|
||||
return null;
|
||||
}
|
||||
|
||||
SamlSession samlSession = (SamlSession) potentialSamlSession;
|
||||
|
||||
XMLGregorianCalendar sessionNotOnOrAfter = samlSession.getSessionNotOnOrAfter();
|
||||
if (sessionNotOnOrAfter != null) {
|
||||
XMLGregorianCalendar now = XMLTimeUtil.getIssueInstant();
|
||||
|
||||
XMLTimeUtil.add(sessionNotOnOrAfter, deployment.getIDP().getAllowedClockSkew()); // add clockSkew
|
||||
|
||||
if (now.compare(sessionNotOnOrAfter) != DatatypeConstants.LESSER) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return samlSession;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ import java.security.Signature;
|
|||
import java.security.SignatureException;
|
||||
import java.util.*;
|
||||
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
|
@ -488,9 +489,9 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
|||
URI nameFormat = subjectNameID == null ? null : subjectNameID.getFormat();
|
||||
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
|
||||
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
|
||||
String index = authn == null ? null : authn.getSessionIndex();
|
||||
final String sessionIndex = index;
|
||||
SamlSession account = new SamlSession(principal, roles, sessionIndex);
|
||||
final String sessionIndex = authn == null ? null : authn.getSessionIndex();
|
||||
final XMLGregorianCalendar sessionNotOnOrAfter = authn == null ? null : authn.getSessionNotOnOrAfter();
|
||||
SamlSession account = new SamlSession(principal, roles, sessionIndex, sessionNotOnOrAfter);
|
||||
sessionStore.saveAccount(account);
|
||||
onCreateSession.onSessionCreated(account);
|
||||
|
||||
|
|
|
@ -132,14 +132,12 @@ public class JettySamlSessionStore implements SamlSessionStore {
|
|||
@Override
|
||||
public boolean isLoggedIn() {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) return false;
|
||||
if (session == null) {
|
||||
log.debug("session was null, returning null");
|
||||
log.debug("session was null, returning false");
|
||||
return false;
|
||||
}
|
||||
SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
|
||||
SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttribute(SamlSession.class.getName()), deployment);
|
||||
if (samlSession == null) {
|
||||
log.debug("SamlSession was not in session, returning null");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.adapters.saml.servlet;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.saml.SamlDeployment;
|
||||
import org.keycloak.adapters.saml.SamlSession;
|
||||
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||
import org.keycloak.adapters.saml.SamlUtil;
|
||||
|
@ -41,10 +42,12 @@ import java.util.Set;
|
|||
public class FilterSamlSessionStore extends FilterSessionStore implements SamlSessionStore {
|
||||
protected static Logger log = Logger.getLogger(SamlSessionStore.class);
|
||||
protected final SessionIdMapper idMapper;
|
||||
private final SamlDeployment deployment;
|
||||
|
||||
public FilterSamlSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, SessionIdMapper idMapper) {
|
||||
public FilterSamlSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, SessionIdMapper idMapper, SamlDeployment deployment) {
|
||||
super(request, facade, maxBuffer);
|
||||
this.idMapper = idMapper;
|
||||
this.deployment = deployment;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -118,12 +121,11 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe
|
|||
@Override
|
||||
public boolean isLoggedIn() {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) return false;
|
||||
if (session == null) {
|
||||
log.debug("session was null, returning null");
|
||||
log.debug("session was null, returning false");
|
||||
return false;
|
||||
}
|
||||
final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
|
||||
final SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttribute(SamlSession.class.getName()), deployment);
|
||||
if (samlSession == null) {
|
||||
log.debug("SamlSession was not in session, returning null");
|
||||
return false;
|
||||
|
|
|
@ -135,7 +135,7 @@ public class SamlFilter implements Filter {
|
|||
log.fine("deployment not configured");
|
||||
return;
|
||||
}
|
||||
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
|
||||
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper, deployment);
|
||||
boolean isEndpoint = request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml");
|
||||
SamlAuthenticator authenticator;
|
||||
if (isEndpoint) {
|
||||
|
|
|
@ -152,9 +152,8 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
|
|||
log.debug("session was null, returning null");
|
||||
return false;
|
||||
}
|
||||
final SamlSession samlSession = (SamlSession)session.getSession().getAttribute(SamlSession.class.getName());
|
||||
final SamlSession samlSession = SamlUtil.validateSamlSession(session.getSession().getAttribute(SamlSession.class.getName()), deployment);
|
||||
if (samlSession == null) {
|
||||
log.debug("SamlSession was not in session, returning null");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,10 +36,13 @@ import org.keycloak.adapters.undertow.SavedRequest;
|
|||
import org.keycloak.adapters.undertow.ServletHttpFacade;
|
||||
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.xml.datatype.DatatypeConstants;
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import java.security.Principal;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -162,9 +165,8 @@ public class ServletSamlSessionStore implements SamlSessionStore {
|
|||
return false;
|
||||
}
|
||||
|
||||
final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
|
||||
final SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttribute(SamlSession.class.getName()), deployment);
|
||||
if (samlSession == null) {
|
||||
log.debug("SamlSession was not found in the session");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -192,7 +194,6 @@ public class ServletSamlSessionStore implements SamlSessionStore {
|
|||
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
|
||||
String sessionId = changeSessionId(session);
|
||||
idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
|
||||
|
||||
}
|
||||
|
||||
protected String changeSessionId(HttpSession session) {
|
||||
|
|
|
@ -31,9 +31,13 @@ import org.keycloak.adapters.saml.SamlUtil;
|
|||
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||
import org.keycloak.adapters.spi.SessionIdMapperUpdater;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.wildfly.security.http.HttpScope;
|
||||
import org.wildfly.security.http.Scope;
|
||||
|
||||
import javax.xml.datatype.DatatypeConstants;
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -150,9 +154,8 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
|
|||
return false;
|
||||
}
|
||||
|
||||
final SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName());
|
||||
final SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttachment(SamlSession.class.getName()), deployment);
|
||||
if (samlSession == null) {
|
||||
log.debug("SamlSession was not in session, returning null");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,11 +21,13 @@ package org.keycloak.testsuite.adapter.servlet;
|
|||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.adapters.saml.SamlAuthenticationError;
|
||||
import org.keycloak.adapters.saml.SamlPrincipal;
|
||||
import org.keycloak.adapters.saml.SamlSession;
|
||||
import org.keycloak.adapters.spi.AuthenticationError;
|
||||
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
|
@ -35,6 +37,7 @@ import javax.ws.rs.core.Context;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
|
@ -149,7 +152,6 @@ public class SendUsernameServlet {
|
|||
return "These roles will be checked: " + checkRolesList.toString();
|
||||
}
|
||||
|
||||
|
||||
private boolean checkRoles() {
|
||||
for (String role : checkRolesList) {
|
||||
System.out.println("In checkRoles() checking role " + role + " for user " + httpServletRequest.getUserPrincipal().getName());
|
||||
|
@ -175,7 +177,29 @@ public class SendUsernameServlet {
|
|||
|
||||
sentPrincipal = principal;
|
||||
|
||||
return output + principal.getName();
|
||||
output += principal.getName() + "\n";
|
||||
output += getSessionInfo() + "\n";
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private String getSessionInfo() {
|
||||
HttpSession session = httpServletRequest.getSession(false);
|
||||
|
||||
if (session != null) {
|
||||
final SamlSession samlSession = (SamlSession) httpServletRequest.getSession(false).getAttribute(SamlSession.class.getName());
|
||||
|
||||
if (samlSession != null) {
|
||||
String output = "Session ID: " + samlSession.getSessionIndex() + "\n";
|
||||
XMLGregorianCalendar sessionNotOnOrAfter = samlSession.getSessionNotOnOrAfter();
|
||||
output += "SessionNotOnOrAfter: " + (sessionNotOnOrAfter == null ? "null" : sessionNotOnOrAfter.toString());
|
||||
return output;
|
||||
}
|
||||
|
||||
return "SamlSession doesn't exists";
|
||||
}
|
||||
|
||||
return "Session doesn't exists";
|
||||
}
|
||||
|
||||
private String getErrorOutput(Integer statusCode) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
|
@ -142,6 +143,13 @@ public class Matchers {
|
|||
new SamlLogoutRequestTypeMatcher(URI.create(destination))
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Matches when the type of a SAML object is instance of {@link AuthnRequestType}.
|
||||
* @return
|
||||
*/
|
||||
public static <T> Matcher<SAML2Object> isSamlAuthnRequest() {
|
||||
return instanceOf(AuthnRequestType.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches when the SAML status of a {@link StatusResponseType} instance is equal to the given code.
|
||||
|
|
|
@ -122,6 +122,14 @@ public class SamlClientBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public SamlClientBuilder assertResponse(Consumer<HttpResponse> consumer) {
|
||||
steps.add((client, currentURI, currentResponse, context) -> {
|
||||
consumer.accept(currentResponse);
|
||||
return null;
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* When executing the {@link HttpUriRequest} obtained from the previous step,
|
||||
* do not to follow HTTP redirects but pass the first response immediately
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.jboss.shrinkwrap.api.asset.UrlAsset;
|
|||
|
||||
import org.junit.Assert;
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||
|
||||
public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
|
||||
|
||||
|
@ -211,7 +212,7 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
|
|||
.build().toString();
|
||||
|
||||
DroneUtils.getCurrentDriver().navigate().to(timeOffsetUri);
|
||||
WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
|
||||
waitForPageToLoad();
|
||||
String pageSource = DroneUtils.getCurrentDriver().getPageSource();
|
||||
System.out.println(pageSource);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
|
||||
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
|
||||
import org.keycloak.testsuite.utils.io.IOUtil;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.SAMLSERVLETDEMO;
|
||||
|
||||
public abstract class AbstractSAMLServletAdapterTest extends AbstractServletsAdapterTest {
|
||||
|
||||
public static final String WEB_XML_WITH_ACTION_FILTER = "web-with-action-filter.xml";
|
||||
|
||||
@Override
|
||||
public void setDefaultPageUriParameters() {
|
||||
super.setDefaultPageUriParameters();
|
||||
testRealmPage.setAuthRealm(SAMLSERVLETDEMO);
|
||||
testRealmSAMLRedirectLoginPage.setAuthRealm(SAMLSERVLETDEMO);
|
||||
testRealmSAMLPostLoginPage.setAuthRealm(SAMLSERVLETDEMO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
|
||||
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
|
||||
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/tenant1-realm.json"));
|
||||
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/tenant2-realm.json"));
|
||||
}
|
||||
|
||||
protected void setAdapterAndServerTimeOffset(int timeOffset, String... servletUris) {
|
||||
setTimeOffset(timeOffset);
|
||||
|
||||
Arrays.stream(servletUris)
|
||||
.map(url -> url += "unsecured")
|
||||
.forEach(servletUri -> {
|
||||
String url = UriBuilder.fromUri(servletUri)
|
||||
.queryParam(AdapterActionsFilter.TIME_OFFSET_PARAM, timeOffset)
|
||||
.build().toString();
|
||||
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||
HttpUriRequest request = new HttpGet(url);
|
||||
CloseableHttpResponse httpResponse = client.execute(request);
|
||||
|
||||
System.out.println(EntityUtils.toString(httpResponse.getEntity()));
|
||||
httpResponse.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot change time on url " + url, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -53,7 +53,7 @@ import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
|
|||
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY92)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY93)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY94)
|
||||
public class SAMLClockSkewAdapterTest extends AbstractServletsAdapterTest {
|
||||
public class SAMLClockSkewAdapterTest extends AbstractSAMLServletAdapterTest {
|
||||
|
||||
@Page protected SalesPostClockSkewServlet salesPostClockSkewServletPage;
|
||||
private static final String DEPLOYMENT_NAME_3_SEC = SalesPostClockSkewServlet.DEPLOYMENT_NAME + "_3Sec";
|
||||
|
@ -88,13 +88,13 @@ public class SAMLClockSkewAdapterTest extends AbstractServletsAdapterTest {
|
|||
.login().user(bburkeUser).build()
|
||||
.processSamlResponse(POST)
|
||||
.transformDocument(doc -> {
|
||||
setAdapterAndServerTimeOffset(timeOffset, salesPostClockSkewServletPage.toString() + "unsecured");
|
||||
setAdapterAndServerTimeOffset(timeOffset, salesPostClockSkewServletPage.toString());
|
||||
return doc;
|
||||
}).build().executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));
|
||||
|
||||
Assert.assertThat(resultPage, matcher);
|
||||
} finally {
|
||||
setAdapterAndServerTimeOffset(0);
|
||||
setAdapterAndServerTimeOffset(0, salesPostClockSkewServletPage.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import org.junit.Assume;
|
||||
import org.junit.BeforeClass;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||
import org.keycloak.testsuite.utils.annotation.UseServletFilter;
|
||||
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
|
||||
@UseServletFilter(filterName = "saml-filter", filterClass = "org.keycloak.adapters.saml.servlet.SamlFilter",
|
||||
filterDependency = "org.keycloak:keycloak-saml-servlet-filter-adapter")
|
||||
public class SAMLFilterServletSessionTimeoutTest extends SAMLServletSessionTimeoutTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void enabled() {
|
||||
String appServerJavaHome = System.getProperty("app.server.java.home", "");
|
||||
Assume.assumeFalse(appServerJavaHome.contains("1.7") || appServerJavaHome.contains("ibm-java-70"));
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import static javax.ws.rs.core.Response.Status.OK;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.keycloak.OAuth2Constants.PASSWORD;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
|
||||
|
@ -50,6 +51,8 @@ import java.util.Iterator;
|
|||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
|
@ -175,7 +178,7 @@ import org.xml.sax.SAXException;
|
|||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT7)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT8)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT9)
|
||||
public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
|
||||
public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest {
|
||||
@Page
|
||||
protected BadClientSalesPostSigServlet badClientSalesPostSigServletPage;
|
||||
|
||||
|
@ -432,21 +435,6 @@ public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
|
|||
SendUsernameServlet.class, SamlMultiTenantResolver.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
|
||||
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
|
||||
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/tenant1-realm.json"));
|
||||
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/tenant2-realm.json"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultPageUriParameters() {
|
||||
super.setDefaultPageUriParameters();
|
||||
testRealmPage.setAuthRealm(SAMLSERVLETDEMO);
|
||||
testRealmSAMLRedirectLoginPage.setAuthRealm(SAMLSERVLETDEMO);
|
||||
testRealmSAMLPostLoginPage.setAuthRealm(SAMLSERVLETDEMO);
|
||||
}
|
||||
|
||||
private void assertForbidden(AbstractPage page, String expectedNotContains) {
|
||||
page.navigateTo();
|
||||
waitUntilElement(By.xpath("//body")).text().not().contains(expectedNotContains);
|
||||
|
@ -1583,7 +1571,7 @@ public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
|
|||
|
||||
.execute(r -> {
|
||||
Assert.assertThat(r, statusCodeIsHC(Response.Status.OK));
|
||||
Assert.assertThat(r, bodyHC(allOf(containsString("principal="), not(containsString("500")))));
|
||||
Assert.assertThat(r, bodyHC(containsString("principal=")));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.rotation.PublicKeyLocator;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
|
||||
import org.keycloak.testsuite.adapter.page.Employee2Servlet;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||
import org.keycloak.testsuite.util.Matchers;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.keycloak.testsuite.util.Matchers.bodyHC;
|
||||
|
||||
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT7)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT8)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT9)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY92)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY93)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY94)
|
||||
public class SAMLServletSessionTimeoutTest extends AbstractSAMLServletAdapterTest {
|
||||
|
||||
@Page
|
||||
protected Employee2Servlet employee2ServletPage;
|
||||
|
||||
@Deployment(name = Employee2Servlet.DEPLOYMENT_NAME)
|
||||
protected static WebArchive employee2() {
|
||||
return samlServletDeployment(Employee2Servlet.DEPLOYMENT_NAME, WEB_XML_WITH_ACTION_FILTER, SendUsernameServlet.class, AdapterActionsFilter.class, PublicKeyLocator.class);
|
||||
}
|
||||
|
||||
private static final int SESSION_LENGTH_IN_SECONDS = 120;
|
||||
private static final int KEYCLOAK_SESSION_TIMEOUT = 1922; /** 1800 session max + 120 {@link SessionTimeoutHelper#IDLE_TIMEOUT_WINDOW_SECONDS} */
|
||||
|
||||
private AtomicReference<String> sessionNotOnOrAfter = new AtomicReference<>();
|
||||
|
||||
private SAML2Object addSessionNotOnOrAfter(SAML2Object ob) {
|
||||
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
ResponseType resp = (ResponseType) ob;
|
||||
|
||||
Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements();
|
||||
|
||||
AuthnStatementType authType = (AuthnStatementType) statements.stream()
|
||||
.filter(statement -> statement instanceof AuthnStatementType)
|
||||
.findFirst().orElse(new AuthnStatementType(XMLTimeUtil.getIssueInstant()));
|
||||
XMLGregorianCalendar sessionTimeout = XMLTimeUtil.add(XMLTimeUtil.getIssueInstant(), SESSION_LENGTH_IN_SECONDS * 1000);
|
||||
sessionNotOnOrAfter.set(sessionTimeout.toString());
|
||||
authType.setSessionNotOnOrAfter(sessionTimeout);
|
||||
resp.getAssertions().get(0).getAssertion().addStatement(authType);
|
||||
|
||||
return ob;
|
||||
}
|
||||
|
||||
private SamlClientBuilder beginAuthenticationAndLogin() {
|
||||
return new SamlClientBuilder()
|
||||
.navigateTo(employee2ServletPage.buildUri())
|
||||
.processSamlResponse(SamlClient.Binding.POST) // Process AuthnResponse
|
||||
.build()
|
||||
|
||||
.login().user(bburkeUser)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void employee2TestSAMLRefreshingSession() {
|
||||
sessionNotOnOrAfter.set(null);
|
||||
|
||||
beginAuthenticationAndLogin()
|
||||
.processSamlResponse(SamlClient.Binding.POST) // Update response with SessionNotOnOrAfter
|
||||
.transformObject(this::addSessionNotOnOrAfter)
|
||||
.build()
|
||||
.addStep(() -> setAdapterAndServerTimeOffset(100, employee2ServletPage.toString())) // Move in time right before sessionNotOnOrAfter
|
||||
.navigateTo(employee2ServletPage.buildUri())
|
||||
.assertResponse(response -> // Check that session is still valid within sessionTimeout limit
|
||||
assertThat(response, // Cannot use matcher as sessionNotOnOrAfter variable is not set in time of creating matcher
|
||||
bodyHC(allOf(containsString("principal=bburke"),
|
||||
containsString("SessionNotOnOrAfter: " + sessionNotOnOrAfter.get())))))
|
||||
.addStep(() -> setAdapterAndServerTimeOffset(SESSION_LENGTH_IN_SECONDS, employee2ServletPage.toString())) // Move in time after sessionNotOnOrAfter
|
||||
.navigateTo(employee2ServletPage.buildUri())
|
||||
.processSamlResponse(SamlClient.Binding.POST) // AuthnRequest should be send
|
||||
.transformObject(ob -> {
|
||||
assertThat(ob, Matchers.isSamlAuthnRequest());
|
||||
return ob;
|
||||
})
|
||||
.build()
|
||||
|
||||
.followOneRedirect() // There is a redirect on Keycloak side
|
||||
.processSamlResponse(SamlClient.Binding.POST) // Process the response from keyclok, no login form should be present since session on keycloak side is still valid
|
||||
.build()
|
||||
.assertResponse(bodyHC(containsString("principal=bburke")))
|
||||
.execute();
|
||||
|
||||
setAdapterAndServerTimeOffset(0, employee2ServletPage.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void employee2TestSAMLSessionTimeoutOnBothSides() {
|
||||
sessionNotOnOrAfter.set(null);
|
||||
|
||||
beginAuthenticationAndLogin()
|
||||
.processSamlResponse(SamlClient.Binding.POST) // Update response with SessionNotOnOrAfter
|
||||
.transformObject(this::addSessionNotOnOrAfter)
|
||||
.build()
|
||||
|
||||
.navigateTo(employee2ServletPage.buildUri())
|
||||
.assertResponse(response -> // Check that session is still valid within sessionTimeout limit
|
||||
assertThat(response, // Cannot use matcher as sessionNotOnOrAfter variable is not set in time of creating matcher
|
||||
bodyHC(allOf(containsString("principal=bburke"),
|
||||
containsString("SessionNotOnOrAfter: " + sessionNotOnOrAfter.get())))))
|
||||
.addStep(() -> setAdapterAndServerTimeOffset(KEYCLOAK_SESSION_TIMEOUT, employee2ServletPage.toString())) // Move in time after sessionNotOnOrAfter and keycloak session
|
||||
.navigateTo(employee2ServletPage.buildUri())
|
||||
.processSamlResponse(SamlClient.Binding.POST) // AuthnRequest should be send
|
||||
.transformObject(ob -> {
|
||||
assertThat(ob, Matchers.isSamlAuthnRequest());
|
||||
return ob;
|
||||
})
|
||||
.build()
|
||||
|
||||
.followOneRedirect() // There is a redirect on Keycloak side
|
||||
.assertResponse(Matchers.bodyHC(containsString("form id=\"kc-form-login\"")))
|
||||
.execute();
|
||||
|
||||
setAdapterAndServerTimeOffset(0, employee2ServletPage.toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<module-name>%CONTEXT_PATH%</module-name>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>javax.ws.rs.core.Application</servlet-name>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>javax.ws.rs.core.Application</servlet-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<filter>
|
||||
<filter-name>AdapterActionsFilter</filter-name>
|
||||
<filter-class>org.keycloak.testsuite.adapter.filter.AdapterActionsFilter</filter-class>
|
||||
</filter>
|
||||
<filter-mapping>
|
||||
<filter-name>AdapterActionsFilter</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<error-page>
|
||||
<location>/error.html</location>
|
||||
</error-page>
|
||||
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Application</web-resource-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</web-resource-collection>
|
||||
<auth-constraint>
|
||||
<role-name>manager</role-name>
|
||||
</auth-constraint>
|
||||
</security-constraint>
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Unsecured-setCheckRoles</web-resource-name>
|
||||
<url-pattern>/setCheckRoles/*</url-pattern>
|
||||
</web-resource-collection>
|
||||
</security-constraint>
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Unsecured-uncheckRoles</web-resource-name>
|
||||
<url-pattern>/uncheckRoles/*</url-pattern>
|
||||
</web-resource-collection>
|
||||
</security-constraint>
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Unsecured</web-resource-name>
|
||||
<url-pattern>/unsecured/*</url-pattern>
|
||||
</web-resource-collection>
|
||||
</security-constraint>
|
||||
|
||||
<login-config>
|
||||
<auth-method>KEYCLOAK-SAML</auth-method>
|
||||
<realm-name>demo</realm-name>
|
||||
</login-config>
|
||||
|
||||
<security-role>
|
||||
<role-name>manager</role-name>
|
||||
</security-role>
|
||||
</web-app>
|
Loading…
Reference in a new issue