Merge pull request #1192 from patriot1burke/master

broker backchannel logout
This commit is contained in:
Bill Burke 2015-04-28 19:18:45 -04:00
commit 08e1f455e2
29 changed files with 391 additions and 110 deletions

View file

@ -63,6 +63,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
return null;
}
@Override
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
}
@Override
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {

View file

@ -76,6 +76,8 @@ public interface IdentityProvider<C extends IdentityProviderModel> 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

View file

@ -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<String, String> headers;
private Map<String, String> 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<String, String> 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<String, String> 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) {
}
}
}
}

View file

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

View file

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

View file

@ -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<OIDCIde
}
@Override
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("") || !getConfig().isBackchannelSupported()) return;
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
if (idToken == null) return;
backchannelLogout(userSession, idToken);
}
protected void backchannelLogout(UserSessionModel userSession, String idToken) {
String sessionId = userSession.getId();
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
.queryParam("state", sessionId);
.queryParam("state", sessionId);
logoutUri.queryParam("id_token_hint", idToken);
String url = logoutUri.build().toString();
try {
int status = JsonSimpleHttp.doGet(url).asStatus();
boolean success = status >=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 AbstractOAuth2IdentityProvider<OIDCIde
String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
if (getConfig().getUserInfoUrl() != null && (id == null || name == null || preferredUsername == null || email == null) ) {
JsonNode userInfo = SimpleHttp.doGet(getConfig().getUserInfoUrl())
.header("Authorization", "Bearer " + accessToken)
.asJson();
SimpleHttp request = JsonSimpleHttp.doGet(getConfig().getUserInfoUrl())
.header("Authorization", "Bearer " + accessToken);
JsonNode userInfo = JsonSimpleHttp.asJson(request);
id = getJsonProperty(userInfo, "sub");
name = getJsonProperty(userInfo, "name");

View file

@ -70,6 +70,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
getConfig().put("validateSignature", String.valueOf(validateSignature));
}
public boolean isBackchannelSupported() {
return Boolean.valueOf(getConfig().get("backchannelSupported"));
}
public void setBackchannelSupported(boolean backchannel) {
getConfig().put("backchannelSupported", String.valueOf(backchannel));
}

View file

@ -17,11 +17,10 @@
*/
package org.keycloak.broker.oidc;
import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.representations.JSONWebKeySet;

View file

@ -0,0 +1,32 @@
package org.keycloak.broker.oidc.util;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.keycloak.broker.provider.util.SimpleHttp;
import java.io.IOException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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());
}
}

View file

@ -218,7 +218,7 @@ public class SAMLEndpoint {
List<UserSessionModel> 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);
}

View file

