Merge pull request #3893 from anderius/feature/KEYCLOAK-3056-verify-signature
[WIP] Saml broker: Added wantAssertionsSigned and wantAssertionsEncrypted
This commit is contained in:
commit
e1b6ba13cc
9 changed files with 115 additions and 9 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;
|
||||
|
@ -286,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
|
||||
*
|
||||
|
@ -540,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);
|
||||
|
@ -564,6 +597,6 @@ public class AssertionUtil {
|
|||
|
||||
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();
|
||||
|
|
|
@ -236,6 +236,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
|
||||
|
||||
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
|
||||
boolean wantAssertionsSigned = getConfig().isWantAssertionsSigned();
|
||||
String entityId = getEntityId(uriInfo, realm);
|
||||
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
|
||||
|
||||
|
@ -247,7 +248,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);
|
||||
|
|
|
@ -551,7 +551,11 @@ http-post-binding-for-authn-request.tooltip=Indicates whether the AuthnRequest m
|
|||
http-post-binding-logout=HTTP-POST Binding Logout
|
||||
http-post-binding-logout.tooltip=Indicates whether to respond to requests 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
|
||||
|
|
|
@ -170,6 +170,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