Skip to content

Commit

Permalink
KEYCLOAK-7094 Support redirect to external logout page
Browse files Browse the repository at this point in the history
  • Loading branch information
hmlnarik committed Jun 5, 2018
1 parent c586c63 commit 5a24139
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
Expand Down Expand Up @@ -119,16 +120,27 @@ public void logoutCurrent(Request request) {
tokenStore.logoutAccount();
}

protected void forwardToLogoutPage(Request request, HttpServletResponse response, SamlDeployment deployment) {
RequestDispatcher disp = request.getRequestDispatcher(deployment.getLogoutPage());
//make sure the login page is never cached
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:");

protected void forwardToLogoutPage(Request request, HttpServletResponse response, SamlDeployment deployment) {
final String location = deployment.getLogoutPage();

try {
disp.forward(request, response);
//make sure the login page is never cached
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");

if (location == null) {
log.warn("Logout page not set.");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (PROTOCOL_PATTERN.matcher(location).find()) {
response.sendRedirect(response.encodeRedirectURL(location));
} else {
RequestDispatcher disp = request.getRequestDispatcher(location);

disp.forward(request, response);
}
} catch (ServletException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.*;
import java.util.Map;
import java.util.regex.Pattern;

/**
* Keycloak authentication valve
Expand Down Expand Up @@ -189,16 +189,27 @@ public void invoke(Request request, Response response) throws IOException, Servl

protected abstract GenericPrincipalFactory createPrincipalFactory();
protected abstract boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException;
protected void forwardToLogoutPage(Request request, HttpServletResponse response,SamlDeployment deployment) {
RequestDispatcher disp = request.getRequestDispatcher(deployment.getLogoutPage());
//make sure the login page is never cached
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:");

protected void forwardToLogoutPage(Request request, HttpServletResponse response, SamlDeployment deployment) {
final String location = deployment.getLogoutPage();

try {
disp.forward(request.getRequest(), response);
//make sure the login page is never cached
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");

if (location == null) {
log.warn("Logout page not set.");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (PROTOCOL_PATTERN.matcher(location).find()) {
response.sendRedirect(response.encodeRedirectURL(location));
} else {
RequestDispatcher disp = request.getRequestDispatcher(location);

disp.forward(request.getRequest(), response);
}
} catch (ServletException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.undertow.UndertowHttpFacade;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
*/
public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {

private static final Logger LOG = Logger.getLogger(AbstractSamlAuthMech.class.getName());

public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
protected SamlDeploymentContext deploymentContext;
protected UndertowUserSessionManagement sessionManagement;
Expand Down Expand Up @@ -68,11 +74,22 @@ protected Integer servePage(final HttpServerExchange exchange, final String loca
return StatusCodes.TEMPORARY_REDIRECT;
}

private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:");

static void sendRedirect(final HttpServerExchange exchange, final String location) {
// TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better
// handle this.
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
if (location == null) {
LOG.log(Level.WARNING, "Logout page not set.");
exchange.setStatusCode(StatusCodes.NOT_FOUND);
exchange.endExchange();
return;
}

if (PROTOCOL_PATTERN.matcher(location).find()) {
exchange.getResponseHeaders().put(Headers.LOCATION, location);
} else {
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
}
}

protected void registerNotifications(final SecurityContext securityContext) {
Expand Down Expand Up @@ -142,7 +159,7 @@ public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange,
protected void redirectLogout(SamlDeployment deployment, HttpServerExchange exchange) {
String page = deployment.getLogoutPage();
sendRedirect(exchange, page);
exchange.setResponseCode(302);
exchange.setStatusCode(StatusCodes.FOUND);
exchange.endExchange();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ protected UndertowHttpFacade createFacade(HttpServerExchange exchange) {

@Override
protected void redirectLogout(SamlDeployment deployment, HttpServerExchange exchange) {
servePage(exchange, deployment.getLogoutPage());
exchange.getResponseHeaders().add(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
exchange.getResponseHeaders().add(Headers.PRAGMA, "no-cache");
exchange.getResponseHeaders().add(Headers.EXPIRES, "0");

super.redirectLogout(deployment, exchange);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.
*/

package org.keycloak.testsuite.adapter.page;

import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;

import java.net.URL;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;

/**
* @author mhajas
*/
public class AdapterLogoutPage extends AbstractPageWithInjectedUrl {

public static final String DEPLOYMENT_NAME = "logout";

private static final String WEB_XML =
"<web-app xmlns=\"http://java.sun.com/xml/ns/javaee\" version=\"3.0\">"
+ " <module-name>" + DEPLOYMENT_NAME + "</module-name>"
+ "</web-app>";

private static final String LOGOUT_PAGE_HTML = "<html><body>Logged out</body></html>";

public static final WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class, AdapterLogoutPage.DEPLOYMENT_NAME + ".war")
.addAsWebInfResource(new StringAsset(WEB_XML), "web.xml")
.add(new StringAsset(LOGOUT_PAGE_HTML), "/index.html");
}

@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;

@Override
public URL getInjectedUrl() {
return url;
}

@Override
public boolean isCurrent() {
return driver.getCurrentUrl().startsWith(url.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,14 @@ protected void modifyAdapterConfig(Archive<?> archive, String adapterConfigPath,
modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", "http", "https");
modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "8080", System.getProperty("auth.server.https.port"));
modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "http", "https");
modifyDocElementAttribute(doc, "SP", "logoutPage", "8081", System.getProperty("app.server.https.port"));
modifyDocElementAttribute(doc, "SP", "logoutPage", "http", "https");
} else {
modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.http.port"));
modifyDocElementAttribute(doc, "SingleSignOnService", "assertionConsumerServiceUrl", "8081", System.getProperty("app.server.http.port"));
modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", "8080", System.getProperty("auth.server.http.port"));
modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "8080", System.getProperty("auth.server.http.port"));
modifyDocElementAttribute(doc, "SP", "logoutPage", "8081", System.getProperty("app.server.http.port"));
}

archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath);
Expand Down Expand Up @@ -244,8 +247,13 @@ public void addFilterDependencies(Archive<?> archive, TestClass testClass) {
}

protected void modifyWebXml(Archive<?> archive, TestClass testClass) {
Document webXmlDoc = loadXML(
archive.get(WEBXML_PATH).getAsset().openStream());
Document webXmlDoc;
try {
webXmlDoc = loadXML(
archive.get(WEBXML_PATH).getAsset().openStream());
} catch (Exception ex) {
throw new RuntimeException("Error when processing " + archive.getName(), ex);
}
if (isTomcatAppServer(testClass.getJavaClass())) {
modifyDocElementValue(webXmlDoc, "auth-method", "KEYCLOAK", "BASIC");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

Expand Down Expand Up @@ -234,6 +236,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
@Page
protected SalesPostAutodetectServlet salesPostAutodetectServletPage;

@Page
protected AdapterLogoutPage adapterLogoutPage;

@Page
protected EcpSP ecpSPPage;

Expand Down Expand Up @@ -362,7 +367,13 @@ protected static WebArchive missingAssertionSig() {

@Deployment(name = EmployeeServlet.DEPLOYMENT_NAME)
protected static WebArchive employeeServlet() {
return samlServletDeployment(EmployeeServlet.DEPLOYMENT_NAME, "employee/WEB-INF/web.xml", SamlSPFacade.class, ServletTestUtils.class);
return samlServletDeployment(EmployeeServlet.DEPLOYMENT_NAME, "employee/WEB-INF/web.xml", SamlSPFacade.class, ServletTestUtils.class)
.add(new StringAsset("<html><body>Logged out</body></html>"), "/logout.jsp");
}

@Deployment(name = AdapterLogoutPage.DEPLOYMENT_NAME)
protected static WebArchive logoutWar() {
return AdapterLogoutPage.createDeployment();
}

@Deployment(name = SalesPostAutodetectServlet.DEPLOYMENT_NAME)
Expand Down Expand Up @@ -641,6 +652,18 @@ public void employeeSigFrontTest() {
testSuccessfulAndUnauthorizedLogin(employeeSigFrontServletPage, testRealmSAMLRedirectLoginPage);
}

@Test
public void testLogoutRedirectToExternalPage() throws Exception {
employeeServletPage.navigateTo();
assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage);
testRealmSAMLPostLoginPage.form().login("bburke", "password");
assertCurrentUrlStartsWith(employeeServletPage);
WaitUtils.waitForPageToLoad();

employeeServletPage.logout();
adapterLogoutPage.assertCurrent();
}

@Test
public void salesMetadataTest() throws Exception {
Document doc = loadXML(AbstractSAMLServletsAdapterTest.class.getResourceAsStream("/adapter-test/keycloak-saml/sp-metadata.xml"));
Expand Down Expand Up @@ -980,7 +1003,7 @@ public void testPostSimpleLoginLogoutIdpInitiatedRedirectTo() {

samlidpInitiatedLoginPage.form().login(bburkeUser);
assertCurrentUrlStartsWith(salesPost2ServletPage);
assertTrue(driver.getCurrentUrl().endsWith("/foo"));
assertThat(driver.getCurrentUrl(), endsWith("/foo"));
waitUntilElement(By.xpath("//body")).text().contains("principal=bburke");
salesPost2ServletPage.logout();
checkLoggedOut(salesPost2ServletPage, testRealmSAMLPostLoginPage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<SP entityID="http://localhost:8081/employee/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
logoutPage="http://localhost:8081/logout/index.html"
forceAuthentication="false">
<PrincipalNameMapping policy="FROM_NAME_ID"/>
<RoleIdentifiers>
Expand Down

0 comments on commit 5a24139

Please sign in to comment.