From bfcf5953467a1692e7d2f6f5bb3dfe2c0f0f9655 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 28 Apr 2015 18:52:30 -0400 Subject: [PATCH 1/2] backchannel broker logout --- .../provider/AbstractIdentityProvider.java | 5 + .../broker/provider/IdentityProvider.java | 2 + .../broker/provider}/util/SimpleHttp.java | 109 ++++++++++++++++-- .../oidc/AbstractOAuth2IdentityProvider.java | 2 +- .../oidc/KeycloakOIDCIdentityProvider.java | 4 +- .../broker/oidc/OIDCIdentityProvider.java | 62 +++++++--- .../oidc/OIDCIdentityProviderConfig.java | 8 ++ .../oidc/OIDCIdentityProviderFactory.java | 3 +- .../broker/oidc/util/JsonSimpleHttp.java | 32 +++++ .../keycloak/broker/saml/SAMLEndpoint.java | 4 +- .../broker/saml/SAMLIdentityProvider.java | 57 +++++++-- .../saml/SAMLIdentityProviderConfig.java | 9 ++ .../protocol/saml/SAML2BindingBuilder.java | 5 +- .../keycloak/protocol/saml/SamlProtocol.java | 2 +- .../keycloak/protocol/saml/SamlService.java | 108 ++++++++++------- .../keycloak/protocol/oidc/TokenManager.java | 2 +- .../oidc/endpoints/LogoutEndpoint.java | 4 +- .../managers/AuthenticationManager.java | 29 ++++- .../services/resources/AccountService.java | 4 +- .../resources/LoginActionsService.java | 2 +- .../resources/admin/RealmAdminResource.java | 2 +- .../resources/admin/UsersResource.java | 2 +- .../facebook/FacebookIdentityProvider.java | 5 +- .../social/github/GitHubIdentityProvider.java | 5 +- .../linkedin/LinkedInIdentityProvider.java | 5 +- .../StackoverflowIdentityProvider.java | 5 +- .../broker-test/test-realm-with-broker.json | 6 +- 27 files changed, 375 insertions(+), 108 deletions(-) rename broker/{oidc/src/main/java/org/keycloak/broker/oidc => core/src/main/java/org/keycloak/broker/provider}/util/SimpleHttp.java (56%) mode change 100644 => 100755 create mode 100755 broker/oidc/src/main/java/org/keycloak/broker/oidc/util/JsonSimpleHttp.java diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java index a231346522..f4ddeaedec 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java @@ -63,6 +63,11 @@ public abstract class AbstractIdentityProvider return null; } + @Override + public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) { + + } + @Override public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) { diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index 01d5455b02..2c503bbf05 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -76,6 +76,8 @@ public interface IdentityProvider extends Provi */ Response retrieveToken(FederatedIdentityModel identity); + void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm); + /** * Called when a Keycloak application initiates a logout through the browser. This is expected to do a logout * with the IDP diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java b/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java old mode 100644 new mode 100755 similarity index 56% rename from broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java rename to broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java index c9050b8296..5fa23b01a9 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java @@ -1,7 +1,4 @@ -package org.keycloak.broker.oidc.util; - -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.ObjectMapper; +package org.keycloak.broker.provider.util; import java.io.IOException; import java.io.InputStream; @@ -21,14 +18,13 @@ import java.util.zip.GZIPInputStream; */ public class SimpleHttp { - private static ObjectMapper mapper = new ObjectMapper(); private String url; private String method; private Map headers; private Map params; - private SimpleHttp(String url, String method) { + protected SimpleHttp(String url, String method) { this.url = url; this.method = method; } @@ -57,9 +53,6 @@ public class SimpleHttp { return this; } - public JsonNode asJson() throws IOException { - return mapper.readTree(asString()); - } public String asString() throws IOException { boolean get = method.equals("GET"); @@ -126,11 +119,105 @@ public class SimpleHttp { return toString(is); } finally { if (os != null) { - os.close(); + try { + os.close(); + } catch (IOException e) { + } } if (is != null) { - is.close(); + try { + is.close(); + } catch (IOException e) { + } + } + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception e) { + } + } + } + } + + public int asStatus() throws IOException { + boolean get = method.equals("GET"); + boolean post = method.equals("POST"); + + StringBuilder sb = new StringBuilder(); + if (get) { + sb.append(url); + } + + if (params != null) { + boolean f = true; + for (Map.Entry p : params.entrySet()) { + if (f) { + f = false; + if (get) { + sb.append("?"); + } + } else { + sb.append("&"); + } + sb.append(URLEncoder.encode(p.getKey(), "UTF-8")); + sb.append("="); + sb.append(URLEncoder.encode(p.getValue(), "UTF-8")); + } + } + + if (get) { + url = sb.toString(); + } + + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + OutputStream os = null; + InputStream is = null; + + try { + connection.setRequestMethod(method); + + if (headers != null) { + for (Map.Entry h : headers.entrySet()) { + connection.setRequestProperty(h.getKey(), h.getValue()); + } + } + + if (post) { + String data = sb.toString(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestProperty("Content-Length", String.valueOf(data.length())); + + os = connection.getOutputStream(); + os.write(data.getBytes()); + } else { + connection.setDoOutput(false); + } + + is = connection.getInputStream(); + return connection.getResponseCode(); + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException e) { + } + } + + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception e) { + } } } } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 95e1f6a146..b80949af2f 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -22,7 +22,7 @@ import org.codehaus.jackson.map.ObjectMapper; import org.jboss.logging.Logger; import org.keycloak.ClientConnection; import org.keycloak.OAuth2Constants; -import org.keycloak.broker.oidc.util.SimpleHttp; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.BrokeredIdentityContext; diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java index 3c59a9650e..d7917654a6 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java @@ -1,6 +1,6 @@ package org.keycloak.broker.oidc; -import org.keycloak.broker.oidc.util.SimpleHttp; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.constants.AdapterConstants; import org.keycloak.events.EventBuilder; @@ -74,7 +74,7 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider { && userSession.getState() != UserSessionModel.State.LOGGING_OUT && userSession.getState() != UserSessionModel.State.LOGGED_OUT ) { - AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers); + AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false); } } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java index b840f37388..01e6c418bd 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -19,7 +19,8 @@ package org.keycloak.broker.oidc; import org.codehaus.jackson.JsonNode; import org.jboss.logging.Logger; -import org.keycloak.broker.oidc.util.SimpleHttp; +import org.keycloak.broker.oidc.util.JsonSimpleHttp; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; @@ -127,20 +128,51 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider=200 && status < 400; + if (!success) { + logger.warn("Failed backchannel broker logout to: " + url); + } + } catch (Exception e) { + logger.warn("Failed backchannel broker logout to: " + url, e); + } + } + + + @Override + public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) { + if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null; String idToken = userSession.getNote(FEDERATED_ID_TOKEN); - if (idToken != null) logoutUri.queryParam("id_token_hint", idToken); - String redirect = RealmsResource.brokerUrl(uriInfo) - .path(IdentityBrokerService.class, "getEndpoint") - .path(OIDCEndpoint.class, "logoutResponse") - .build(realm.getName(), getConfig().getAlias()).toString(); - logoutUri.queryParam("post_logout_redirect_uri", redirect); - Response response = Response.status(302).location(logoutUri.build()).build(); - return response; + if (idToken != null && getConfig().isBackchannelSupported()) { + backchannelLogout(userSession, idToken); + return null; + } else { + String sessionId = userSession.getId(); + UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl()) + .queryParam("state", sessionId); + if (idToken != null) logoutUri.queryParam("id_token_hint", idToken); + String redirect = RealmsResource.brokerUrl(uriInfo) + .path(IdentityBrokerService.class, "getEndpoint") + .path(OIDCEndpoint.class, "logoutResponse") + .build(realm.getName(), getConfig().getAlias()).toString(); + logoutUri.queryParam("post_logout_redirect_uri", redirect); + Response response = Response.status(302).location(logoutUri.build()).build(); + return response; + } } @Override @@ -184,9 +216,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProviderBill Burke + * @version $Revision: 1 $ + */ +public class JsonSimpleHttp extends SimpleHttp { + public JsonSimpleHttp(String url, String method) { + super(url, method); + } + + public static JsonSimpleHttp doGet(String url) { + return new JsonSimpleHttp(url, "GET"); + } + + public static JsonSimpleHttp doPost(String url) { + return new JsonSimpleHttp(url, "POST"); + } + + private static ObjectMapper mapper = new ObjectMapper(); + + public static JsonNode asJson(SimpleHttp request) throws IOException { + return mapper.readTree(request.asString()); + } + +} diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java index b3efc97561..d2d32ebefe 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -218,7 +218,7 @@ public class SAMLEndpoint { List userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId); for (UserSessionModel userSession : userSessions) { try { - AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers); + AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false); } catch (Exception e) { logger.warn("failed to do backchannel logout for userSession", e); } @@ -230,7 +230,7 @@ public class SAMLEndpoint { UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId); if (userSession != null) { try { - AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers); + AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false); } catch (Exception e) { logger.warn("failed to do backchannel logout for userSession", e); } diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index 98fd6efb3f..4517a94304 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -17,10 +17,12 @@ */ package org.keycloak.broker.saml; +import org.jboss.logging.Logger; import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.NameIDType; @@ -34,12 +36,17 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.saml.SAML2AuthnRequestBuilder; import org.keycloak.protocol.saml.SAML2LogoutRequestBuilder; import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder; +import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; +import java.io.IOException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; @@ -48,6 +55,7 @@ import java.security.PublicKey; * @author Pedro Igor */ public class SAMLIdentityProvider extends AbstractIdentityProvider { + protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class); public SAMLIdentityProvider(SAMLIdentityProviderConfig config) { super(config); } @@ -141,25 +149,56 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider=200 && status < 400; + if (!success) { + logger.warn("Failed saml backchannel broker logout to: " + singleLogoutServiceUrl); + } + } catch (Exception e) { + logger.warn("Failed saml backchannel broker logout to: " + singleLogoutServiceUrl, e); + } + } + + @Override + public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) { + String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl(); + if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("")) return null; + + if (getConfig().isBackchannelSupported()) { + backchannelLogout(userSession, uriInfo, realm); + return null; + } else { + try { + SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl); + return logoutBuilder.postBinding().request(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + protected SAML2LogoutRequestBuilder buildLogoutRequest(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm, String singleLogoutServiceUrl) { SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() .assertionExpiration(realm.getAccessCodeLifespan()) .issuer(getEntityId(uriInfo, realm)) .sessionIndex(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX)) .userPrincipal(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT), userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT)) - .destination(getConfig().getSingleLogoutServiceUrl()); + .destination(singleLogoutServiceUrl) + .relayState(userSession.getId()); if (getConfig().isWantAuthnRequestsSigned()) { logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) .signDocument(); } - try { - return logoutBuilder.relayState(userSession.getId()).postBinding().request(); - } catch (Exception e) { - throw new RuntimeException(e); - } - + return logoutBuilder; } @Override diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java index 10ef46441b..ad11be3e41 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -110,4 +110,13 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { public void setPostBindingResponse(boolean postBindingResponse) { getConfig().put("postBindingResponse", String.valueOf(postBindingResponse)); } + + public boolean isBackchannelSupported() { + return Boolean.valueOf(getConfig().get("backchannelSupported")); + } + + public void setBackchannelSupported(boolean backchannel) { + getConfig().put("backchannelSupported", String.valueOf(backchannel)); + } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java index 8143d1540e..7fe2fb3678 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java @@ -39,6 +39,7 @@ import static org.keycloak.saml.common.util.StringUtil.isNotNull; */ public class SAML2BindingBuilder { protected static final Logger logger = Logger.getLogger(SAML2BindingBuilder.class); + public static final String RELAY_STATE = "RelayState"; protected KeyPair signingKeyPair; protected X509Certificate signingCertificate; @@ -328,7 +329,7 @@ public class SAML2BindingBuilder { builder.append(""); if (isNotNull(relayState)) { - builder.append(""); + builder.append(""); } builder.append("