saml redirect logout 1st step

This commit is contained in:
Bill Burke committed 2015-01-16 18:57:33 -05:00
1 parent e796c11b64
commit a0ba6d2c16
31 files changed
+801 -63

No files matched your search

@@ -140,11 +140,14 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
}
public String htmlResponse() throws ProcessingException, ConfigurationException, IOException {
return buildHtml(encoded());
return buildHtml(encoded(), destination);
}
public Response response() throws ConfigurationException, ProcessingException, IOException {
return buildResponse(document);
return buildResponse(document, destination);
}
public Response response(String actionUrl) throws ConfigurationException, ProcessingException, IOException {
return buildResponse(document, actionUrl);
}
}
@@ -162,11 +165,15 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
public Document getDocument() {
return document;
}
public URI responseUri() throws ConfigurationException, ProcessingException, IOException {
return generateRedirectUri("SAMLResponse", document);
public URI responseUri(String redirectUri) throws ConfigurationException, ProcessingException, IOException {
return generateRedirectUri("SAMLResponse", redirectUri, document);
}
public Response response() throws ProcessingException, ConfigurationException, IOException {
URI uri = responseUri();
return response(destination);
}
public Response response(String redirectUri) throws ProcessingException, ConfigurationException, IOException {
URI uri = responseUri(redirectUri);
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
@@ -259,8 +266,8 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
}
protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
String str = buildHtmlPostResponse(responseDoc);
protected Response buildResponse(Document responseDoc, String actionUrl) throws ProcessingException, ConfigurationException, IOException {
String str = buildHtmlPostResponse(responseDoc, actionUrl);
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
@@ -269,14 +276,14 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
.header("Cache-Control", "no-cache, no-store").build();
}
protected String buildHtmlPostResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
protected String buildHtmlPostResponse(Document responseDoc, String actionUrl) throws ProcessingException, ConfigurationException, IOException {
byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8");
String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes));
return buildHtml(samlResponse);
return buildHtml(samlResponse, actionUrl);
}
protected String buildHtml(String samlResponse) {
protected String buildHtml(String samlResponse, String actionUrl) {
if (destination == null) {
throw SALM2LoginResponseBuilder.logger.nullValueError("Destination is null");
}
@@ -291,7 +298,7 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
builder.append("</HEAD>");
builder.append("<BODY Onload=\"document.forms[0].submit()\">");
builder.append("<FORM METHOD=\"POST\" ACTION=\"" + destination + "\">");
builder.append("<FORM METHOD=\"POST\" ACTION=\"" + actionUrl + "\">");
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + key + "\"" + " VALUE=\"" + samlResponse + "\"/>");
if (isNotNull(relayState)) {
@@ -315,8 +322,8 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
}
protected URI generateRedirectUri(String samlParameterName, Document document) throws ConfigurationException, ProcessingException, IOException {
UriBuilder builder = UriBuilder.fromUri(destination)
protected URI generateRedirectUri(String samlParameterName, String redirectUri, Document document) throws ConfigurationException, ProcessingException, IOException {
UriBuilder builder = UriBuilder.fromUri(redirectUri)
.replaceQuery(null)
.queryParam(samlParameterName, base64Encoded(document));
if (relayState != null) {
@@ -0,0 +1,81 @@
package org.keycloak.protocol.saml;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ParsingException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.picketlink.identity.federation.saml.v2.protocol.StatusCodeType;
import org.picketlink.identity.federation.saml.v2.protocol.StatusResponseType;
import org.picketlink.identity.federation.saml.v2.protocol.StatusType;
import org.w3c.dom.Document;
import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2LogoutResponseBuilder extends SAML2BindingBuilder<SAML2LogoutResponseBuilder> {
protected String logoutRequestID;
public SAML2LogoutResponseBuilder logoutRequestID(String logoutRequestID) {
this.logoutRequestID = logoutRequestID;
return this;
}
public RedirectBindingBuilder redirectBinding() throws ConfigurationException, ProcessingException {
Document samlResponseDocument = buildDocument();
return new RedirectBindingBuilder(samlResponseDocument);
}
public PostBindingBuilder postBinding() throws ConfigurationException, ProcessingException {
Document samlResponseDocument = buildDocument();
return new PostBindingBuilder(samlResponseDocument);
}
public Document buildDocument() throws ProcessingException {
Document samlResponse = null;
try {
StatusResponseType statusResponse = new StatusResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
// Status
StatusType statusType = new StatusType();
StatusCodeType statusCodeType = new StatusCodeType();
statusCodeType.setValue(URI.create(JBossSAMLURIConstants.STATUS_SUCCESS.get()));
statusType.setStatusCode(statusCodeType);
statusResponse.setStatus(statusType);
statusResponse.setInResponseTo(logoutRequestID);
NameIDType issuer = new NameIDType();
issuer.setValue(responseIssuer);
statusResponse.setIssuer(issuer);
statusResponse.setDestination(destination);
SAML2Response saml2Response = new SAML2Response();
samlResponse = saml2Response.convert(statusResponse);
} catch (ConfigurationException e) {
throw new ProcessingException(e);
} catch (ParsingException e) {
throw new ProcessingException(e);
}
if (encrypt) encryptDocument(samlResponse);
return samlResponse;
}
}
@@ -21,12 +21,16 @@ import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
import org.keycloak.services.resources.flows.Flows;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ParsingException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
import org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.security.PublicKey;
import java.util.UUID;
@@ -55,6 +59,12 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_ENCRYPT = "saml.encrypt";
public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
public static final String SAML_LOGOUT_ISSUER = "saml.logout.issuer";
public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
public static final String SAML_LOGOUT_BINDING_URI = "SAML_LOGOUT_BINDING_URI";
public static final String SAML_LOGOUT_SIGNATURE_ALGORITHM = "saml.logout.signature.algorithm";
public static final String SAML_NAME_ID = "SAML_NAME_ID";
public static final String SAML_NAME_ID_FORMAT = "SAML_NAME_ID_FORMAT";
public static final String SAML_DEFAULT_NAMEID_FORMAT = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get();
@@ -122,6 +132,15 @@ public class SamlProtocol implements LoginProtocol {
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || "true".equals(client.getAttribute(SAML_FORCE_POST_BINDING));
}
protected boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
String note = session.getNote(SamlProtocol.SAML_LOGOUT_BINDING);
return SamlProtocol.SAML_POST_BINDING.equals(note);
}
protected boolean isLogoutPostBindingForClient(ClientModel client) {
return SamlProtocol.SAML_POST_BINDING.equals(client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING));
}
protected String getNameIdFormat(ClientSessionModel clientSession) {
String nameIdFormat = clientSession.getNote(GeneralConstants.NAMEID_FORMAT);
if(nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT;
@@ -222,19 +241,19 @@ public class SamlProtocol implements LoginProtocol {
}
}
private boolean requiresRealmSignature(ClientModel client) {
public static boolean requiresRealmSignature(ClientModel client) {
return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE));
}
private boolean requiresAssertionSignature(ClientModel client) {
public static boolean requiresAssertionSignature(ClientModel client) {
return "true".equals(client.getAttribute(SAML_ASSERTION_SIGNATURE));
}
private boolean includeAuthnStatement(ClientModel client) {
public static boolean includeAuthnStatement(ClientModel client) {
return "true".equals(client.getAttribute(SAML_AUTHNSTATEMENT));
}
private boolean multivaluedRoles(ClientModel client) {
public static boolean multivaluedRoles(ClientModel client) {
return "true".equals(client.getAttribute(SAML_MULTIVALUED_ROLES));
}
@@ -271,34 +290,77 @@ public class SamlProtocol implements LoginProtocol {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
protected String getBindingUri(ClientModel client) {
String bindingUri = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING_URI);
if (bindingUri == null ) bindingUri = ((ApplicationModel)client).getManagementUrl();
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), bindingUri);
}
@Override
public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
if (!(client instanceof ApplicationModel)) return null;
ApplicationModel app = (ApplicationModel)client;
String bindingUri = getBindingUri(client);
if (bindingUri == null) return null;
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(clientSession, client);
try {
if (isLogoutPostBindingForClient(app)) {
return logoutBuilder.postBinding().response(bindingUri);
} else {
return logoutBuilder.redirectBinding().response(bindingUri);
}
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ParsingException e) {
throw new RuntimeException(e);
}
}
@Override
public Response finishLogout(UserSessionModel userSession) {
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(userSession.getNote(SAML_LOGOUT_REQUEST_ID));
builder.destination(userSession.getNote(SAML_LOGOUT_ISSUER));
String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
if (signingAlgorithm != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
builder.signatureAlgorithm(algorithm)
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
}
try {
if (isLogoutPostBindingForInitiator(userSession)) {
return builder.postBinding().response(userSession.getNote(SAML_LOGOUT_BINDING_URI));
} else {
return builder.redirectBinding().response(userSession.getNote(SAML_LOGOUT_BINDING_URI));
}
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
if (!(client instanceof ApplicationModel)) return;
ApplicationModel app = (ApplicationModel)client;
if (app.getManagementUrl() == null) return;
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(clientSession, client);
// build userPrincipal with subject used at login
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
.userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
.destination(client.getClientId());
if (requiresRealmSignature(client)) {
logoutBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
}
/*
if (requiresEncryption(client)) {
PublicKey publicKey = null;
try {
publicKey = PemUtils.decodePublicKey(client.getAttribute(ClientModel.PUBLIC_KEY));
} catch (Exception e) {
logger.error("failed", e);
return;
}
logoutBuilder.encrypt(publicKey);
}
*/
String logoutRequestString = null;
try {
@@ -344,6 +406,31 @@ public class SamlProtocol implements LoginProtocol {
}
protected SAML2LogoutRequestBuilder createLogoutRequest(ClientSessionModel clientSession, ClientModel client) {
// build userPrincipal with subject used at login
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
.userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
.destination(client.getClientId());
if (requiresRealmSignature(client)) {
logoutBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
}
/*
if (requiresEncryption(client)) {
PublicKey publicKey = null;
try {
publicKey = PemUtils.decodePublicKey(client.getAttribute(ClientModel.PUBLIC_KEY));
} catch (Exception e) {
logger.error("failed", e);
return;
}
logoutBuilder.encrypt(publicKey);
}
*/
return logoutBuilder;
}
@Override
public void close() {
@@ -20,6 +20,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OpenIDConnectService;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.util.StreamUtil;
@@ -118,10 +119,24 @@ public class SamlService {
return null;
}
protected Response handleSamlResponse(String samleResponse, String relayState) {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
protected Response handleSamlResponse(String samlResponse, String relayState) {
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
if (authResult == null) {
logger.warn("Unknown saml response.");
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
}
// assume this is a logout response
UserSessionModel userSession = authResult.getSession();
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
logger.warn("Unknown saml response.");
logger.warn("UserSession is not tagged as logging out.");
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
}
return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection);
}
protected Response handleSamlRequest(String samlRequest, String relayState) {
@@ -176,7 +191,7 @@ public class SamlService {
} else if (samlObject instanceof LogoutRequestType) {
event.event(EventType.LOGOUT);
LogoutRequestType logout = (LogoutRequestType) samlObject;
return logoutRequest(logout, client);
return logoutRequest(logout, client, relayState);
} else {
event.event(EventType.LOGIN);
@@ -255,13 +270,32 @@ public class SamlService {
protected abstract String getBindingType();
protected Response logoutRequest(LogoutRequestType requestAbstractType, ClientModel client) {
protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) {
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
if (authResult != null) {
logout(authResult.getSession());
String bindingUri = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING_URI);
if (bindingUri == null ) bindingUri = ((ApplicationModel)client).getManagementUrl();
bindingUri = ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), bindingUri);
UserSessionModel userSession = authResult.getSession();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
if (SamlProtocol.requiresRealmSignature(client)) {
userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString());
}
if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
String logoutBinding = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING);
if (logoutBinding == null) logoutBinding = getBindingType();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
userSession.setNote(SamlProtocol.SAML_LOGOUT_ISSUER, logoutRequest.getIssuer().getValue());
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection);
}
String redirectUri = null;
if (client instanceof ApplicationModel) {
@@ -269,20 +303,23 @@ public class SamlService {
}
if (redirectUri != null) {
String validatedRedirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);;
if (validatedRedirect == null) {
redirectUri = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);
if (redirectUri == null) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri.");
}
return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build();
}
if (redirectUri != null) {
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
} else {
return Response.ok().build();
}
}
private void logout(UserSessionModel userSession) {
authManager.logout(session, realm, userSession, uriInfo, clientConnection);
event.user(userSession.getUser()).session(userSession).success();
private Response logout(UserSessionModel userSession) {
Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection);
if (response == null) event.user(userSession.getUser()).session(userSession).success();
return response;
}
private boolean checkSsl() {