diff --git a/examples/demo-template/product-app/src/main/webapp/products/servlet-logout.jsp b/examples/demo-template/product-app/src/main/webapp/products/servlet-logout.jsp
new file mode 100755
index 0000000000..0813b275ec
--- /dev/null
+++ b/examples/demo-template/product-app/src/main/webapp/products/servlet-logout.jsp
@@ -0,0 +1,16 @@
+<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
+ pageEncoding="ISO-8859-1"%>
+<%@ page import="org.keycloak.example.oauth.ProductDatabaseClient" %>
+<%@ page import="org.keycloak.util.KeycloakUriBuilder" %>
+<%@ page import="org.keycloak.ServiceUrlConstants" %>
+
+
+ Servlet Logout
+
+
+Performs a servlet logout
+ <%
+ request.logout();
+%>
+
+
\ No newline at end of file
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
index c13b1de59b..ac08c808d2 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
@@ -5,6 +5,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
@@ -18,6 +19,7 @@ import org.keycloak.util.StreamUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -46,6 +48,21 @@ public class ServerRequest {
}
}
+ public static void invokeLogout(KeycloakDeployment deployment, String sessionId) throws IOException, HttpFailure {
+ URI uri = deployment.getLogoutUrl().clone().queryParam("session_state", sessionId).build();
+ HttpGet logout = new HttpGet(uri);
+ HttpResponse response = deployment.getClient().execute(logout);
+ int status = response.getStatusLine().getStatusCode();
+ HttpEntity entity = response.getEntity();
+ if (status != 200) {
+ error(status, entity);
+ }
+ if (entity == null) {
+ return;
+ }
+ entity.getContent().close();
+ }
+
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri) throws HttpFailure, IOException {
String codeUrl = deployment.getCodeUrl();
String client_id = deployment.getResourceName();
diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java
index 9a5ea4eb27..4be97da6d5 100755
--- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java
+++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java
@@ -54,6 +54,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal skp, RefreshableKeycloakSecurityContext securityContext) {
+ request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
Set roles = getRolesFromToken(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
Session session = request.getSessionInternal(true);
diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
index d9b6911d83..52945cb7a3 100755
--- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
+++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
@@ -5,6 +5,7 @@ import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Session;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
@@ -21,6 +22,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.ServerRequest;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@@ -54,6 +56,24 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
cache = false;
}
+ @Override
+ public void logout(Request request) throws ServletException {
+ KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
+ if (ksc != null) {
+ request.removeAttribute(KeycloakSecurityContext.class.getName());
+ Session session = request.getSessionInternal(false);
+ if (session != null) {
+ session.removeNote(KeycloakSecurityContext.class.getName());
+ try {
+ ServerRequest.invokeLogout(deploymentContext.getDeployment(), ksc.getToken().getSessionState());
+ } catch (Exception e) {
+ log.error("failed to invoke remote logout", e);
+ }
+ }
+ }
+ super.logout(request);
+ }
+
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType() == Lifecycle.AFTER_START_EVENT) init();
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
index d43adbfcd3..45522fd035 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
@@ -16,12 +16,23 @@
*/
package org.keycloak.adapters.undertow;
+import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext;
+import io.undertow.security.api.SecurityNotification;
import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.Session;
import io.undertow.servlet.api.ConfidentialPortManager;
+import io.undertow.servlet.handlers.ServletRequestContext;
+import io.undertow.util.Sessions;
+import org.jboss.logging.Logger;
+import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RequestAuthenticator;
+import org.keycloak.adapters.ServerRequest;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
/**
* @author Bill Burke
@@ -29,13 +40,13 @@ import org.keycloak.adapters.RequestAuthenticator;
* @version $Revision: 1 $
*/
public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
+ private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class);
- protected AdapterDeploymentContext deploymentContext;
protected UndertowUserSessionManagement userSessionManagement;
protected ConfidentialPortManager portManager;
public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, ConfidentialPortManager portManager) {
- this.deploymentContext = deploymentContext;
+ super(deploymentContext);
this.userSessionManagement = userSessionManagement;
this.portManager = portManager;
}
@@ -50,9 +61,40 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
RequestAuthenticator authenticator = createRequestAuthenticator(deployment, exchange, securityContext, facade);
- return super.keycloakAuthenticate(exchange, authenticator);
+ return keycloakAuthenticate(exchange, securityContext, authenticator);
}
+ @Override
+ protected void registerNotifications(SecurityContext securityContext) {
+
+ final NotificationReceiver logoutReceiver = new NotificationReceiver() {
+ @Override
+ public void handleNotification(SecurityNotification notification) {
+ if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
+ final ServletRequestContext servletRequestContext = notification.getExchange().getAttachment(ServletRequestContext.ATTACHMENT_KEY);
+ HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
+ req.removeAttribute(KeycloakUndertowAccount.class.getName());
+ req.removeAttribute(KeycloakSecurityContext.class.getName());
+ HttpSession session = req.getSession(false);
+ if (session == null) return;
+ KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
+ if (account == null) return;
+ session.removeAttribute(KeycloakSecurityContext.class.getName());
+ session.removeAttribute(KeycloakUndertowAccount.class.getName());
+ String sessionId = account.getKeycloakSecurityContext().getToken().getSessionState();
+ try {
+ ServerRequest.invokeLogout(deploymentContext.getDeployment(), sessionId);
+ } catch (Exception e) {
+ log.error("failed to invoke remote logout", e);
+ }
+ }
+ };
+
+ securityContext.registerNotificationReceiver(logoutReceiver);
+ }
+
+
+
protected RequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
int confidentialPort = getConfidentilPort(exchange);
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java
old mode 100644
new mode 100755
index b650f6dcf0..8e55e5a22d
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java
@@ -17,12 +17,22 @@
package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticationMechanism;
+import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext;
+import io.undertow.security.api.SecurityNotification;
import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.Session;
import io.undertow.util.AttachmentKey;
+import io.undertow.util.Sessions;
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.RequestAuthenticator;
+import org.keycloak.adapters.ServerRequest;
+
+import java.io.IOException;
+
import static org.keycloak.adapters.undertow.ServletKeycloakAuthMech.KEYCLOAK_CHALLENGE_ATTACHMENT_KEY;
/**
@@ -31,7 +41,13 @@ import static org.keycloak.adapters.undertow.ServletKeycloakAuthMech.KEYCLOAK_CH
* @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
*/
public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism {
+ private static final Logger log = Logger.getLogger(UndertowKeycloakAuthMech.class);
public static final AttachmentKey KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
+ protected AdapterDeploymentContext deploymentContext;
+
+ public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext) {
+ this.deploymentContext = deploymentContext;
+ }
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
@@ -45,12 +61,36 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis
return new ChallengeResult(false);
}
+ protected void registerNotifications(SecurityContext securityContext) {
+
+ final NotificationReceiver logoutReceiver = new NotificationReceiver() {
+ @Override
+ public void handleNotification(SecurityNotification notification) {
+ if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
+ Session session = Sessions.getSession(notification.getExchange());
+ if (session == null) return;
+ KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
+ if (account == null) return;
+ session.removeAttribute(KeycloakUndertowAccount.class.getName());
+ String sessionId = account.getKeycloakSecurityContext().getToken().getSessionState();
+ try {
+ ServerRequest.invokeLogout(deploymentContext.getDeployment(), sessionId);
+ } catch (Exception e) {
+ log.error("failed to invoke remote logout", e);
+ }
+ }
+ };
+
+ securityContext.registerNotificationReceiver(logoutReceiver);
+ }
+
/**
* Call this inside your authenticate method.
*/
- protected AuthenticationMechanismOutcome keycloakAuthenticate(HttpServerExchange exchange, RequestAuthenticator authenticator) {
+ protected AuthenticationMechanismOutcome keycloakAuthenticate(HttpServerExchange exchange, SecurityContext securityContext, RequestAuthenticator authenticator) {
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
+ registerNotifications(securityContext);
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
AuthChallenge challenge = authenticator.getChallenge();
diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json
index b51cee6361..baf39769b2 100755
--- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json
@@ -1,58 +1,58 @@
-{
- "admin": {
- "realm": "master"
- },
-
- "audit": {
- "provider": "${keycloak.audit.provider,keycloak.model.provider:jpa}",
- "mongo": {
- "host": "${keycloak.audit.mongo.host:127.0.0.1}",
- "port": "${keycloak.audit.mongo.port:27017}",
- "db": "${keycloak.audit.mongo.db:keycloak-audit}",
- "clearOnStartup": "${keycloak.audit.mongo.clearOnStartup:false}"
- }
- },
-
- "model": {
- "provider": "${keycloak.model.provider:jpa}",
- "mongo": {
- "host": "${keycloak.model.mongo.host:127.0.0.1}",
- "port": "${keycloak.model.mongo.port:27017}",
- "db": "${keycloak.model.mongo.db:keycloak}",
- "clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}"
- }
- },
-
- "modelCache": {
- "provider": "${keycloak.model.cache.provider:}"
- },
-
- "timer": {
- "provider": "basic"
- },
-
- "theme": {
- "default": "keycloak",
- "staticMaxAge": 2592000,
- "cacheTemplates": "${keycloak.theme.cacheTemplates:true}",
- "folder": {
- "dir": "${keycloak.theme.dir}"
- }
- },
-
- "login": {
- "provider": "freemarker"
- },
-
- "account": {
- "provider": "freemarker"
- },
-
- "email": {
- "provider": "freemarker"
- },
-
- "scheduled": {
- "interval": 900
- }
+{
+ "admin": {
+ "realm": "master"
+ },
+
+ "audit": {
+ "provider": "${keycloak.audit.provider,keycloak.model.provider:jpa}",
+ "mongo": {
+ "host": "${keycloak.audit.mongo.host:127.0.0.1}",
+ "port": "${keycloak.audit.mongo.port:27017}",
+ "db": "${keycloak.audit.mongo.db:keycloak-audit}",
+ "clearOnStartup": "${keycloak.audit.mongo.clearOnStartup:false}"
+ }
+ },
+
+ "model": {
+ "provider": "${keycloak.model.provider:jpa}",
+ "mongo": {
+ "host": "${keycloak.model.mongo.host:127.0.0.1}",
+ "port": "${keycloak.model.mongo.port:27017}",
+ "db": "${keycloak.model.mongo.db:keycloak}",
+ "clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}"
+ }
+ },
+
+ "modelCache": {
+ "provider": "${keycloak.model.cache.provider:}"
+ },
+
+ "timer": {
+ "provider": "basic"
+ },
+
+ "theme": {
+ "default": "keycloak",
+ "staticMaxAge": 2592000,
+ "cacheTemplates": "${keycloak.theme.cacheTemplates:true}",
+ "folder": {
+ "dir": "${keycloak.theme.dir}"
+ }
+ },
+
+ "login": {
+ "provider": "freemarker"
+ },
+
+ "account": {
+ "provider": "freemarker"
+ },
+
+ "email": {
+ "provider": "freemarker"
+ },
+
+ "scheduled": {
+ "interval": 900
+ }
}
\ No newline at end of file
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index b063436777..1cc65cafc1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -174,6 +174,47 @@ public class AdapterTest {
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ }
+
+ @Test
+ public void testServletRequestLogout() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
+
+ // back
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+ // test logout
+
+ driver.navigate().to("http://localhost:8081/customer-portal/logout");
+
+
+
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java
index 9cd334831e..e77d0f871d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java
@@ -24,6 +24,14 @@ public class CustomerServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ PrintWriter pw = resp.getWriter();
+ if (req.getRequestURI().toString().endsWith("logout")) {
+ resp.setStatus(200);
+ pw.println("ok");
+ pw.flush();
+ req.logout();
+ return;
+ }
KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
Client client = ClientBuilder.newClient();
@@ -36,7 +44,6 @@ public class CustomerServlet extends HttpServlet {
.header(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString())
.get(String.class);
resp.setContentType("text/html");
- PrintWriter pw = resp.getWriter();
pw.println(html);
pw.flush();
} finally {