@ -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<SAMLIdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class);
public SAMLIdentityProvider(SAMLIdentityProviderConfig config) {
super(config);
}
@ -141,25 +149,56 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
}
@Override
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
if (getConfig().getSingleLogoutServiceUrl() == null || getConfig().getSingleLogoutServiceUrl().trim().equals("")) return null;
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl();
if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("") || !getConfig().isBackchannelSupported()) return;
SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl);
try {
int status = SimpleHttp.doPost(singleLogoutServiceUrl)
.param(GeneralConstants.SAML_REQUEST_KEY, logoutBuilder.postBinding().encoded())
.param(GeneralConstants.RELAY_STATE, userSession.getId()).asStatus();
boolean success = status >=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

View file

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

View file

@ -42,7 +42,7 @@
<span tooltip-placement="right" tooltip="Indicates if this provider should be tried by default for authentication even before displaying login screen" class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="enabled">Store Tokens</label>
<label class="col-sm-2 control-label" for="storeToken">Store Tokens</label>
<div class="col-sm-4">
<input ng-model="identityProvider.storeToken" id="storeToken" onoffswitch />
</div>
@ -93,6 +93,13 @@
</div>
<span tooltip-placement="right" tooltip="End session endpoint to use to logout user from external IDP." class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="backchannelSupported">Backchannel Logout</label>
<div class="col-sm-4">
<input ng-model="identityProvider.config.backchannelSupported" id="backchannelSupported" value="'true'" onoffswitchvalue />
</div>
<span tooltip-placement="right" tooltip="Does the external IDP support backchannel logout?" class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="userInfoUrl">User Info Url</label>
<div class="col-sm-4">

View file

@ -42,7 +42,7 @@
<span tooltip-placement="right" tooltip="Indicates if this provider should be tried by default for authentication even before displaying login screen" class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="enabled">Store Tokens</label>
<label class="col-sm-2 control-label" for="storeToken">Store Tokens</label>
<div class="col-sm-4">
<input ng-model="identityProvider.storeToken" id="storeToken" onoffswitch />
</div>
@ -87,6 +87,13 @@
</div>
<span tooltip-placement="right" tooltip="The Url that must be used to send logout requests." class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="backchannelSupported">Backchannel Logout</label>
<div class="col-sm-4">
<input ng-model="identityProvider.config.backchannelSupported" id="backchannelSupported" value="'true'" onoffswitchvalue />
</div>
<span tooltip-placement="right" tooltip="Does the external IDP support backchannel logout?" class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="nameIDPolicyFormat">NameID Policy Format</label>
<div class="col-sm-4">

View file

@ -39,6 +39,7 @@ import static org.keycloak.saml.common.util.StringUtil.isNotNull;
*/
public class SAML2BindingBuilder<T extends 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<T extends SAML2BindingBuilder> {
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + key + "\"" + " VALUE=\"" + samlResponse + "\"/>");
if (isNotNull(relayState)) {
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"" + escapeAttribute(relayState) + "\"/>");
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + RELAY_STATE + "\" " + "VALUE=\"" + escapeAttribute(relayState) + "\"/>");
}
builder.append("<NOSCRIPT>");
@ -355,7 +356,7 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
.replaceQuery(null)
.queryParam(samlParameterName, base64Encoded(document));
if (relayState != null) {
builder.queryParam("RelayState", relayState);
builder.queryParam(RELAY_STATE, relayState);
}
if (sign) {

View file

@ -161,7 +161,7 @@ public class SamlProtocol implements LoginProtocol {
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client);
}
protected boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
public static boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
String note = session.getNote(SamlProtocol.SAML_LOGOUT_BINDING);
return SamlProtocol.SAML_POST_BINDING.equals(note);
}

View file

