Merge pull request #495 from patriot1burke/master

HttpServletRequest.logout()
This commit is contained in:
Bill Burke 2014-07-03 14:08:43 -04:00
commit f92ad89a6c
9 changed files with 246 additions and 62 deletions

View file

@ -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" %>
<html>
<head>
<title>Servlet Logout</title>
</head>
<body bgcolor="#F5F6CE">
Performs a servlet logout
<%
request.logout();
%>
</body>
</html>

View file

@ -5,6 +5,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity; 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.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
@ -18,6 +19,7 @@ import org.keycloak.util.StreamUtil;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; 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 { public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri) throws HttpFailure, IOException {
String codeUrl = deployment.getCodeUrl(); String codeUrl = deployment.getCodeUrl();
String client_id = deployment.getResourceName(); String client_id = deployment.getResourceName();

View file

@ -54,6 +54,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
@Override @Override
protected void completeOAuthAuthentication(KeycloakPrincipal skp, RefreshableKeycloakSecurityContext securityContext) { protected void completeOAuthAuthentication(KeycloakPrincipal skp, RefreshableKeycloakSecurityContext securityContext) {
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
Set<String> roles = getRolesFromToken(securityContext); Set<String> roles = getRolesFromToken(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext); GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
Session session = request.getSessionInternal(true); Session session = request.getSessionInternal(true);

View file

@ -5,6 +5,7 @@ import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener; import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response; import org.apache.catalina.connector.Response;
@ -21,6 +22,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.ServerRequest;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -54,6 +56,24 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
cache = false; 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 @Override
public void lifecycleEvent(LifecycleEvent event) { public void lifecycleEvent(LifecycleEvent event) {
if (event.getType() == Lifecycle.AFTER_START_EVENT) init(); if (event.getType() == Lifecycle.AFTER_START_EVENT) init();

View file

@ -16,12 +16,23 @@
*/ */
package org.keycloak.adapters.undertow; package org.keycloak.adapters.undertow;
import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext; import io.undertow.security.api.SecurityContext;
import io.undertow.security.api.SecurityNotification;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.servlet.api.ConfidentialPortManager; 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.AdapterDeploymentContext;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RequestAuthenticator; import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.adapters.ServerRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -29,13 +40,13 @@ import org.keycloak.adapters.RequestAuthenticator;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech { public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class);
protected AdapterDeploymentContext deploymentContext;
protected UndertowUserSessionManagement userSessionManagement; protected UndertowUserSessionManagement userSessionManagement;
protected ConfidentialPortManager portManager; protected ConfidentialPortManager portManager;
public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, ConfidentialPortManager portManager) { public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, ConfidentialPortManager portManager) {
this.deploymentContext = deploymentContext; super(deploymentContext);
this.userSessionManagement = userSessionManagement; this.userSessionManagement = userSessionManagement;
this.portManager = portManager; this.portManager = portManager;
} }
@ -50,9 +61,40 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
RequestAuthenticator authenticator = createRequestAuthenticator(deployment, exchange, securityContext, facade); 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) { protected RequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
int confidentialPort = getConfidentilPort(exchange); int confidentialPort = getConfidentilPort(exchange);

View file

@ -17,12 +17,22 @@
package org.keycloak.adapters.undertow; package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext; import io.undertow.security.api.SecurityContext;
import io.undertow.security.api.SecurityNotification;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.util.AttachmentKey; 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.AuthChallenge;
import org.keycloak.adapters.AuthOutcome; import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.RequestAuthenticator; 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; 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. * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
*/ */
public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism { public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism {
private static final Logger log = Logger.getLogger(UndertowKeycloakAuthMech.class);
public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class); public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
protected AdapterDeploymentContext deploymentContext;
public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext) {
this.deploymentContext = deploymentContext;
}
@Override @Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
@ -45,12 +61,36 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis
return new ChallengeResult(false); 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. * 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(); AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) { if (outcome == AuthOutcome.AUTHENTICATED) {
registerNotifications(securityContext);
return AuthenticationMechanismOutcome.AUTHENTICATED; return AuthenticationMechanismOutcome.AUTHENTICATED;
} }
AuthChallenge challenge = authenticator.getChallenge(); AuthChallenge challenge = authenticator.getChallenge();

View file

@ -1,58 +1,58 @@
{ {
"admin": { "admin": {
"realm": "master" "realm": "master"
}, },
"audit": { "audit": {
"provider": "${keycloak.audit.provider,keycloak.model.provider:jpa}", "provider": "${keycloak.audit.provider,keycloak.model.provider:jpa}",
"mongo": { "mongo": {
"host": "${keycloak.audit.mongo.host:127.0.0.1}", "host": "${keycloak.audit.mongo.host:127.0.0.1}",
"port": "${keycloak.audit.mongo.port:27017}", "port": "${keycloak.audit.mongo.port:27017}",
"db": "${keycloak.audit.mongo.db:keycloak-audit}", "db": "${keycloak.audit.mongo.db:keycloak-audit}",
"clearOnStartup": "${keycloak.audit.mongo.clearOnStartup:false}" "clearOnStartup": "${keycloak.audit.mongo.clearOnStartup:false}"
} }
}, },
"model": { "model": {
"provider": "${keycloak.model.provider:jpa}", "provider": "${keycloak.model.provider:jpa}",
"mongo": { "mongo": {
"host": "${keycloak.model.mongo.host:127.0.0.1}", "host": "${keycloak.model.mongo.host:127.0.0.1}",
"port": "${keycloak.model.mongo.port:27017}", "port": "${keycloak.model.mongo.port:27017}",
"db": "${keycloak.model.mongo.db:keycloak}", "db": "${keycloak.model.mongo.db:keycloak}",
"clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}" "clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}"
} }
}, },
"modelCache": { "modelCache": {
"provider": "${keycloak.model.cache.provider:}" "provider": "${keycloak.model.cache.provider:}"
}, },
"timer": { "timer": {
"provider": "basic" "provider": "basic"
}, },
"theme": { "theme": {
"default": "keycloak", "default": "keycloak",
"staticMaxAge": 2592000, "staticMaxAge": 2592000,
"cacheTemplates": "${keycloak.theme.cacheTemplates:true}", "cacheTemplates": "${keycloak.theme.cacheTemplates:true}",
"folder": { "folder": {
"dir": "${keycloak.theme.dir}" "dir": "${keycloak.theme.dir}"
} }
}, },
"login": { "login": {
"provider": "freemarker" "provider": "freemarker"
}, },
"account": { "account": {
"provider": "freemarker" "provider": "freemarker"
}, },
"email": { "email": {
"provider": "freemarker" "provider": "freemarker"
}, },
"scheduled": { "scheduled": {
"interval": 900 "interval": 900
} }
} }

View file

@ -174,6 +174,47 @@ public class AdapterTest {
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); 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 @Test

View file

@ -24,6 +24,14 @@ public class CustomerServlet extends HttpServlet {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 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()); KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
Client client = ClientBuilder.newClient(); Client client = ClientBuilder.newClient();
@ -36,7 +44,6 @@ public class CustomerServlet extends HttpServlet {
.header(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString()) .header(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString())
.get(String.class); .get(String.class);
resp.setContentType("text/html"); resp.setContentType("text/html");
PrintWriter pw = resp.getWriter();
pw.println(html); pw.println(html);
pw.flush(); pw.flush();
} finally { } finally {