Repository: syncope Updated Branches: refs/heads/2_0_X 1cf0d2a02 -> b652bec77 refs/heads/master 2a76703a8 -> 7e5d38b71
[SYNCOPE-1185] Ensuring that the correct SAMLSSOResponseValidator is taken into account Project: http://git-wip-us.apache.org/repos/asf/syncope/repo Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/b652bec7 Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/b652bec7 Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/b652bec7 Branch: refs/heads/2_0_X Commit: b652bec77f87090cbdeb48b9e5591265e754e493 Parents: 1cf0d2a Author: Francesco Chicchiriccò <ilgro...@apache.org> Authored: Fri Aug 4 09:36:00 2017 +0200 Committer: Francesco Chicchiriccò <ilgro...@apache.org> Committed: Fri Aug 4 09:36:00 2017 +0200 ---------------------------------------------------------------------- .../saml/sso/SAMLSSOResponseValidator.java | 369 ------------------ .../core/logic/saml2/SAML2ReaderWriter.java | 13 +- .../logic/saml2/SAMLSSOResponseValidator.java | 371 +++++++++++++++++++ 3 files changed, 374 insertions(+), 379 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/syncope/blob/b652bec7/ext/saml2sp/logic/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java ---------------------------------------------------------------------- diff --git a/ext/saml2sp/logic/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java b/ext/saml2sp/logic/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java deleted file mode 100644 index bff1fd8..0000000 --- a/ext/saml2sp/logic/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java +++ /dev/null @@ -1,369 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.cxf.rs.security.saml.sso; - -import java.util.Date; -import java.util.List; -import java.util.logging.Logger; - -import org.w3c.dom.Element; - -import org.apache.cxf.common.logging.LogUtils; -import org.apache.wss4j.common.ext.WSSecurityException; -import org.apache.wss4j.common.saml.builder.SAML2Constants; -import org.apache.wss4j.common.util.DOM2Writer; -import org.opensaml.saml.saml2.core.AudienceRestriction; -import org.opensaml.saml.saml2.core.AuthnStatement; - -/** - * Validate a SAML 2.0 Protocol Response according to the Web SSO profile. The Response - * should be validated by the SAMLProtocolResponseValidator first. - */ -//CHECKSTYLE:OFF -public class SAMLSSOResponseValidator { - - private static final Logger LOG = LogUtils.getL7dLogger(SAMLSSOResponseValidator.class); - - private String issuerIDP; - private String assertionConsumerURL; - private String clientAddress; - private String requestId; - private String spIdentifier; - private boolean enforceResponseSigned; - private boolean enforceAssertionsSigned = true; - private boolean enforceKnownIssuer = true; - private TokenReplayCache<String> replayCache; - - /** - * Enforce that Assertions contained in the Response must be signed (if the Response itself is not - * signed). The default is true. - */ - public void setEnforceAssertionsSigned(boolean enforceAssertionsSigned) { - this.enforceAssertionsSigned = enforceAssertionsSigned; - } - - /** - * Enforce that the Issuer of the received Response/Assertion is known. The default is true. - */ - public void setEnforceKnownIssuer(boolean enforceKnownIssuer) { - this.enforceKnownIssuer = enforceKnownIssuer; - } - - /** - * Validate a SAML 2 Protocol Response - * @param samlResponse - * @param postBinding - * @return a SSOValidatorResponse object - * @throws WSSecurityException - */ - public SSOValidatorResponse validateSamlResponse( - org.opensaml.saml.saml2.core.Response samlResponse, - boolean postBinding - ) throws WSSecurityException { - // Check the Issuer - validateIssuer(samlResponse.getIssuer()); - - // The Response must contain at least one Assertion. - if (samlResponse.getAssertions() == null || samlResponse.getAssertions().isEmpty()) { - LOG.fine("The Response must contain at least one Assertion"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // The Response must contain a Destination that matches the assertionConsumerURL if it is - // signed - String destination = samlResponse.getDestination(); - if (samlResponse.isSigned() - && (destination == null || !destination.equals(assertionConsumerURL))) { - LOG.fine("The Response must contain a destination that matches the assertion consumer URL"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - if (enforceResponseSigned && !samlResponse.isSigned()) { - LOG.fine("The Response must be signed!"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // Validate Assertions - org.opensaml.saml.saml2.core.Assertion validAssertion = null; - Date sessionNotOnOrAfter = null; - for (org.opensaml.saml.saml2.core.Assertion assertion : samlResponse.getAssertions()) { - // Check the Issuer - if (assertion.getIssuer() == null) { - LOG.fine("Assertion Issuer must not be null"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - validateIssuer(assertion.getIssuer()); - - if (!samlResponse.isSigned() && enforceAssertionsSigned && assertion.getSignature() == null) { - LOG.fine("The enclosed assertions in the SAML Response must be signed"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // Check for AuthnStatements and validate the Subject accordingly - if (assertion.getAuthnStatements() != null - && !assertion.getAuthnStatements().isEmpty()) { - org.opensaml.saml.saml2.core.Subject subject = assertion.getSubject(); - org.opensaml.saml.saml2.core.SubjectConfirmation subjectConf = - validateAuthenticationSubject(subject, assertion.getID(), postBinding); - if (subjectConf != null) { - validateAudienceRestrictionCondition(assertion.getConditions()); - validAssertion = assertion; - // Store Session NotOnOrAfter - for (AuthnStatement authnStatment : assertion.getAuthnStatements()) { - if (authnStatment.getSessionNotOnOrAfter() != null) { - sessionNotOnOrAfter = authnStatment.getSessionNotOnOrAfter().toDate(); - } - } - // Fall back to the SubjectConfirmationData NotOnOrAfter if we have no session NotOnOrAfter - if (sessionNotOnOrAfter == null) { - sessionNotOnOrAfter = subjectConf.getSubjectConfirmationData().getNotOnOrAfter().toDate(); - } - } - } - } - - if (validAssertion == null) { - LOG.fine("The Response did not contain any Authentication Statement that matched " - + "the Subject Confirmation criteria"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - SSOValidatorResponse validatorResponse = new SSOValidatorResponse(); - validatorResponse.setResponseId(samlResponse.getID()); - validatorResponse.setSessionNotOnOrAfter(sessionNotOnOrAfter); - if (samlResponse.getIssueInstant() != null) { - validatorResponse.setCreated(samlResponse.getIssueInstant().toDate()); - } - - Element assertionElement = validAssertion.getDOM(); - Element clonedAssertionElement = (Element)assertionElement.cloneNode(true); - validatorResponse.setAssertionElement(clonedAssertionElement); - validatorResponse.setAssertion(DOM2Writer.nodeToString(clonedAssertionElement)); - - return validatorResponse; - } - - /** - * Validate the Issuer (if it exists) - */ - private void validateIssuer(org.opensaml.saml.saml2.core.Issuer issuer) throws WSSecurityException { - if (issuer == null) { - return; - } - - // Issuer value must match (be contained in) Issuer IDP - if (enforceKnownIssuer && !issuerIDP.startsWith(issuer.getValue())) { - LOG.fine("Issuer value: " + issuer.getValue() + " does not match issuer IDP: " - + issuerIDP); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // Format must be nameid-format-entity - if (issuer.getFormat() != null - && !SAML2Constants.NAMEID_FORMAT_ENTITY.equals(issuer.getFormat())) { - LOG.fine("Issuer format is not null and does not equal: " - + SAML2Constants.NAMEID_FORMAT_ENTITY); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - } - - /** - * Validate the Subject (of an Authentication Statement). - */ - private org.opensaml.saml.saml2.core.SubjectConfirmation validateAuthenticationSubject( - org.opensaml.saml.saml2.core.Subject subject, String id, boolean postBinding - ) throws WSSecurityException { - if (subject.getSubjectConfirmations() == null) { - return null; - } - - org.opensaml.saml.saml2.core.SubjectConfirmation validSubjectConf = null; - // We need to find a Bearer Subject Confirmation method - for (org.opensaml.saml.saml2.core.SubjectConfirmation subjectConf - : subject.getSubjectConfirmations()) { - if (SAML2Constants.CONF_BEARER.equals(subjectConf.getMethod())) { - validateSubjectConfirmation(subjectConf.getSubjectConfirmationData(), id, postBinding); - validSubjectConf = subjectConf; - } - } - - return validSubjectConf; - } - - /** - * Validate a (Bearer) Subject Confirmation - */ - private void validateSubjectConfirmation( - org.opensaml.saml.saml2.core.SubjectConfirmationData subjectConfData, String id, boolean postBinding - ) throws WSSecurityException { - if (subjectConfData == null) { - LOG.fine("Subject Confirmation Data of a Bearer Subject Confirmation is null"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // Recipient must match assertion consumer URL - String recipient = subjectConfData.getRecipient(); - if (recipient == null || !recipient.equals(assertionConsumerURL)) { - LOG.fine("Recipient " + recipient + " does not match assertion consumer URL " - + assertionConsumerURL); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // We must have a NotOnOrAfter timestamp - if (subjectConfData.getNotOnOrAfter() == null - || subjectConfData.getNotOnOrAfter().isBeforeNow()) { - LOG.fine("Subject Conf Data does not contain NotOnOrAfter or it has expired"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // Need to keep bearer assertion IDs based on NotOnOrAfter to detect replay attacks - if (postBinding && replayCache != null) { - if (replayCache.getId(id) == null) { - Date expires = subjectConfData.getNotOnOrAfter().toDate(); - Date currentTime = new Date(); - long ttl = expires.getTime() - currentTime.getTime(); - replayCache.putId(id, ttl / 1000L); - } else { - LOG.fine("Replay attack with token id: " + id); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - } - - // Check address - if (subjectConfData.getAddress() != null && clientAddress != null - && !subjectConfData.getAddress().equals(clientAddress)) { - LOG.fine("Subject Conf Data address " + subjectConfData.getAddress() + " does match" - + " client address " + clientAddress); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // It must not contain a NotBefore timestamp - if (subjectConfData.getNotBefore() != null) { - LOG.fine("The Subject Conf Data must not contain a NotBefore timestamp"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - // InResponseTo must match the AuthnRequest request Id - if (requestId != null && !requestId.equals(subjectConfData.getInResponseTo())) { - LOG.fine("The InResponseTo String does match the original request id " + requestId); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } else if (requestId == null && subjectConfData.getInResponseTo() != null) { - LOG.fine("No InResponseTo String is allowed for the unsolicted case"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - - } - - private void validateAudienceRestrictionCondition( - org.opensaml.saml.saml2.core.Conditions conditions - ) throws WSSecurityException { - if (conditions == null) { - LOG.fine("Conditions are null"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - List<AudienceRestriction> audienceRestrs = conditions.getAudienceRestrictions(); - if (!matchSaml2AudienceRestriction(spIdentifier, audienceRestrs)) { - LOG.fine("Assertion does not contain unique subject provider identifier " - + spIdentifier + " in the audience restriction conditions"); - throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); - } - } - - - private boolean matchSaml2AudienceRestriction( - String appliesTo, List<AudienceRestriction> audienceRestrictions - ) { - boolean oneMatchFound = false; - if (audienceRestrictions != null && !audienceRestrictions.isEmpty()) { - for (AudienceRestriction audienceRestriction : audienceRestrictions) { - if (audienceRestriction.getAudiences() != null) { - boolean matchFound = false; - for (org.opensaml.saml.saml2.core.Audience audience : audienceRestriction.getAudiences()) { - if (appliesTo.equals(audience.getAudienceURI())) { - matchFound = true; - oneMatchFound = true; - break; - } - } - if (!matchFound) { - return false; - } - } - } - } - - return oneMatchFound; - } - - public String getIssuerIDP() { - return issuerIDP; - } - - public void setIssuerIDP(String issuerIDP) { - this.issuerIDP = issuerIDP; - } - - public String getAssertionConsumerURL() { - return assertionConsumerURL; - } - - public void setAssertionConsumerURL(String assertionConsumerURL) { - this.assertionConsumerURL = assertionConsumerURL; - } - - public String getClientAddress() { - return clientAddress; - } - - public void setClientAddress(String clientAddress) { - this.clientAddress = clientAddress; - } - - public String getRequestId() { - return requestId; - } - - public void setRequestId(String requestId) { - this.requestId = requestId; - } - - public String getSpIdentifier() { - return spIdentifier; - } - - public void setSpIdentifier(String spIdentifier) { - this.spIdentifier = spIdentifier; - } - - public void setReplayCache(TokenReplayCache<String> replayCache) { - this.replayCache = replayCache; - } - - public boolean isEnforceResponseSigned() { - return enforceResponseSigned; - } - - /** - * Enforce whether a SAML Response must be signed. - */ - public void setEnforceResponseSigned(boolean enforceResponseSigned) { - this.enforceResponseSigned = enforceResponseSigned; - } - -} http://git-wip-us.apache.org/repos/asf/syncope/blob/b652bec7/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java ---------------------------------------------------------------------- diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java index 8e47911..6fe20e6 100644 --- a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java +++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java @@ -43,7 +43,6 @@ import javax.xml.transform.stream.StreamResult; import org.apache.commons.codec.binary.Base64; import org.apache.cxf.rs.security.saml.DeflateEncoderDecoder; import org.apache.cxf.rs.security.saml.sso.SAMLProtocolResponseValidator; -import org.apache.cxf.rs.security.saml.sso.SAMLSSOResponseValidator; import org.apache.cxf.staxutils.StaxUtils; import org.apache.syncope.common.lib.SSOConstants; import org.apache.syncope.common.lib.types.SAML2BindingType; @@ -90,10 +89,6 @@ public class SAML2ReaderWriter { private String jceSigAlgo; - private SAMLProtocolResponseValidator protocolValidator; - - private SAMLSSOResponseValidator ssoResponseValidator; - private SAMLSPCallbackHandler callbackHandler; public void init() { @@ -109,11 +104,6 @@ public class SAML2ReaderWriter { jceSigAlgo = "SHA1withDSA"; } - protocolValidator = new SAMLProtocolResponseValidator(); - protocolValidator.setKeyInfoMustBeAvailable(true); - - ssoResponseValidator = new SAMLSSOResponseValidator(); - callbackHandler = new SAMLSPCallbackHandler(loader.getKeyPass()); } @@ -223,8 +213,11 @@ public class SAML2ReaderWriter { crypto.setKeyStore(loader.getKeyStore()); crypto.setTrustStore(idp.getTrustStore()); + SAMLProtocolResponseValidator protocolValidator = new SAMLProtocolResponseValidator(); + protocolValidator.setKeyInfoMustBeAvailable(true); protocolValidator.validateSamlResponse(samlResponse, crypto, callbackHandler); + SAMLSSOResponseValidator ssoResponseValidator = new SAMLSSOResponseValidator(); ssoResponseValidator.setAssertionConsumerURL(assertionConsumerURL); ssoResponseValidator.setIssuerIDP(idp.getId()); ssoResponseValidator.setRequestId(requestId); http://git-wip-us.apache.org/repos/asf/syncope/blob/b652bec7/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAMLSSOResponseValidator.java ---------------------------------------------------------------------- diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAMLSSOResponseValidator.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAMLSSOResponseValidator.java new file mode 100644 index 0000000..a730140 --- /dev/null +++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAMLSSOResponseValidator.java @@ -0,0 +1,371 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.logic.saml2; + +import java.util.Date; +import java.util.List; +import java.util.logging.Logger; + +import org.w3c.dom.Element; + +import org.apache.cxf.common.logging.LogUtils; +import org.apache.cxf.rs.security.saml.sso.SSOValidatorResponse; +import org.apache.cxf.rs.security.saml.sso.TokenReplayCache; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.common.saml.builder.SAML2Constants; +import org.apache.wss4j.common.util.DOM2Writer; +import org.opensaml.saml.saml2.core.AudienceRestriction; +import org.opensaml.saml.saml2.core.AuthnStatement; + +/** + * Validate a SAML 2.0 Protocol Response according to the Web SSO profile. The Response + * should be validated by the SAMLProtocolResponseValidator first. + */ +//CHECKSTYLE:OFF +public class SAMLSSOResponseValidator { + + private static final Logger LOG = LogUtils.getL7dLogger(SAMLSSOResponseValidator.class); + + private String issuerIDP; + private String assertionConsumerURL; + private String clientAddress; + private String requestId; + private String spIdentifier; + private boolean enforceResponseSigned; + private boolean enforceAssertionsSigned = true; + private boolean enforceKnownIssuer = true; + private TokenReplayCache<String> replayCache; + + /** + * Enforce that Assertions contained in the Response must be signed (if the Response itself is not + * signed). The default is true. + */ + public void setEnforceAssertionsSigned(boolean enforceAssertionsSigned) { + this.enforceAssertionsSigned = enforceAssertionsSigned; + } + + /** + * Enforce that the Issuer of the received Response/Assertion is known. The default is true. + */ + public void setEnforceKnownIssuer(boolean enforceKnownIssuer) { + this.enforceKnownIssuer = enforceKnownIssuer; + } + + /** + * Validate a SAML 2 Protocol Response + * @param samlResponse + * @param postBinding + * @return a SSOValidatorResponse object + * @throws WSSecurityException + */ + public SSOValidatorResponse validateSamlResponse( + org.opensaml.saml.saml2.core.Response samlResponse, + boolean postBinding + ) throws WSSecurityException { + // Check the Issuer + validateIssuer(samlResponse.getIssuer()); + + // The Response must contain at least one Assertion. + if (samlResponse.getAssertions() == null || samlResponse.getAssertions().isEmpty()) { + LOG.fine("The Response must contain at least one Assertion"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // The Response must contain a Destination that matches the assertionConsumerURL if it is + // signed + String destination = samlResponse.getDestination(); + if (samlResponse.isSigned() + && (destination == null || !destination.equals(assertionConsumerURL))) { + LOG.fine("The Response must contain a destination that matches the assertion consumer URL"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + if (enforceResponseSigned && !samlResponse.isSigned()) { + LOG.fine("The Response must be signed!"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // Validate Assertions + org.opensaml.saml.saml2.core.Assertion validAssertion = null; + Date sessionNotOnOrAfter = null; + for (org.opensaml.saml.saml2.core.Assertion assertion : samlResponse.getAssertions()) { + // Check the Issuer + if (assertion.getIssuer() == null) { + LOG.fine("Assertion Issuer must not be null"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + validateIssuer(assertion.getIssuer()); + + if (!samlResponse.isSigned() && enforceAssertionsSigned && assertion.getSignature() == null) { + LOG.fine("The enclosed assertions in the SAML Response must be signed"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // Check for AuthnStatements and validate the Subject accordingly + if (assertion.getAuthnStatements() != null + && !assertion.getAuthnStatements().isEmpty()) { + org.opensaml.saml.saml2.core.Subject subject = assertion.getSubject(); + org.opensaml.saml.saml2.core.SubjectConfirmation subjectConf = + validateAuthenticationSubject(subject, assertion.getID(), postBinding); + if (subjectConf != null) { + validateAudienceRestrictionCondition(assertion.getConditions()); + validAssertion = assertion; + // Store Session NotOnOrAfter + for (AuthnStatement authnStatment : assertion.getAuthnStatements()) { + if (authnStatment.getSessionNotOnOrAfter() != null) { + sessionNotOnOrAfter = authnStatment.getSessionNotOnOrAfter().toDate(); + } + } + // Fall back to the SubjectConfirmationData NotOnOrAfter if we have no session NotOnOrAfter + if (sessionNotOnOrAfter == null) { + sessionNotOnOrAfter = subjectConf.getSubjectConfirmationData().getNotOnOrAfter().toDate(); + } + } + } + } + + if (validAssertion == null) { + LOG.fine("The Response did not contain any Authentication Statement that matched " + + "the Subject Confirmation criteria"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + SSOValidatorResponse validatorResponse = new SSOValidatorResponse(); + validatorResponse.setResponseId(samlResponse.getID()); + validatorResponse.setSessionNotOnOrAfter(sessionNotOnOrAfter); + if (samlResponse.getIssueInstant() != null) { + validatorResponse.setCreated(samlResponse.getIssueInstant().toDate()); + } + + Element assertionElement = validAssertion.getDOM(); + Element clonedAssertionElement = (Element)assertionElement.cloneNode(true); + validatorResponse.setAssertionElement(clonedAssertionElement); + validatorResponse.setAssertion(DOM2Writer.nodeToString(clonedAssertionElement)); + + return validatorResponse; + } + + /** + * Validate the Issuer (if it exists) + */ + private void validateIssuer(org.opensaml.saml.saml2.core.Issuer issuer) throws WSSecurityException { + if (issuer == null) { + return; + } + + // Issuer value must match (be contained in) Issuer IDP + if (enforceKnownIssuer && !issuerIDP.startsWith(issuer.getValue())) { + LOG.fine("Issuer value: " + issuer.getValue() + " does not match issuer IDP: " + + issuerIDP); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // Format must be nameid-format-entity + if (issuer.getFormat() != null + && !SAML2Constants.NAMEID_FORMAT_ENTITY.equals(issuer.getFormat())) { + LOG.fine("Issuer format is not null and does not equal: " + + SAML2Constants.NAMEID_FORMAT_ENTITY); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + } + + /** + * Validate the Subject (of an Authentication Statement). + */ + private org.opensaml.saml.saml2.core.SubjectConfirmation validateAuthenticationSubject( + org.opensaml.saml.saml2.core.Subject subject, String id, boolean postBinding + ) throws WSSecurityException { + if (subject.getSubjectConfirmations() == null) { + return null; + } + + org.opensaml.saml.saml2.core.SubjectConfirmation validSubjectConf = null; + // We need to find a Bearer Subject Confirmation method + for (org.opensaml.saml.saml2.core.SubjectConfirmation subjectConf + : subject.getSubjectConfirmations()) { + if (SAML2Constants.CONF_BEARER.equals(subjectConf.getMethod())) { + validateSubjectConfirmation(subjectConf.getSubjectConfirmationData(), id, postBinding); + validSubjectConf = subjectConf; + } + } + + return validSubjectConf; + } + + /** + * Validate a (Bearer) Subject Confirmation + */ + private void validateSubjectConfirmation( + org.opensaml.saml.saml2.core.SubjectConfirmationData subjectConfData, String id, boolean postBinding + ) throws WSSecurityException { + if (subjectConfData == null) { + LOG.fine("Subject Confirmation Data of a Bearer Subject Confirmation is null"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // Recipient must match assertion consumer URL + String recipient = subjectConfData.getRecipient(); + if (recipient == null || !recipient.equals(assertionConsumerURL)) { + LOG.fine("Recipient " + recipient + " does not match assertion consumer URL " + + assertionConsumerURL); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // We must have a NotOnOrAfter timestamp + if (subjectConfData.getNotOnOrAfter() == null + || subjectConfData.getNotOnOrAfter().isBeforeNow()) { + LOG.fine("Subject Conf Data does not contain NotOnOrAfter or it has expired"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // Need to keep bearer assertion IDs based on NotOnOrAfter to detect replay attacks + if (postBinding && replayCache != null) { + if (replayCache.getId(id) == null) { + Date expires = subjectConfData.getNotOnOrAfter().toDate(); + Date currentTime = new Date(); + long ttl = expires.getTime() - currentTime.getTime(); + replayCache.putId(id, ttl / 1000L); + } else { + LOG.fine("Replay attack with token id: " + id); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + } + + // Check address + if (subjectConfData.getAddress() != null && clientAddress != null + && !subjectConfData.getAddress().equals(clientAddress)) { + LOG.fine("Subject Conf Data address " + subjectConfData.getAddress() + " does match" + + " client address " + clientAddress); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // It must not contain a NotBefore timestamp + if (subjectConfData.getNotBefore() != null) { + LOG.fine("The Subject Conf Data must not contain a NotBefore timestamp"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + // InResponseTo must match the AuthnRequest request Id + if (requestId != null && !requestId.equals(subjectConfData.getInResponseTo())) { + LOG.fine("The InResponseTo String does match the original request id " + requestId); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } else if (requestId == null && subjectConfData.getInResponseTo() != null) { + LOG.fine("No InResponseTo String is allowed for the unsolicted case"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + + } + + private void validateAudienceRestrictionCondition( + org.opensaml.saml.saml2.core.Conditions conditions + ) throws WSSecurityException { + if (conditions == null) { + LOG.fine("Conditions are null"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + List<AudienceRestriction> audienceRestrs = conditions.getAudienceRestrictions(); + if (!matchSaml2AudienceRestriction(spIdentifier, audienceRestrs)) { + LOG.fine("Assertion does not contain unique subject provider identifier " + + spIdentifier + " in the audience restriction conditions"); + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity"); + } + } + + + private boolean matchSaml2AudienceRestriction( + String appliesTo, List<AudienceRestriction> audienceRestrictions + ) { + boolean oneMatchFound = false; + if (audienceRestrictions != null && !audienceRestrictions.isEmpty()) { + for (AudienceRestriction audienceRestriction : audienceRestrictions) { + if (audienceRestriction.getAudiences() != null) { + boolean matchFound = false; + for (org.opensaml.saml.saml2.core.Audience audience : audienceRestriction.getAudiences()) { + if (appliesTo.equals(audience.getAudienceURI())) { + matchFound = true; + oneMatchFound = true; + break; + } + } + if (!matchFound) { + return false; + } + } + } + } + + return oneMatchFound; + } + + public String getIssuerIDP() { + return issuerIDP; + } + + public void setIssuerIDP(String issuerIDP) { + this.issuerIDP = issuerIDP; + } + + public String getAssertionConsumerURL() { + return assertionConsumerURL; + } + + public void setAssertionConsumerURL(String assertionConsumerURL) { + this.assertionConsumerURL = assertionConsumerURL; + } + + public String getClientAddress() { + return clientAddress; + } + + public void setClientAddress(String clientAddress) { + this.clientAddress = clientAddress; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getSpIdentifier() { + return spIdentifier; + } + + public void setSpIdentifier(String spIdentifier) { + this.spIdentifier = spIdentifier; + } + + public void setReplayCache(TokenReplayCache<String> replayCache) { + this.replayCache = replayCache; + } + + public boolean isEnforceResponseSigned() { + return enforceResponseSigned; + } + + /** + * Enforce whether a SAML Response must be signed. + */ + public void setEnforceResponseSigned(boolean enforceResponseSigned) { + this.enforceResponseSigned = enforceResponseSigned; + } + +}