@ -6,6 +6,12 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.ClientConnection;
import org.keycloak.VerificationException;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -18,22 +24,18 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
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.ProcessingException;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.HttpAuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.ErrorPage;
import org.keycloak.util.StreamUtil;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
@ -51,6 +53,7 @@ import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
@ -122,7 +125,7 @@ public class SamlService {
protected Response handleSamlResponse(String samlResponse, String relayState) {
event.event(EventType.LOGOUT);
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
// validate destination
if (!uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
@ -162,7 +165,7 @@ public class SamlService {
SAML2Object samlObject = documentHolder.getSamlObject();
RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject;
RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.getClientByClientId(issuer);
@ -177,7 +180,7 @@ public class SamlService {
event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
}
if ((client instanceof ClientModel) && ((ClientModel)client).isBearerOnly()) {
if ((client instanceof ClientModel) && ((ClientModel) client).isBearerOnly()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.BEARER_ONLY);
@ -221,6 +224,7 @@ public class SamlService {
protected abstract void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException;
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
@ -231,7 +235,8 @@ public class SamlService {
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
String bindingType = getBindingType(requestAbstractType);
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) bindingType = SamlProtocol.SAML_POST_BINDING;
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING)))
bindingType = SamlProtocol.SAML_POST_BINDING;
String redirect = null;
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes
@ -243,7 +248,7 @@ public class SamlService {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
}
if (redirect == null && client instanceof ClientModel) {
redirect = ((ClientModel)client).getManagementUrl();
redirect = ((ClientModel) client).getManagementUrl();
}
}
@ -265,10 +270,10 @@ public class SamlService {
// Handle NameIDPolicy from SP
NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy();
if(nameIdPolicy != null && !SamlProtocol.forceNameIdFormat(client)) {
if (nameIdPolicy != null && !SamlProtocol.forceNameIdFormat(client)) {
String nameIdFormat = nameIdPolicy.getFormat().toString();
// TODO: Handle AllowCreate too, relevant for persistent NameID.
if(isSupportedNameIdFormat(nameIdFormat)) {
if (isSupportedNameIdFormat(nameIdFormat)) {
clientSession.setNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
} else {
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
@ -344,7 +349,8 @@ public class SamlService {
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
if (authResult != null) {
String logoutBinding = getBindingType();
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) logoutBinding = SamlProtocol.SAML_POST_BINDING;
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING)))
logoutBinding = SamlProtocol.SAML_POST_BINDING;
String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
UserSessionModel userSession = authResult.getSession();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
@ -364,33 +370,51 @@ public class SamlService {
}
logger.debug("browser Logout");
return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
}
} else if (logoutRequest.getSessionIndex() != null) {
for (String sessionIndex : logoutRequest.getSessionIndex()) {
ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex);
if (clientSession == null) continue;
if (clientSession.getClient().getClientId().equals(client.getClientId())) {
// remove requesting client from logout
clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT);
}
UserSessionModel userSession = clientSession.getUserSession();
try {
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
} catch (Exception e) {
logger.warn("Failure with backchannel logout", e);
}
String redirectUri = null;
if (client instanceof ClientModel) {
redirectUri = ((ClientModel)client).getBaseUrl();
}
if (redirectUri != null) {
redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client);
if (redirectUri == null) {
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
}
if (redirectUri != null) {
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
} else {
return Response.ok().build();
}
}
// default
private Response logout(UserSessionModel userSession) {
Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
if (response == null) event.user(userSession.getUser()).session(userSession).success();
return response;
String logoutBinding = getBindingType();
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
String logoutRelayState = relayState;
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(logoutRequest.getID());
builder.destination(logoutBindingUri);
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
builder.relayState(logoutRelayState);
if (SamlProtocol.requiresRealmSignature(client)) {
SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client);
builder.signatureAlgorithm(algorithm)
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
}
try {
if (SamlProtocol.SAML_POST_BINDING.equals(logoutBinding)) {
return builder.postBinding().response(logoutBindingUri);
} else {
return builder.redirectBinding().response(logoutBindingUri);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private boolean checkSsl() {
@ -414,6 +438,7 @@ public class SamlService {
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
}
@Override
protected SAMLDocumentHolder extractResponseDocument(String response) {
return SAMLRequestParser.parseResponsePostBinding(response);
@ -446,7 +471,6 @@ public class SamlService {
}
@Override
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
@ -478,7 +502,7 @@ public class SamlService {
@GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
logger.debug("SAML GET");
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
}

View file

@ -86,7 +86,7 @@ public class TokenManager {
UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers);
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
}
ClientSessionModel clientSession = null;

View file

@ -126,7 +126,7 @@ public class LogoutEndpoint {
return AuthenticationManager.browserLogout(session, realm, authResult.getSession(), uriInfo, clientConnection, headers);
} else if (userSession != null) { // non browser logout
event.event(EventType.LOGOUT);
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
event.user(userSession.getUser()).session(userSession).success();
}
@ -183,7 +183,7 @@ public class LogoutEndpoint {
}
private void logout(UserSessionModel userSession) {
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
event.user(userSession.getUser()).session(userSession).success();
}

View file

@ -104,7 +104,20 @@ public class AuthenticationManager {
}
public static void backchannelLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
/**
* Do not logout broker
*
* @param session
* @param realm
* @param userSession
* @param uriInfo
* @param connection
* @param headers
*/
public static void backchannelLogout(KeycloakSession session, RealmModel realm,
UserSessionModel userSession, UriInfo uriInfo,
ClientConnection connection, HttpHeaders headers,
boolean logoutBroker) {
if (userSession == null) return;
UserModel user = userSession.getUser();
userSession.setState(UserSessionModel.State.LOGGING_OUT);
@ -115,6 +128,16 @@ public class AuthenticationManager {
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
}
if (logoutBroker) {
String brokerId = userSession.getNote(IdentityBrokerService.BROKER_PROVIDER_ID);
if (brokerId != null) {
IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId);
try {
identityProvider.backchannelLogout(userSession, uriInfo, realm);
} catch (Exception e) {
}
}
}
userSession.setState(UserSessionModel.State.LOGGED_OUT);
session.sessions().removeUserSession(realm, userSession);
}
@ -131,8 +154,8 @@ public class AuthenticationManager {
protocol.backchannelLogout(userSession, clientSession);
clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT);
}
}
}
public static Response browserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
if (userSession == null) return null;
@ -525,7 +548,7 @@ public class AuthenticationManager {
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
if (!isSessionValid(realm, userSession)) {
if (userSession != null) backchannelLogout(session, realm, userSession, uriInfo, connection, headers);
if (userSession != null) backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
logger.debug("User session not active");
return null;
}

View file

@ -480,7 +480,7 @@ public class AccountService {
UserModel user = auth.getUser();
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
}
UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "sessionsPage");
@ -675,7 +675,7 @@ public class AccountService {
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel s : sessions) {
if (!s.getId().equals(auth.getSession().getId())) {
AuthenticationManager.backchannelLogout(session, realm, s, uriInfo, clientConnection, headers);
AuthenticationManager.backchannelLogout(session, realm, s, uriInfo, clientConnection, headers, true);
}
}

View file

@ -595,7 +595,7 @@ public class LoginActionsService {
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
}

View file

@ -280,7 +280,7 @@ public class RealmAdminResource {
public void deleteSession(@PathParam("session") String sessionId) {
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
if (userSession == null) throw new NotFoundException("Sesssion not found");
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers);
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
}
/**

View file

@ -379,7 +379,7 @@ public class UsersResource {
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
}
}

View file

@ -3,7 +3,8 @@ package org.keycloak.social.facebook;
import org.codehaus.jackson.JsonNode;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
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.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.social.SocialIdentityProvider;
@ -27,7 +28,7 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken).asJson();
JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken));
String id = getJsonProperty(profile, "id");

View file

@ -3,7 +3,8 @@ package org.keycloak.social.github;
import org.codehaus.jackson.JsonNode;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
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.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.social.SocialIdentityProvider;
@ -28,7 +29,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
@Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken).asJson();
JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken));
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));

View file

@ -25,7 +25,8 @@ import org.codehaus.jackson.JsonNode;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
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.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.social.SocialIdentityProvider;
@ -55,7 +56,7 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
log.debug("doGetFederatedIdentity()");
try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken).asJson();
JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken));
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));

View file

@ -26,7 +26,8 @@ import java.util.HashMap;
import org.codehaus.jackson.JsonNode;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
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.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.social.SocialIdentityProvider;
@ -64,7 +65,7 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
if (log.isDebugEnabled()) {
log.debug("StackOverflow profile request to: " + URL);
}
JsonNode profile = SimpleHttp.doGet(URL).asJson().get("items").get(0);
JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(URL)).get("items").get(0);
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));

View file

@ -120,7 +120,8 @@
"forceAuthn": true,
"validateSignature": true,
"postBindingResponse": true,
"postBindingAuthnRequest": true
"postBindingAuthnRequest": true,
"backchannelSupported": true
}
},
{
@ -169,7 +170,8 @@
"tokenUrl": "http://localhost:8082/auth/realms/realm-with-oidc-identity-provider/protocol/openid-connect/token",
"userInfoUrl": "http://localhost:8082/auth/realms/realm-with-oidc-identity-provider/protocol/openid-connect/userinfo",
"logoutUrl": "http://localhost:8082/auth/realms/realm-with-oidc-identity-provider/tokens/logout",
"defaultScope": "email profile"
"defaultScope": "email profile",
"backchannelSupported": "true"
}
}
],