Skip to content

Commit

Permalink
saml backchannel logout
Browse files Browse the repository at this point in the history
  • Loading branch information
patriot1burke committed Mar 25, 2015
1 parent eb85e6d commit 13268c5
Show file tree
Hide file tree
Showing 48 changed files with 784 additions and 281 deletions.
18 changes: 18 additions & 0 deletions broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java 100644 → 100755
Expand Up @@ -32,6 +32,8 @@ public class FederatedIdentity {
private String email; private String email;
private String token; private String token;
private String identityProviderId; private String identityProviderId;
private String brokerSessionId;
private String brokerUserId;


public FederatedIdentity(String id) { public FederatedIdentity(String id) {
if (id == null) { if (id == null) {
Expand Down Expand Up @@ -102,6 +104,22 @@ public void setIdentityProviderId(String identityProviderId) {
this.identityProviderId = identityProviderId; this.identityProviderId = identityProviderId;
} }


public String getBrokerSessionId() {
return brokerSessionId;
}

public void setBrokerSessionId(String brokerSessionId) {
this.brokerSessionId = brokerSessionId;
}

public String getBrokerUserId() {
return brokerUserId;
}

public void setBrokerUserId(String brokerUserId) {
this.brokerUserId = brokerUserId;
}

@Override @Override
public String toString() { public String toString() {
return "{" + return "{" +
Expand Down
Expand Up @@ -129,16 +129,7 @@ protected String extractTokenFromResponse(String response, String tokenName) {
} }


protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, String response) { protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, String response) {
AccessTokenResponse tokenResponse = null; String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
try {
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
} catch (IOException e) {
throw new IdentityBrokerException("Could not decode access token response.", e);
}
String accessToken = tokenResponse.getToken();
notes.put(FEDERATED_ACCESS_TOKEN, accessToken);
notes.put(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn()));


if (accessToken == null) { if (accessToken == null) {
throw new IdentityBrokerException("No access token from server."); throw new IdentityBrokerException("No access token from server.");
Expand All @@ -147,6 +138,7 @@ protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, Stri
return doGetFederatedIdentity(accessToken); return doGetFederatedIdentity(accessToken);
} }



protected FederatedIdentity doGetFederatedIdentity(String accessToken) { protected FederatedIdentity doGetFederatedIdentity(String accessToken) {
return null; return null;
} }
Expand Down Expand Up @@ -213,12 +205,7 @@ public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_P
try { try {


if (authorizationCode != null) { if (authorizationCode != null) {
String response = SimpleHttp.doPost(getConfig().getTokenUrl()) String response = generateTokenRequest(authorizationCode).asString();
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
.param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString())
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE).asString();


HashMap<String, String> userNotes = new HashMap<String, String>(); HashMap<String, String> userNotes = new HashMap<String, String>();
FederatedIdentity federatedIdentity = getFederatedIdentity(userNotes, response); FederatedIdentity federatedIdentity = getFederatedIdentity(userNotes, response);
Expand All @@ -236,5 +223,14 @@ public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_P
event.error(Errors.IDENTITY_PROVIDER_LOGIN_FAILURE); event.error(Errors.IDENTITY_PROVIDER_LOGIN_FAILURE);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
} }

public SimpleHttp generateTokenRequest(String authorizationCode) {
return SimpleHttp.doPost(getConfig().getTokenUrl())
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
.param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString())
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
}
} }
} }
@@ -0,0 +1,69 @@
package org.keycloak.broker.oidc;

import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.util.JsonSerialization;

import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import java.io.IOException;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakIdentityProvider extends OIDCIdentityProvider {

public KeycloakIdentityProvider(OIDCIdentityProviderConfig config) {
super(config);
}

protected class KeycloakEndpoint extends OIDCEndpoint {
public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm) {
super(callback, realm);
}

@POST
@Path(AdapterConstants.K_LOGOUT)
public Response backchannelLogout(String input) {
JWSInput token = new JWSInput(input);
if (!token.verify(getConfig().getSigningCertificate())) {
return Response.status(400).build(); }
LogoutAction action = null;
try {
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!validateAction(action)) return Response.status(400).build();
if (action.getAdapterSessionIds() != null) {

}
throw new RuntimeException("not impelmented yet");
}

protected boolean validateAction(AdminAction action) {
if (!action.validate()) {
logger.warn("admin request failed, not validated" + action.getAction());
return false;
}
if (action.isExpired()) {
logger.warn("admin request failed, expired token");
return false;
}
if (!getConfig().getClientId().equals(action.getResource())) {
logger.warn("Resource name does not match");
return false;

}
return true;
}


}
}
Expand Up @@ -18,19 +18,24 @@
package org.keycloak.broker.oidc; package org.keycloak.broker.oidc;


