HttpServletRequest.logout()

This commit is contained in:
Bill Burke 2014-07-03 14:08:19 -04:00
parent 4593bff76f
commit e99a675c50
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.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();

View file

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

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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);

View file

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

View file

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

View file

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

View file

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