KEYCLOAK-7207 Check session expiration for SAML session

This commit is contained in:
mhajas 2019-06-27 12:56:12 +02:00 committed by Hynek Mlnařík
parent bf33cb0cf9
commit 4b18c6a117
19 changed files with 440 additions and 44 deletions

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

View file

@ -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) {

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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) {

View file

@ -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.

View file

@ -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

View file

@ -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);
}

View file

@ -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);
}
});
}
}

View file

@ -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());
}
}

View file

@ -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"));
}
}

View file

@ -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=")));
});
}

View file

@ -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());
}
}

View file

@ -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>