keycloak-aplcache
Changes
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java 40(+36 -4)
Details
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index 89d6680..bb15d23 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -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.STSubType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.common.ErrorCodes;
import org.keycloak.saml.common.PicketLinkLogger;
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.fed.IssueInstantMissingException;
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.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
@@ -288,6 +288,22 @@ public class AssertionUtil {
}
/**
+ * 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
*
* @param assertion
@@ -541,7 +557,23 @@ public class AssertionUtil {
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();
Document doc = saml2Response.convert(responseType);
@@ -560,11 +592,11 @@ public class AssertionUtil {
SAMLParser parser = new SAMLParser();
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
- AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil
+ AssertionType assertion = (AssertionType) parser.parse(parser.createEventReader(DocumentUtil
.getNodeAsStream(decryptedDocumentElement)));
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
- return responseType;
+ return decryptedDocumentElement;
}
}
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
index 9a28137..5feda2b 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
@@ -23,10 +23,10 @@ package org.keycloak.saml;
*/
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 =
"<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";
if (wantAuthnRequestsSigned && signingCerts != null) {
descriptor += signingCerts;
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 38e57cb..60893ba 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -49,9 +49,12 @@ import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser;
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.exceptions.ConfigurationException;
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.constants.X500SAMLProfileConstants;
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.UriBuilder;
import javax.ws.rs.core.UriInfo;
+import javax.xml.namespace.QName;
import java.io.IOException;
import java.security.Key;
import java.security.cert.X509Certificate;
@@ -83,6 +87,8 @@ import java.util.List;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
import java.util.*;
@@ -344,7 +350,38 @@ public class SAMLEndpoint {
if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
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.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index 1f8f793..bdfea6a 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -233,6 +233,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
+ boolean wantAssertionsSigned = getConfig().isWantAssertionsSigned();
String entityId = getEntityId(uriInfo, realm);
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
@@ -244,7 +245,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
for (RsaKeyMetadata key : keys) {
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();
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
index f07495f..6c6bb26 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
@@ -121,6 +121,22 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
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() {
return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
index 6349953..bd109e6 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
@@ -47,7 +47,8 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
String nameIdFormat = samlClient.getNameIDFormat();
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
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
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java
index aa4ca66..6833b34 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java
@@ -78,7 +78,7 @@ public class ValidationTest {
public void testBrokerExportDescriptor() throws Exception {
URL schemaFile = getClass().getResource("/schema/saml/v2/saml-schema-metadata-2.0.xsd");
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");
SchemaFactory schemaFactory = SchemaFactory
.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 76c6d50..d12fe03 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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.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.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
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
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
index 3aad92b..cf2fb83 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
@@ -149,6 +149,20 @@
</div>
<kc-tooltip>{{:: 'want-authn-requests-signed.tooltip' | translate}}</kc-tooltip>
</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'">
<label class="col-md-2 control-label" for="signatureAlgorithm">{{:: 'signature-algorithm' | translate}}</label>
<div class="col-sm-6">