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 {