/* * Copyright 2014 Federal Chancellery Austria * MOA-ID has been developed in a cooperation between BRZ, the Federal * Chancellery Austria - ICT staff unit, and Graz University of Technology. * * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * http://www.osor.eu/eupl/ * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and * limitations under the Licence. * * This product combines work with different licenses. See the "NOTICE" text * file for details on the various modules and licenses. * The "NOTICE" text file is part of the distribution. Any derivative works * that you distribute must include a readable copy of the "NOTICE" text file. */ package at.gv.egovernment.moa.id.protocols.pvp2x.verification; import java.util.ArrayList; import java.util.List; import org.joda.time.DateTime; import org.opensaml.common.binding.decoding.BasicURLComparator; import org.opensaml.saml2.core.Audience; import org.opensaml.saml2.core.AudienceRestriction; import org.opensaml.saml2.core.Conditions; import org.opensaml.saml2.core.EncryptedAssertion; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.StatusCode; import org.opensaml.saml2.core.validator.AudienceRestrictionSchemaValidator; import org.opensaml.saml2.core.validator.AudienceSchemaValidator; import org.opensaml.saml2.encryption.Decrypter; import org.opensaml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; import org.opensaml.xml.encryption.ChainingEncryptedKeyResolver; import org.opensaml.xml.encryption.DecryptionException; import org.opensaml.xml.encryption.InlineEncryptedKeyResolver; import org.opensaml.xml.encryption.SimpleRetrievalMethodEncryptedKeyResolver; import org.opensaml.xml.security.credential.Credential; import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver; import org.opensaml.xml.validation.ValidationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import at.gv.egiz.eaaf.modules.pvp2.exception.SchemaValidationException; import at.gv.egiz.eaaf.modules.pvp2.impl.verification.SAMLVerificationEngine; import at.gv.egiz.eaaf.modules.pvp2.sp.exception.AssertionValidationExeption; import at.gv.egovernment.moa.id.commons.MOAIDAuthConstants; import at.gv.egovernment.moa.id.commons.api.AuthConfiguration; import at.gv.egovernment.moa.id.commons.api.exceptions.ConfigurationException; import at.gv.egovernment.moa.logging.Logger; /** * @author tlenz * */ @Service("SAMLVerificationEngineSP") public class SAMLVerificationEngineSP extends SAMLVerificationEngine { @Autowired AuthConfiguration authConfig; /** * Validate a PVP response and all included assertions * * @param samlResp * @param validateDestination * @param assertionDecryption * @param spEntityID * @param loggerSPName * @throws AssertionValidationExeption */ public void validateAssertion(Response samlResp, boolean validateDestination, Credential assertionDecryption, String spEntityID, String loggerSPName) throws AssertionValidationExeption { validateAssertion(samlResp, validateDestination, assertionDecryption, spEntityID, loggerSPName, true); } public void validateAssertion(Response samlResp, boolean validateDestination, Credential assertionDecryption, String spEntityID, String loggerSPName, boolean validateDateTime) throws AssertionValidationExeption { try { if (samlResp.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) { List saml2assertions = new ArrayList(); //validate destination URL List allowedPublicURLPrefix = authConfig.getPublicURLPrefix(); boolean isValidDestination = false; for (String allowedPreFix : allowedPublicURLPrefix) { if (validateDestination && samlResp.getDestination().startsWith( allowedPreFix)) { isValidDestination = true; break; } } if (!isValidDestination && validateDestination) { Logger.warn("PVP 2.1 assertion destination does not match to IDP URL"); throw new AssertionValidationExeption("sp.pvp2.07", new Object[]{loggerSPName, "'Destination' attribute is not valid"}); } //validate response issueInstant DateTime issueInstant = samlResp.getIssueInstant(); if (issueInstant == null) { Logger.warn("PVP response does not include a 'IssueInstant' attribute"); throw new AssertionValidationExeption("sp.pvp2.07", new Object[]{loggerSPName, "'IssueInstant' attribute is not included"}); } if (validateDateTime && issueInstant.minusMinutes(MOAIDAuthConstants.TIME_JITTER).isAfterNow()) { Logger.warn("PVP response: IssueInstant DateTime is not valid anymore."); throw new AssertionValidationExeption("sp.pvp2.07", new Object[]{loggerSPName, "'IssueInstant' Time is not valid any more"}); } //check encrypted Assertions List encryAssertionList = samlResp.getEncryptedAssertions(); if (encryAssertionList != null && encryAssertionList.size() > 0) { //decrypt assertions Logger.debug("Found encryped assertion. Start decryption ..."); StaticKeyInfoCredentialResolver skicr = new StaticKeyInfoCredentialResolver(assertionDecryption); ChainingEncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(); encryptedKeyResolver.getResolverChain().add( new InlineEncryptedKeyResolver() ); encryptedKeyResolver.getResolverChain().add( new EncryptedElementTypeEncryptedKeyResolver() ); encryptedKeyResolver.getResolverChain().add( new SimpleRetrievalMethodEncryptedKeyResolver() ); Decrypter samlDecrypter = new Decrypter(null, skicr, encryptedKeyResolver); for (EncryptedAssertion encAssertion : encryAssertionList) saml2assertions.add(samlDecrypter.decrypt(encAssertion)); Logger.debug("Assertion decryption finished. "); } else { saml2assertions.addAll(samlResp.getAssertions()); } //validate all assertions List validatedassertions = new ArrayList(); for (org.opensaml.saml2.core.Assertion saml2assertion : saml2assertions) { boolean isAssertionValid = true; try { //schema validation performSchemaValidation(saml2assertion.getDOM()); //validate DateTime conditions Conditions conditions = saml2assertion.getConditions(); if (conditions != null) { DateTime notbefore = conditions.getNotBefore().minusMinutes(5); DateTime notafter = conditions.getNotOnOrAfter(); if (validateDateTime && (notbefore.isAfterNow() || notafter.isBeforeNow()) ) { isAssertionValid = false; Logger.info("Assertion:" + saml2assertion.getID() + " is out of Date. " + "{ Current : " + new DateTime() + " NotBefore: " + notbefore + " NotAfter : " + notafter + " }");; } //validate audienceRestrictions are valid for this SP List audienceRest = conditions.getAudienceRestrictions(); if (audienceRest == null || audienceRest.size() == 0) { Logger.info("Assertion:" + saml2assertion.getID() + " has not 'AudienceRestriction' element"); isAssertionValid = false; } else { for (AudienceRestriction el : audienceRest) { el.registerValidator(new AudienceRestrictionSchemaValidator()); el.validate(false); for (Audience audience : el.getAudiences()) { audience.registerValidator(new AudienceSchemaValidator()); audience.validate(false); if (!urlCompare(spEntityID, audience.getAudienceURI())) { Logger.info("Assertion:" + saml2assertion.getID() + " 'AudienceRestriction' is not valid."); isAssertionValid = false; } } } } } else { Logger.info("Assertion:" + saml2assertion.getID() + " contains not 'Conditions' element"); isAssertionValid = false; } //add assertion if it is valid if (isAssertionValid) { Logger.debug("Add valid Assertion:" + saml2assertion.getID()); validatedassertions.add(saml2assertion); } else Logger.warn("Remove non-valid Assertion:" + saml2assertion.getID()); } catch (SchemaValidationException e) { isAssertionValid = false; Logger.info("Assertion:" + saml2assertion.getID() + " Schema validation FAILED. Msg:" + e.getMessage()); } catch (ValidationException e) { isAssertionValid = false; Logger.info("Assertion:" + saml2assertion.getID() + " AudienceRestriction schema-validation FAILED. Msg:" + e.getMessage()); } } if (validatedassertions.isEmpty()) { Logger.info("No valid PVP 2.1 assertion received."); throw new AssertionValidationExeption("sp.pvp2.10", new Object[]{loggerSPName}); } samlResp.getAssertions().clear(); samlResp.getEncryptedAssertions().clear(); samlResp.getAssertions().addAll(validatedassertions); } else { Logger.info("PVP 2.1 assertion includes an error. Receive errorcode " + samlResp.getStatus().getStatusCode().getValue()); throw new AssertionValidationExeption("sp.pvp2.05", new Object[]{loggerSPName, samlResp.getIssuer().getValue(), samlResp.getStatus().getStatusCode().getValue(), samlResp.getStatus().getStatusMessage().getMessage()}); } } catch (DecryptionException e) { Logger.warn("Assertion decrypt FAILED.", e); throw new AssertionValidationExeption("sp.pvp2.11", null, e); } catch (ConfigurationException e) { throw new AssertionValidationExeption("pvp.12", null, e); } } protected static boolean urlCompare(String url1, String url2) { BasicURLComparator comparator = new BasicURLComparator(); comparator.setCaseInsensitive(false); return comparator.compare(url1, url2); } }