From 4750b22b6d12643949888576e18dfc85e6d973df Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 16 Oct 2014 09:14:04 -0400 Subject: [PATCH] saml signatures --- ...SALM2PostBindingLoginResponseBuilder.java} | 147 ++---------------- .../saml/SAML2PostBindingBuilder.java | 133 ++++++++++++++++ .../SAML2PostBindingErrorResponseBuilder.java | 57 +++++++ ...SAML2PostBindingLogoutResponseBuilder.java | 69 ++++++++ .../protocol/saml/SAMLRequestParser.java | 1 - .../{SamlLogin.java => SalmProtocol.java} | 85 ++++------ ...nFactory.java => SamlProtocolFactory.java} | 4 +- .../protocol/saml/SamlProtocolUtils.java | 66 ++++++++ .../keycloak/protocol/saml/SamlService.java | 83 ++++------ ...org.keycloak.protocol.LoginProtocolFactory | 2 +- .../testsuite/account/AccountTest.java | 10 +- .../src/test/resources/testsaml.json | 21 ++- 12 files changed, 430 insertions(+), 248 deletions(-) rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SAML2PostBindingResponseBuilder.java => SALM2PostBindingLoginResponseBuilder.java} (50%) create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SamlLogin.java => SalmProtocol.java} (69%) rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SamlLoginFactory.java => SamlProtocolFactory.java} (86%) create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2PostBindingLoginResponseBuilder.java similarity index 50% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingResponseBuilder.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2PostBindingLoginResponseBuilder.java index 6e89bd85dc..c65cbf33ed 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingResponseBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2PostBindingLoginResponseBuilder.java @@ -8,7 +8,6 @@ import org.picketlink.common.exceptions.ConfigurationException; import org.picketlink.common.exceptions.ProcessingException; import org.picketlink.common.util.DocumentUtil; import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; -import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature; 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; @@ -43,7 +42,7 @@ import static org.picketlink.common.util.StringUtil.isNotNull; * @author Anil.Saldhana@redhat.com * @author bburke@redhat.com */ -public class SAML2PostBindingResponseBuilder { +public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilder { protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger(); protected List roles = new LinkedList(); @@ -51,188 +50,68 @@ public class SAML2PostBindingResponseBuilder { protected boolean multiValuedRoles; protected boolean disableAuthnStatement; protected String requestID; - protected String responseIssuer; protected String authMethod; - protected String relayState; - protected String destination; protected String requestIssuer; protected Map attributes = new HashMap(); - public SAML2PostBindingResponseBuilder attributes(Map attributes) { + public SALM2PostBindingLoginResponseBuilder attributes(Map attributes) { this.attributes = attributes; return this; } - public SAML2PostBindingResponseBuilder attribute(String name, Object value) { + public SALM2PostBindingLoginResponseBuilder attribute(String name, Object value) { this.attributes.put(name, value); return this; } - public SAML2PostBindingResponseBuilder requestID(String requestID) { + public SALM2PostBindingLoginResponseBuilder requestID(String requestID) { this.requestID =requestID; return this; } - public SAML2PostBindingResponseBuilder requestIssuer(String requestIssuer) { + public SALM2PostBindingLoginResponseBuilder requestIssuer(String requestIssuer) { this.requestIssuer =requestIssuer; return this; } - public SAML2PostBindingResponseBuilder responseIssuer(String issuer) { - this.responseIssuer = issuer; - return this; - } - - public SAML2PostBindingResponseBuilder roles(List roles) { + public SALM2PostBindingLoginResponseBuilder roles(List roles) { this.roles = roles; return this; } - public SAML2PostBindingResponseBuilder roles(String... roles) { + public SALM2PostBindingLoginResponseBuilder roles(String... roles) { for (String role : roles) { this.roles.add(role); } return this; } - public SAML2PostBindingResponseBuilder authMethod(String authMethod) { + public SALM2PostBindingLoginResponseBuilder authMethod(String authMethod) { this.authMethod = authMethod; return this; } - public SAML2PostBindingResponseBuilder userPrincipal(String userPrincipal) { + public SALM2PostBindingLoginResponseBuilder userPrincipal(String userPrincipal) { this.userPrincipal = userPrincipal; return this; } - public SAML2PostBindingResponseBuilder relayState(String relayState) { - this.relayState = relayState; - return this; - } - - public SAML2PostBindingResponseBuilder destination(String destination) { - this.destination = destination; - return this; - } - - public SAML2PostBindingResponseBuilder multiValuedRoles(boolean multiValuedRoles) { + public SALM2PostBindingLoginResponseBuilder multiValuedRoles(boolean multiValuedRoles) { this.multiValuedRoles = multiValuedRoles; return this; } - public SAML2PostBindingResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) { + public SALM2PostBindingLoginResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) { this.disableAuthnStatement = disableAuthnStatement; return this; } - public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException { - Document doc = getErrorResponse(status); - return buildResponse(doc); - - - } - - public Document getErrorResponse(String status) { - Document samlResponse = null; - ResponseType responseType = null; - - SAML2Response saml2Response = new SAML2Response(); - - // Create a response type - String id = IDGenerator.create("ID_"); - - IssuerInfoHolder issuerHolder = new IssuerInfoHolder(responseIssuer); - issuerHolder.setStatusCode(status); - - IDPInfoHolder idp = new IDPInfoHolder(); - idp.setNameIDFormatValue(null); - idp.setNameIDFormat(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()); - - SPInfoHolder sp = new SPInfoHolder(); - sp.setResponseDestinationURI(destination); - - responseType = saml2Response.createResponseType(id); - responseType.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status)); - responseType.setDestination(destination); - - // Lets see how the response looks like - if (logger.isTraceEnabled()) { - StringWriter sw = new StringWriter(); - try { - saml2Response.marshall(responseType, sw); - } catch (ProcessingException e) { - logger.trace(e); - } - logger.trace("SAML Response Document: " + sw.toString()); - } - - /* - if (supportSignature) { - try { - SAML2Signature ss = new SAML2Signature(); - samlResponse = ss.sign(responseType, keyManager.getSigningKeyPair()); - } catch (Exception e) { - logger.trace(e); - throw new RuntimeException(logger.signatureError(e)); - } - } else - try { - samlResponse = saml2Response.convert(responseType); - } catch (Exception e) { - logger.trace(e); - } - */ - - return samlResponse; - } - public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException { Document responseDoc = getResponse(); return buildResponse(responseDoc); } - protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException { - byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8"); - String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes)); - - if (destination == null) { - throw logger.nullValueError("Destination is null"); - } - - StringBuilder builder = new StringBuilder(); - - String key = GeneralConstants.SAML_RESPONSE_KEY; - builder.append(""); - builder.append(""); - - builder.append("HTTP Post Binding Response (Response)"); - builder.append(""); - builder.append(""); - - builder.append("
"); - builder.append(""); - - if (isNotNull(relayState)) { - builder.append(""); - } - - builder.append(""); - - builder.append("
"); - - String str = builder.toString(); - - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoCache(true); - return Response.ok(str, MediaType.TEXT_HTML_TYPE) - .header("Pragma", "no-cache") - .header("Cache-Control", "no-cache, no-store").build(); - } - public Document getResponse() throws ConfigurationException, ProcessingException { Document samlResponseDocument = null; @@ -295,6 +174,10 @@ public class SAML2PostBindingResponseBuilder { throw logger.samlAssertionMarshallError(e); } + if (signed) { + signDocument(samlResponseDocument); + } return samlResponseDocument; } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java new file mode 100755 index 0000000000..a1b105c1af --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java @@ -0,0 +1,133 @@ +package org.keycloak.protocol.saml; + +import org.picketlink.common.constants.GeneralConstants; +import org.picketlink.common.exceptions.ConfigurationException; +import org.picketlink.common.exceptions.ProcessingException; +import org.picketlink.common.util.DocumentUtil; +import org.picketlink.identity.federation.web.util.PostBindingUtil; +import org.w3c.dom.Document; + +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; + +import static org.picketlink.common.util.StringUtil.isNotNull; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SAML2PostBindingBuilder { + protected KeyPair signingKeyPair; + protected X509Certificate signingCertificate; + protected boolean signed; + protected String signatureDigestMethod; + protected String signatureMethod; + protected String relayState; + protected String destination; + protected String responseIssuer; + + public T sign(KeyPair keyPair) { + this.signingKeyPair = keyPair; + this.signed = true; + return (T)this; + } + + public T sign(PrivateKey privateKey, PublicKey publicKey) { + this.signingKeyPair = new KeyPair(publicKey, privateKey); + this.signed = true; + return (T)this; + } + + public T sign(KeyPair keyPair, X509Certificate cert) { + this.signingKeyPair = keyPair; + this.signingCertificate = cert; + this.signed = true; + return (T)this; + } + + public T signatureDigestMethod(String method) { + this.signatureDigestMethod = method; + return (T)this; + } + + public T signatureMethod(String method) { + this.signatureMethod = method; + return (T)this; + } + + public T sign(PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) { + this.signingKeyPair = new KeyPair(publicKey, privateKey); + this.signingCertificate = cert; + this.signed = true; + return (T)this; + } + + public T destination(String destination) { + this.destination = destination; + return (T)this; + } + + public T responseIssuer(String issuer) { + this.responseIssuer = issuer; + return (T)this; + } + + public T relayState(String relayState) { + this.relayState = relayState; + return (T)this; + } + + + + protected void signDocument(Document samlDocument) throws ProcessingException { + SamlProtocolUtils.signDocument(samlDocument, signingKeyPair, signatureMethod, signatureDigestMethod, signingCertificate); + } + + protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException { + byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8"); + String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes)); + + if (destination == null) { + throw SALM2PostBindingLoginResponseBuilder.logger.nullValueError("Destination is null"); + } + + StringBuilder builder = new StringBuilder(); + + String key = GeneralConstants.SAML_RESPONSE_KEY; + builder.append(""); + builder.append(""); + + builder.append("HTTP Post Binding Response (Response)"); + builder.append(""); + builder.append(""); + + builder.append("
"); + builder.append(""); + + if (isNotNull(relayState)) { + builder.append(""); + } + + builder.append(""); + + builder.append("
"); + + String str = builder.toString(); + + CacheControl cacheControl = new CacheControl(); + cacheControl.setNoCache(true); + return Response.ok(str, MediaType.TEXT_HTML_TYPE) + .header("Pragma", "no-cache") + .header("Cache-Control", "no-cache, no-store").build(); + } + +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java new file mode 100755 index 0000000000..6d56e51a78 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java @@ -0,0 +1,57 @@ +package org.keycloak.protocol.saml; + +import org.picketlink.common.constants.JBossSAMLURIConstants; +import org.picketlink.common.exceptions.ConfigurationException; +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.saml.v2.protocol.ResponseType; +import org.w3c.dom.Document; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.StringWriter; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SAML2PostBindingErrorResponseBuilder extends SAML2PostBindingBuilder { + + public Document getErrorResponse(String status) throws ProcessingException { + Document samlResponse = null; + ResponseType responseType = null; + + SAML2Response saml2Response = new SAML2Response(); + + // Create a response type + String id = IDGenerator.create("ID_"); + + IssuerInfoHolder issuerHolder = new IssuerInfoHolder(responseIssuer); + issuerHolder.setStatusCode(status); + + IDPInfoHolder idp = new IDPInfoHolder(); + idp.setNameIDFormatValue(null); + idp.setNameIDFormat(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()); + + SPInfoHolder sp = new SPInfoHolder(); + sp.setResponseDestinationURI(destination); + + responseType = saml2Response.createResponseType(id); + responseType.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status)); + responseType.setDestination(destination); + + if (signed) { + signDocument(samlResponse); + } + return samlResponse; + } + + public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException { + Document doc = getErrorResponse(status); + return buildResponse(doc); + }} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java new file mode 100755 index 0000000000..70c1909d80 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java @@ -0,0 +1,69 @@ +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.request.SAML2Request; +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.DocumentUtil; +import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil; +import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS; +import org.picketlink.identity.federation.saml.v2.assertion.NameIDType; +import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType; +import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; +import org.picketlink.identity.federation.web.util.PostBindingUtil; +import org.w3c.dom.Document; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.net.URI; +import java.security.KeyPair; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SAML2PostBindingLogoutResponseBuilder extends SAML2PostBindingBuilder { + protected String userPrincipal; + + public SAML2PostBindingLogoutResponseBuilder userPrincipal(String userPrincipal) { + this.userPrincipal = userPrincipal; + return this; + } + + public String buildRequestString() { + try { + Document logoutRequestDocument = new SAML2Request().convert(createLogoutRequest()); + if (signed) { + signDocument(logoutRequestDocument); + } + byte[] responseBytes = DocumentUtil.getDocumentAsString(logoutRequestDocument).getBytes("UTF-8"); + return PostBindingUtil.base64Encode(new String(responseBytes)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private LogoutRequestType createLogoutRequest() throws ConfigurationException { + LogoutRequestType lort = new SAML2Request().createLogoutRequest(responseIssuer); + + NameIDType nameID = new NameIDType(); + nameID.setValue(userPrincipal); + //Deal with NameID Format + String nameIDFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); + nameID.setFormat(URI.create(nameIDFormat)); + lort.setNameID(nameID); + + long assertionValidity = PicketLinkCoreSTS.instance().getConfiguration().getIssuedTokenTimeout(); + + lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionValidity)); + lort.setDestination(URI.create(destination)); + return lort; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAMLRequestParser.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAMLRequestParser.java index 4d5b6d4b24..db198920f0 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAMLRequestParser.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAMLRequestParser.java @@ -43,6 +43,5 @@ public class SAMLRequestParser { logger.samlBase64DecodingError(e); } return null; - } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLogin.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java similarity index 69% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLogin.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java index 04e3dd13b4..c685146790 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLogin.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java @@ -20,34 +20,19 @@ import org.keycloak.services.resources.RealmsResource; 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.common.util.StringUtil; -import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request; import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants; -import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil; -import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil; -import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS; -import org.picketlink.identity.federation.saml.v2.assertion.NameIDType; -import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType; import org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler; -import org.picketlink.identity.federation.web.util.PostBindingUtil; -import org.w3c.dom.Document; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.net.URI; -import java.security.Principal; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class SamlLogin implements LoginProtocol { - protected static final Logger logger = Logger.getLogger(SamlLogin.class); +public class SalmProtocol implements LoginProtocol { + protected static final Logger logger = Logger.getLogger(SalmProtocol.class); public static final String LOGIN_PROTOCOL = "saml"; public static final String SAML_BINDING = "saml_binding"; public static final String SAML_POST_BINDING = "post"; @@ -61,19 +46,19 @@ public class SamlLogin implements LoginProtocol { @Override - public SamlLogin setSession(KeycloakSession session) { + public SalmProtocol setSession(KeycloakSession session) { this.session = session; return this; } @Override - public SamlLogin setRealm(RealmModel realm) { + public SalmProtocol setRealm(RealmModel realm) { this.realm = realm; return this; } @Override - public SamlLogin setUriInfo(UriInfo uriInfo) { + public SalmProtocol setUriInfo(UriInfo uriInfo) { this.uriInfo = uriInfo; return this; } @@ -93,15 +78,11 @@ public class SamlLogin implements LoginProtocol { } protected Response getErrorResponse(ClientSessionModel clientSession, String status) { - String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE); - String redirectUri = clientSession.getRedirectUri(); - SAML2PostBindingResponseBuilder builder = new SAML2PostBindingResponseBuilder(); - String responseIssuer = getResponseIssuer(realm); - builder .relayState(relayState) - .destination(redirectUri) - .responseIssuer(responseIssuer) - .requestIssuer(clientSession.getClient().getClientId()); - try { + SAML2PostBindingErrorResponseBuilder builder = new SAML2PostBindingErrorResponseBuilder() + .relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)) + .destination(clientSession.getRedirectUri()) + .responseIssuer(getResponseIssuer(realm)); + try { return builder.buildErrorResponse(status); } catch (Exception e) { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); @@ -111,7 +92,7 @@ public class SamlLogin implements LoginProtocol { @Override public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { ClientSessionModel clientSession = accessCode.getClientSession(); - if (SamlLogin.SAML_POST_BINDING.equals(clientSession.getNote(SamlLogin.SAML_BINDING))) { + if (SalmProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SalmProtocol.SAML_BINDING))) { return postBinding(userSession, clientSession); } throw new RuntimeException("still need to implement redirect binding"); @@ -123,7 +104,7 @@ public class SamlLogin implements LoginProtocol { String redirectUri = clientSession.getRedirectUri(); String responseIssuer = getResponseIssuer(realm); - SAML2PostBindingResponseBuilder builder = new SAML2PostBindingResponseBuilder(); + SALM2PostBindingLoginResponseBuilder builder = new SALM2PostBindingLoginResponseBuilder(); builder.requestID(requestID) .relayState(relayState) .destination(redirectUri) @@ -140,7 +121,10 @@ public class SamlLogin implements LoginProtocol { builder.roles(roleModel.getName()); } } - + ClientModel client = clientSession.getClient(); + if (requiresRealmSignature(client)) { + builder.sign(realm.getPrivateKey(), realm.getPublicKey()); + } try { return builder.buildLoginResponse(); } catch (Exception e) { @@ -149,7 +133,11 @@ public class SamlLogin implements LoginProtocol { } } - public void initClaims(SAML2PostBindingResponseBuilder builder, ClientModel model, UserModel user) { + private boolean requiresRealmSignature(ClientModel client) { + return "true".equals(client.getAttribute("samlServerSignature")); + } + + public void initClaims(SALM2PostBindingLoginResponseBuilder builder, ClientModel model, UserModel user) { if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) { builder.attribute(X500SAMLProfileConstants.EMAIL_ADDRESS.getFriendlyName(), user.getEmail()); } @@ -172,15 +160,18 @@ public class SamlLogin implements LoginProtocol { ApplicationModel app = (ApplicationModel)client; if (app.getManagementUrl() == null) return; + SAML2PostBindingLogoutResponseBuilder logoutBuilder = new SAML2PostBindingLogoutResponseBuilder() + .userPrincipal(userSession.getUser().getUsername()) + .destination(client.getClientId()); + if (requiresRealmSignature(client)) { + logoutBuilder.sign(realm.getPrivateKey(), realm.getPublicKey()); + } String logoutRequestString = null; try { - LogoutRequestType logoutRequest = createLogoutRequest(userSession.getUser(), client); - Document logoutRequestDocument = new SAML2Request().convert(logoutRequest); - - byte[] responseBytes = DocumentUtil.getDocumentAsString(logoutRequestDocument).getBytes("UTF-8"); - logoutRequestString = PostBindingUtil.base64Encode(new String(responseBytes)); + logoutRequestString = logoutBuilder.buildRequestString(); } catch (Exception e) { logger.warn("failed to send saml logout", e); + return; } @@ -219,24 +210,6 @@ public class SamlLogin implements LoginProtocol { } - private LogoutRequestType createLogoutRequest(UserModel user, ClientModel client) throws ConfigurationException, ProcessingException { - LogoutRequestType lort = new SAML2Request().createLogoutRequest(getResponseIssuer(realm)); - - NameIDType nameID = new NameIDType(); - nameID.setValue(user.getUsername()); - //Deal with NameID Format - String nameIDFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); - nameID.setFormat(URI.create(nameIDFormat)); - lort.setNameID(nameID); - - long assertionValidity = PicketLinkCoreSTS.instance().getConfiguration().getIssuedTokenTimeout(); - - lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionValidity)); - lort.setDestination(URI.create(client.getClientId())); - return lort; - } - - @Override public void close() { diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLoginFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java similarity index 86% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLoginFactory.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java index 21618ed733..5021d0cbb7 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLoginFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java @@ -13,7 +13,7 @@ import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS; * @author Bill Burke * @version $Revision: 1 $ */ -public class SamlLoginFactory implements LoginProtocolFactory { +public class SamlProtocolFactory implements LoginProtocolFactory { @Override public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { @@ -22,7 +22,7 @@ public class SamlLoginFactory implements LoginProtocolFactory { @Override public LoginProtocol create(KeycloakSession session) { - return new SamlLogin().setSession(session); + return new SalmProtocol().setSession(session); } @Override diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java new file mode 100755 index 0000000000..342f089887 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java @@ -0,0 +1,66 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.VerificationException; +import org.keycloak.models.ClientModel; +import org.keycloak.util.PemUtils; +import org.picketlink.common.exceptions.ProcessingException; +import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import java.security.KeyPair; +import java.security.PublicKey; +import java.security.cert.X509Certificate; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlProtocolUtils { + + public static void verifyPostBindingSignature(ClientModel client, Document document) throws VerificationException { + if (!"true".equals(client.getAttribute("samlClientSignature"))) { + return; + } + SAML2Signature saml2Signature = new SAML2Signature(); + String publicKeyPem = client.getAttribute(ClientModel.PUBLIC_KEY); + if (publicKeyPem == null) throw new VerificationException("Client does not have a public key."); + PublicKey publicKey = null; + try { + publicKey = PemUtils.decodePublicKey(publicKeyPem); + } catch (Exception e) { + throw new VerificationException("Could not decode public key", e); + } + try { + if (!saml2Signature.validate(document, publicKey)) { + throw new VerificationException("Invalid signature on document"); + } + } catch (ProcessingException e) { + throw new VerificationException("Error validating signature", e); + } + } + + public static void signDocument(Document samlDocument, KeyPair signingKeyPair, String signatureMethod, String signatureDigestMethod, X509Certificate signingCertificate) throws ProcessingException { + SAML2Signature samlSignature = new SAML2Signature(); + + if (signatureMethod != null) { + samlSignature.setSignatureMethod(signatureMethod); + } + + if (signatureDigestMethod != null) { + samlSignature.setDigestMethod(signatureDigestMethod); + } + + Node nextSibling = samlSignature.getNextSiblingOfIssuer(samlDocument); + + samlSignature.setNextSibling(nextSibling); + + if (signingCertificate != null) { + samlSignature.setX509Certificate(signingCertificate); + } + + samlSignature.signSAMLDocument(samlDocument, signingKeyPair); + } + + +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 855b476512..708010e2e2 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -1,13 +1,11 @@ package org.keycloak.protocol.saml; import org.jboss.logging.Logger; -import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.ClientConnection; -import org.keycloak.OAuth2Constants; -import org.keycloak.events.Details; +import org.keycloak.VerificationException; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -27,15 +25,14 @@ import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder import org.picketlink.identity.federation.saml.v2.SAML2Object; import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType; import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType; +import org.picketlink.identity.federation.saml.v2.protocol.RequestAbstractType; +import org.w3c.dom.Document; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; -import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; -import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; @@ -45,8 +42,6 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; import java.net.URI; -import java.util.HashMap; -import java.util.Map; /** * Resource class for the oauth/openid connect token service @@ -137,24 +132,7 @@ public class SamlService { SAML2Object samlObject = documentHolder.getSamlObject(); - if (samlObject instanceof AuthnRequestType) { - event.event(EventType.LOGIN); - // Get the SAML Request Message - AuthnRequestType requestAbstractType = (AuthnRequestType) samlObject; - return loginRequest(relayState, requestAbstractType); - } else if (samlObject instanceof LogoutRequestType) { - event.event(EventType.LOGOUT); - LogoutRequestType requestAbstractType = (LogoutRequestType) samlObject; - return logoutRequest(relayState, requestAbstractType); - - } else { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); - } - } - - protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType) { + RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject; String issuer = requestAbstractType.getIssuer().getValue(); ClientModel client = realm.findClient(issuer); @@ -176,6 +154,32 @@ public class SamlService { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); } + try { + SamlProtocolUtils.verifyPostBindingSignature(client, documentHolder.getSamlDocument()); + } catch (VerificationException e) { + logger.error("request validation failed", e); + event.error(Errors.INVALID_CLIENT); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid requester."); + } + if (samlObject instanceof AuthnRequestType) { + event.event(EventType.LOGIN); + // Get the SAML Request Message + AuthnRequestType authn = (AuthnRequestType) samlObject; + return loginRequest(relayState, authn, client); + } else if (samlObject instanceof LogoutRequestType) { + event.event(EventType.LOGOUT); + LogoutRequestType logout = (LogoutRequestType) samlObject; + return logoutRequest(logout, client); + + } else { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.INVALID_TOKEN); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + } + } + + protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) { + URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); @@ -186,10 +190,10 @@ public class SamlService { ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlLogin.LOGIN_PROTOCOL); + clientSession.setAuthMethod(SalmProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirect); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); - clientSession.setNote(SamlLogin.SAML_BINDING, SamlLogin.SAML_POST_BINDING); + clientSession.setNote(SalmProtocol.SAML_BINDING, SalmProtocol.SAML_POST_BINDING); clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); clientSession.setNote("REQUEST_ID", requestAbstractType.getID()); @@ -212,28 +216,7 @@ public class SamlService { return forms.createLogin(); } - protected Response logoutRequest(String relayState, LogoutRequestType requestAbstractType) { - String issuer = requestAbstractType.getIssuer().getValue(); - ClientModel client = realm.findClient(issuer); - - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); - } - - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); - } - if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); - } - if (client.isDirectGrantsOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); - } - + protected Response logoutRequest(LogoutRequestType requestAbstractType, ClientModel client) { // 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) { diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory index 632a1dbf02..d0a2dd046f 100755 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory @@ -1 +1 @@ -org.keycloak.protocol.saml.SamlLoginFactory \ No newline at end of file +org.keycloak.protocol.saml.SamlProtocolFactory \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 4cfaaa3586..620f5e2a9c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -157,11 +157,11 @@ public class AccountTest { }); } -// @Test -// @Ignore -// public void runit() throws Exception { -// Thread.sleep(10000000); -// } + @Test + @Ignore + public void runit() throws Exception { + Thread.sleep(10000000); + } @Test public void returnToAppFromQueryParam() { diff --git a/testsuite/integration/src/test/resources/testsaml.json b/testsuite/integration/src/test/resources/testsaml.json index 198f170ef3..0cd30ba437 100755 --- a/testsuite/integration/src/test/resources/testsaml.json +++ b/testsuite/integration/src/test/resources/testsaml.json @@ -32,12 +32,31 @@ "name": "http://localhost:8080/sales-post/", "enabled": true, "fullScopeAllowed": true, + "protocol": "saml", "baseUrl": "http://localhost:8080/sales-post", "adminUrl": "http://localhost:8080/sales-post", "redirectUris": [ "http://localhost:8080/sales-post/*" ] - } + }, + { + "name": "http://localhost:8080/sales-post-sig/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/sales-post-sig", + "adminUrl": "http://localhost:8080/sales-post-sig", + "redirectUris": [ + "http://localhost:8080/sales-post-sig/*" + ], + "attributes": { + "samlServerSignature": "true", + "samlClientSignature": "true", + "privateKey": "MIICWwIBAAKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQABAoGADaTtoG/+foOZUiLjRWKL/OmyavK9vjgyFtThNkZY4qHOh0h3og0RdSbgIxAsIpEa1FUwU2W5yvI6mNeJ3ibFgCgcxqPk6GkAC7DWfQfdQ8cS+dCuaFTs8ObIQEvU50YzeNPiiFxRA+MnauCUXaKm/PnDfjd4tPgru7XZvlGh0wECQQDsBbN2cKkBKpr/b5oJiBcBaSZtWiMNuYBDn9x8uORj+Gy/49BUIMHF2EWyxOWz6ocP5YiynNRkPe21Zus7PEr1AkEA5yWQOkxUTIg43s4pxNSeHtL+Ebqcg54lY2xOQK0yufxUVZI8ODctAKmVBMiCKpU3mZQquOaQicuGtocpgxlScQI/YM31zZ5nsxLGf/5GL6KhzPJT0IYn2nk7IoFu7bjn9BjwgcPurpLA52TNMYWQsTqAKwT6DEhG1NaRqNWNpb4VAkBehObAYBwMm5udyHIeEc+CzUalm0iLLa0eRdiN7AUVNpCJ2V2Uo0NcxPux1AgeP5xXydXafDXYkwhINWcNO9qRAkEA58ckAC5loUGwU5dLaugsGH/a2Q8Ac8bmPglwfCstYDpl8Gp/eimb1eKyvDEELOhyImAv4/uZV9wN85V0xZXWsw==", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQAB", + "X509Certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" + } + } ], "roles" : { "realm" : [