import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonNode;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.broker.oidc.util.SimpleHttp; import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.FederatedIdentity; import org.keycloak.broker.provider.FederatedIdentity;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.EventsManager; import org.keycloak.services.managers.EventsManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
Expand All @@ -39,10 +44,13 @@
import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Flows;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;


import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
Expand All @@ -53,6 +61,7 @@
* @author Pedro Igor * @author Pedro Igor
*/ */
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> { public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);


public static final String OAUTH2_PARAMETER_PROMPT = "prompt"; public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
public static final String SCOPE_OPENID = "openid"; public static final String SCOPE_OPENID = "openid";
Expand All @@ -78,6 +87,8 @@ public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm) {
super(callback, realm); super(callback, realm);
} }




@GET @GET
@Path("logout_response") @Path("logout_response")
public Response logoutResponse(@Context UriInfo uriInfo, public Response logoutResponse(@Context UriInfo uriInfo,
Expand Down Expand Up @@ -229,10 +240,6 @@ private IDToken validateIdToken(String encodedToken) {
} }
} }


private String decodeJWS(String token) {
return new JWSInput(token).readContentAsString();
}

@Override @Override
protected String getDefaultScopes() { protected String getDefaultScopes() {
return "openid"; return "openid";
Expand Down
Expand Up @@ -47,5 +47,14 @@ public String getLogoutUrl() {
public void setLogoutUrl(String url) { public void setLogoutUrl(String url) {
getConfig().put("logoutUrl", url); getConfig().put("logoutUrl", url);
} }
public String getSigningCertificate() {
return getConfig().get("signingCertificate");
}

public void setSigningCertificate(String signingCertificate) {
getConfig().put("signingCertificate", signingCertificate);
}




} }
100 changes: 47 additions & 53 deletions broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
Expand Up @@ -78,6 +78,8 @@
public class SAMLEndpoint { public class SAMLEndpoint {
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class); protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX"; public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
public static final String SAML_FEDERATED_SUBJECT = "SAML_FEDERATED_SUBJECT";
public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
protected RealmModel realm; protected RealmModel realm;
protected EventBuilder event; protected EventBuilder event;
protected SAMLIdentityProviderConfig config; protected SAMLIdentityProviderConfig config;
Expand Down Expand Up @@ -179,7 +181,7 @@ protected Response handleSamlRequest(String samlRequest, String relayState) {
SAMLDocumentHolder holder = extractRequestDocument(samlRequest); SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject(); RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
// validate destination // validate destination
if (!uriInfo.getAbsolutePath().toString().equals(requestAbstractType.getDestination())) { if (!uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) {
event.event(EventType.IDENTITY_PROVIDER_RESPONSE); event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE); event.error(Errors.INVALID_SAML_RESPONSE);
event.detail(Details.REASON, "invalid_destination"); event.detail(Details.REASON, "invalid_destination");
Expand Down Expand Up @@ -210,66 +212,55 @@ protected Response handleSamlRequest(String samlRequest, String relayState) {
} }


protected Response logoutRequest(LogoutRequestType request, String relayState) { protected Response logoutRequest(LogoutRequestType request, String relayState) {
UserModel user = session.users().getUserByUsername(request.getNameID().getValue(), realm); String brokerUserId = config.getAlias() + "." + request.getNameID().getValue();
if (user == null) { if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
event.event(EventType.LOGOUT); List<UserSessionModel> userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId);
event.error(Errors.USER_SESSION_NOT_FOUND); for (UserSessionModel userSession : userSessions) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
if (sessions == null || sessions.size() == 0) {
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
for (UserSessionModel userSession : sessions) {
String brokerId = userSession.getNote(IdentityBrokerService.BROKER_PROVIDER_ID);
if (!config.getAlias().equals(brokerId)) continue;
boolean logout = false;
if (request.getSessionIndex() == null || request.getSessionIndex().size() == 0) {
logout = true;
} else {
for (String sessionIndex : request.getSessionIndex()) {
if (sessionIndex.equals(userSession.getNote(SAML_FEDERATED_SESSION_INDEX))) {
logout = true;
break;
}
}
}
if (logout) {
try { try {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers); AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to logout", e); logger.warn("failed to do backchannel logout for userSession", e);
} }
} }


String issuerURL = getEntityId(uriInfo, realm); } else {
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder(); for (String sessionIndex : request.getSessionIndex()) {
builder.logoutRequestID(request.getID()); String brokerSessionId = brokerUserId + "." + sessionIndex;
builder.destination(config.getSingleLogoutServiceUrl()); UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
builder.issuer(issuerURL); if (userSession != null) {
builder.relayState(relayState); try {
if (config.isWantAuthnRequestsSigned()) { AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) } catch (Exception e) {
.signDocument(); logger.warn("failed to do backchannel logout for userSession", e);
} }
try {
if (config.isPostBindingResponse()) {
return builder.postBinding().response();
} else {
return builder.redirectBinding().response();
} }
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} }
}


String issuerURL = getEntityId(uriInfo, realm);
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(request.getID());
builder.destination(config.getSingleLogoutServiceUrl());
builder.issuer(issuerURL);
builder.relayState(relayState);
if (config.isWantAuthnRequestsSigned()) {
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
}
try {
if (config.isPostBindingResponse()) {
return builder.postBinding().response();
} else {
return builder.redirectBinding().response();
}
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} }
throw new RuntimeException("Unreachable");
} }


