HttpServletRequest.logout()
This commit is contained in:
parent
4593bff76f
commit
e99a675c50
9 changed files with 246 additions and 62 deletions
|
@ -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>
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
42
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java
Normal file → Executable file
42
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java
Normal file → Executable 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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue