Two new configuration options for the Saml broker:

* wantAssertionsSigned: This will toggle the flag in the SP Metadata Descriptor, and validate the signature if and only if "Validate signature" is selected.
 * wantAssertionsEncrypted: This will simply require that the assertion is encrypted.

 Default behavior is unchanged. The signature validation uses the original XML, and supports therefore an IdP that adds whitespace and line breaks between tags (for example OpenAM).
This commit is contained in:
Anders Båtstrand 2017-02-23 21:28:01 +01:00
parent 75909a0add
commit 89c6cda2ac
9 changed files with 116 additions and 11 deletions

View file

@ -23,10 +23,10 @@ package org.keycloak.saml;
*/ */
public class SPMetadataDescriptor { public class SPMetadataDescriptor {
public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String signingCerts) { public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, String entityId, String nameIDPolicyFormat, String signingCerts) {
String descriptor = String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" + "<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\n" + " <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\" WantAssertionsSigned=\"" + wantAssertionsSigned + "\"\n" +
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n"; " protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n";
if (wantAuthnRequestsSigned && signingCerts != null) { if (wantAuthnRequestsSigned && signingCerts != null) {
descriptor += signingCerts; descriptor += signingCerts;

View file

@ -32,6 +32,7 @@ import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.assertion.SubjectType.STSubType; import org.keycloak.dom.saml.v2.assertion.SubjectType.STSubType;
import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.common.ErrorCodes; import org.keycloak.saml.common.ErrorCodes;
import org.keycloak.saml.common.PicketLinkLogger; import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory; import org.keycloak.saml.common.PicketLinkLoggerFactory;
@ -41,7 +42,6 @@ import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.exceptions.fed.IssueInstantMissingException; import org.keycloak.saml.common.exceptions.fed.IssueInstantMissingException;
import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxParserUtil;
import org.keycloak.saml.common.util.StaxUtil; import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response; import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature; import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
@ -287,6 +287,22 @@ public class AssertionUtil {
return false; return false;
} }
/**
* Given an assertion element, validate the signature.
*/
public static boolean isSignatureValid(Element assertionElement, KeyLocator keyLocator) {
try {
Document doc = DocumentUtil.createDocument();
Node n = doc.importNode(assertionElement, true);
doc.appendChild(n);
return new SAML2Signature().validate(doc, keyLocator);
} catch (Exception e) {
logger.signatureAssertionValidationError(e);
}
return false;
}
/** /**
* Check whether the assertion has expired * Check whether the assertion has expired
* *
@ -541,7 +557,23 @@ public class AssertionUtil {
return responseType.getAssertions().get(0).getAssertion(); return responseType.getAssertions().get(0).getAssertion();
} }
public static ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException { public static boolean isAssertionEncrypted(ResponseType responseType) throws ProcessingException {
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
if (assertions.isEmpty()) {
throw new ProcessingException("No assertion from response.");
}
ResponseType.RTChoiceType rtChoiceType = assertions.get(0);
return rtChoiceType.getEncryptedAssertion() != null;
}
/**
* This method modifies the given responseType, and replaces the encrypted assertion with a decrypted version.
*
* It returns the assertion element as it was decrypted. This can be used in sginature verification.
*/
public static Element decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException {
SAML2Response saml2Response = new SAML2Response(); SAML2Response saml2Response = new SAML2Response();
Document doc = saml2Response.convert(responseType); Document doc = saml2Response.convert(responseType);
@ -560,11 +592,11 @@ public class AssertionUtil {
SAMLParser parser = new SAMLParser(); SAMLParser parser = new SAMLParser();
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement); JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil AssertionType assertion = (AssertionType) parser.parse(parser.createEventReader(DocumentUtil
.getNodeAsStream(decryptedDocumentElement))); .getNodeAsStream(decryptedDocumentElement)));
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion)); responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
return responseType; return decryptedDocumentElement;
} }
} }

View file

@ -49,9 +49,12 @@ import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser; import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants; import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil; import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
@ -74,6 +77,7 @@ 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;
import javax.xml.namespace.QName;
import java.io.IOException; import java.io.IOException;
import java.security.Key; import java.security.Key;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -83,6 +87,8 @@ import java.util.List;
import org.keycloak.rotation.HardcodedKeyLocator; import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator; import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.util.*; import java.util.*;
@ -344,7 +350,38 @@ public class SAMLEndpoint {
if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) { if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
return callback.error(relayState, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); return callback.error(relayState, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
} }
AssertionType assertion = AssertionUtil.getAssertion(responseType, keys.getPrivateKey());
boolean assertionIsEncrypted = AssertionUtil.isAssertionEncrypted(responseType);
if (config.isWantAssertionsEncrypted() && !assertionIsEncrypted) {
logger.error("The assertion is not encrypted, which is required.");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
return ErrorPage.error(session, Messages.INVALID_REQUESTER);
}
Element assertionElement;
if (assertionIsEncrypted) {
// This methods writes the parsed and decrypted assertion back on the responseType parameter:
assertionElement = AssertionUtil.decryptAssertion(responseType, keys.getPrivateKey());
} else {
/* We verify the assertion using original document to handle cases where the IdP
includes whitespace and/or newlines inside tags. */
assertionElement = DocumentUtil.getElement(holder.getSamlDocument(), new QName(JBossSAMLConstants.ASSERTION.get()));
}
if (config.isWantAssertionsSigned() && config.isValidateSignature()) {
if (!AssertionUtil.isSignatureValid(assertionElement, getIDPKeyLocator())) {
logger.error("validation failed");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SIGNATURE);
return ErrorPage.error(session, Messages.INVALID_REQUESTER);
}
}
AssertionType assertion = responseType.getAssertions().get(0).getAssertion();
SubjectType subject = assertion.getSubject(); SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType(); SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID(); NameIDType subjectNameID = (NameIDType) subType.getBaseID();

View file

@ -233,6 +233,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned(); boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
boolean wantAssertionsSigned = getConfig().isWantAssertionsSigned();
String entityId = getEntityId(uriInfo, realm); String entityId = getEntityId(uriInfo, realm);
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat(); String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
@ -244,7 +245,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
for (RsaKeyMetadata key : keys) { for (RsaKeyMetadata key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value()); addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
} }
String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, keysString.toString()); String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, wantAssertionsSigned, entityId, nameIDPolicyFormat, keysString.toString());
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build(); return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
} }

View file

@ -121,6 +121,22 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned)); getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
} }
public boolean isWantAssertionsSigned() {
return Boolean.valueOf(getConfig().get("wantAssertionsSigned"));
}
public void setWantAssertionsSigned(boolean wantAssertionsSigned) {
getConfig().put("wantAssertionsSigned", String.valueOf(wantAssertionsSigned));
}
public boolean isWantAssertionsEncrypted() {
return Boolean.valueOf(getConfig().get("wantAssertionsEncrypted"));
}
public void setWantAssertionsEncrypted(boolean wantAssertionsEncrypted) {
getConfig().put("wantAssertionsEncrypted", String.valueOf(wantAssertionsEncrypted));
}
public boolean isAddExtensionsElementWithKeyInfo() { public boolean isAddExtensionsElementWithKeyInfo() {
return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo")); return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
} }

View file

@ -47,7 +47,8 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
String nameIdFormat = samlClient.getNameIDFormat(); String nameIdFormat = samlClient.getNameIDFormat();
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT; if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true); String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true);
return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, samlClient.requiresClientSignature(), client.getClientId(), nameIdFormat, spCertificate); return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl,
samlClient.requiresClientSignature(), samlClient.requiresAssertionSignature(), client.getClientId(), nameIdFormat, spCertificate);
} }
@Override @Override

View file

@ -78,7 +78,7 @@ public class ValidationTest {
public void testBrokerExportDescriptor() throws Exception { public void testBrokerExportDescriptor() throws Exception {
URL schemaFile = getClass().getResource("/schema/saml/v2/saml-schema-metadata-2.0.xsd"); URL schemaFile = getClass().getResource("/schema/saml/v2/saml-schema-metadata-2.0.xsd");
Source xmlFile = new StreamSource(new ByteArrayInputStream(SPMetadataDescriptor.getSPDescriptor( Source xmlFile = new StreamSource(new ByteArrayInputStream(SPMetadataDescriptor.getSPDescriptor(
"POST", "http://realm/assertion", "http://realm/logout", true, "test", SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT, KeycloakModelUtils.generateKeyPairCertificate("test").getCertificate() "POST", "http://realm/assertion", "http://realm/logout", true, false, "test", SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT, KeycloakModelUtils.generateKeyPairCertificate("test").getCertificate()
).getBytes()), "SP Descriptor"); ).getBytes()), "SP Descriptor");
SchemaFactory schemaFactory = SchemaFactory SchemaFactory schemaFactory = SchemaFactory
.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

View file

@ -524,7 +524,11 @@ http-post-binding-response.tooltip=Indicates whether to respond to requests usin
http-post-binding-for-authn-request=HTTP-POST Binding for AuthnRequest http-post-binding-for-authn-request=HTTP-POST Binding for AuthnRequest
http-post-binding-for-authn-request.tooltip=Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used. http-post-binding-for-authn-request.tooltip=Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.
want-authn-requests-signed=Want AuthnRequests Signed want-authn-requests-signed=Want AuthnRequests Signed
want-authn-requests-signed.tooltip=Indicates whether the identity provider expects signed a AuthnRequest. want-authn-requests-signed.tooltip=Indicates whether the identity provider expects a signed AuthnRequest.
want-assertions-signed=Want Assertions Signed
want-assertions-signed.tooltip=Indicates whether this service provider expects a signed Assertion.
want-assertions-encrypted=Want Assertions Encrypted
want-assertions-encrypted.tooltip=Indicates whether this service provider expects an encrypted Assertion.
force-authentication=Force Authentication force-authentication=Force Authentication
identity-provider.force-authentication.tooltip=Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context. identity-provider.force-authentication.tooltip=Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context.
validate-signature=Validate Signature validate-signature=Validate Signature

View file

@ -149,6 +149,20 @@
</div> </div>
<kc-tooltip>{{:: 'want-authn-requests-signed.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'want-authn-requests-signed.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group">
<label class="col-md-2 control-label" for="wantAssertionsSigned">{{:: 'want-assertions-signed' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.wantAssertionsSigned" id="wantAssertionsSigned" name="wantAssertionsSigned" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'want-assertions-signed.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="wantAssertionsEncrypted">{{:: 'want-assertions-encrypted' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.wantAssertionsEncrypted" id="wantAssertionsEncrypted" name="wantAssertionsEncrypted" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'want-assertions-encrypted.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="identityProvider.config.wantAuthnRequestsSigned == 'true'"> <div class="form-group" data-ng-show="identityProvider.config.wantAuthnRequestsSigned == 'true'">
<label class="col-md-2 control-label" for="signatureAlgorithm">{{:: 'signature-algorithm' | translate}}</label> <label class="col-md-2 control-label" for="signatureAlgorithm">{{:: 'signature-algorithm' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">