private String getEntityId(UriInfo uriInfo, RealmModel realm) { private String getEntityId(UriInfo uriInfo, RealmModel realm) {
Expand All @@ -283,8 +274,8 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
SubjectType.STSubType subType = subject.getSubType(); SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID(); NameIDType subjectNameID = (NameIDType) subType.getBaseID();
Map<String, String> notes = new HashMap<>(); Map<String, String> notes = new HashMap<>();
notes.put("SAML_FEDERATED_SUBJECT", subjectNameID.getValue()); notes.put(SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
if (subjectNameID.getFormat() != null) notes.put("SAML_FEDERATED_SUBJECT_NAMEFORMAT", subjectNameID.getFormat().toString()); if (subjectNameID.getFormat() != null) notes.put(SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
FederatedIdentity identity = new FederatedIdentity(subjectNameID.getValue()); FederatedIdentity identity = new FederatedIdentity(subjectNameID.getValue());


identity.setUsername(subjectNameID.getValue()); identity.setUsername(subjectNameID.getValue());
Expand All @@ -304,7 +295,10 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
break; break;
} }
} }
String brokerUserId = config.getAlias() + "." + subjectNameID.getValue();
identity.setBrokerUserId(brokerUserId);
if (authn != null && authn.getSessionIndex() != null) { if (authn != null && authn.getSessionIndex() != null) {
identity.setBrokerSessionId(identity.getBrokerUserId() + "." + authn.getSessionIndex());
notes.put(SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex()); notes.put(SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
} }
return callback.authenticated(notes, config, identity, relayState); return callback.authenticated(notes, config, identity, relayState);
Expand Down
Expand Up @@ -122,8 +122,8 @@ public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, Uri


SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
.issuer(getEntityId(uriInfo, realm)) .issuer(getEntityId(uriInfo, realm))
.sessionIndex(userSession.getNote("SAML_FEDERATED_SESSION_INDEX")) .sessionIndex(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX))
.userPrincipal(userSession.getNote("SAML_FEDERATED_SUBJECT"), userSession.getNote("SAML_FEDERATED_SUBJECT_NAMEFORMAT")) .userPrincipal(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT), userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT))
.destination(getConfig().getSingleLogoutServiceUrl()); .destination(getConfig().getSingleLogoutServiceUrl());
if (getConfig().isWantAuthnRequestsSigned()) { if (getConfig().isWantAuthnRequestsSigned()) {
logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
Expand Down

0 comments on commit 13268c5

Please sign in to comment.