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:
parent
75909a0add
commit
89c6cda2ac
9 changed files with 116 additions and 11 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -287,6 +287,22 @@ public class AssertionUtil {
|
|||
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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue