All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.litsec.opensaml.saml2.common.assertion.AssertionValidator Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
/*
 * Copyright 2016-2018 Litsec AB
 *
 * Licensed 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 se.litsec.opensaml.saml2.common.assertion;

import static se.litsec.opensaml.common.validation.ValidationSupport.check;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import org.joda.time.DateTime;
import org.joda.time.chrono.ISOChronology;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.assertion.AssertionValidationException;
import org.opensaml.saml.common.assertion.ValidationContext;
import org.opensaml.saml.common.assertion.ValidationResult;
import org.opensaml.saml.saml2.assertion.ConditionValidator;
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
import org.opensaml.saml.saml2.assertion.StatementValidator;
import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Condition;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.Statement;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import se.litsec.opensaml.common.validation.AbstractObjectValidator;
import se.litsec.opensaml.common.validation.AbstractSignableObjectValidator;
import se.litsec.opensaml.common.validation.CoreValidatorParameters;
import se.litsec.opensaml.common.validation.ValidationSupport.ValidationResultException;

/**
 * A validator for {@code Assertion} objects.
 * 
 * 

* Supports the following {@link ValidationContext} static parameters: *

*
    *
  • The static parameters defined for {@link AbstractSignableObjectValidator}.
  • *
  • {@link CoreValidatorParameters#STRICT_VALIDATION}: Optional. If not supplied, defaults to 'false'. Tells whether * strict validation should be performed.
  • *
  • {@link CoreValidatorParameters#ALLOWED_CLOCK_SKEW}: Optional. Gives the number of milliseconds that is the * maximum allowed clock skew. If not given {@link AbstractObjectValidator#DEFAULT_ALLOWED_CLOCK_SKEW} is used.
  • *
  • {@link CoreValidatorParameters#MAX_AGE_MESSAGE}: Optional. Gives the maximum age (difference between issuance * time and the validation time). If not given, the {@link AbstractObjectValidator#DEFAULT_MAX_AGE_RECEIVED_MESSAGE} is * used.
  • *
  • {@link CoreValidatorParameters#RECEIVE_INSTANT}: Optional. Gives the timestamp (milliseconds since epoch) for * when the response message was received. If not given the current time is used.
  • *
  • {@link CoreValidatorParameters#AUTHN_REQUEST}: Optional. If supplied will be used in a number of validations when * information from the corresponding {@code AuthnRequest} is needed. If not supplied, other, more detailed parameters * must be given.
  • *
  • {@link CoreValidatorParameters#AUTHN_REQUEST_ID}: Required if the {@link CoreValidatorParameters#AUTHN_REQUEST} * is not assigned. Is used when validating the {@code InResponseTo} attribute of the response.
  • *
  • {@link CoreValidatorParameters#RECEIVE_URL}: Required. A String holding the URL on which we received the response * message. Is used when the {@code Destination} attribute is validated.
  • *
  • {@link CoreValidatorParameters#EXPECTED_ISSUER}: Optional. If set, is used when the issuer of the response is * validated. If not set, the issuer from the {@link CoreValidatorParameters#AUTHN_REQUEST} is used (if available).
  • *
  • {@link #RESPONSE_ISSUE_INSTANT}: Optional. If set, the IssueInstant of the Assertion being validated is compared * with the corresponding response issue instant.
  • *
* *

* Supports the following {@link ValidationContext} dynamic parameters: *

*
    *
  • {@link SAML2AssertionValidationParameters#CONFIRMED_SUBJECT_CONFIRMATION}: Optional. Will be present after * validation if subject confirmation was successfully performed.
  • *
* *

* Note: Also check the validation context parameters defined by the {@code SubjectConfirmationValidator} and * {@code ConditionValidator} instances that are installed. *

* * @author Martin Lindström ([email protected]) */ public class AssertionValidator extends AbstractSignableObjectValidator { /** * Carries a {@link Long} holding the issue instant of the Response that contained the assertion being validated. */ public static final String RESPONSE_ISSUE_INSTANT = CoreValidatorParameters.STD_PREFIX + ".ResponseIssueInstant"; /** Class logger. */ private final Logger log = LoggerFactory.getLogger(AssertionValidator.class); /** Registered {@link SubjectConfirmation} validators. */ protected Map subjectConfirmationValidators; /** Registered {@link Condition} validators. */ protected Map conditionValidators; /** Registered {@link Statement} validators. */ private Map statementValidators; /** * Constructor. * * @param trustEngine * the trust used to validate the object's signature * @param signaturePrevalidator * the signature pre-validator used to pre-validate the object's signature * @param confirmationValidators * validators used to validate {@link SubjectConfirmation} methods within the assertion * @param conditionValidators * validators used to validate the {@link Condition} elements within the assertion * @param statementValidators * validators used to validate {@link Statement}s within the assertion */ public AssertionValidator(SignatureTrustEngine trustEngine, SignaturePrevalidator signaturePrevalidator, Collection confirmationValidators, Collection conditionValidators, Collection statementValidators) { super(trustEngine, signaturePrevalidator); this.subjectConfirmationValidators = new HashMap<>(); if (confirmationValidators != null) { for (SubjectConfirmationValidator validator : confirmationValidators) { if (validator != null) { this.subjectConfirmationValidators.put(validator.getServicedMethod(), validator); } } } this.conditionValidators = new HashMap<>(); if (conditionValidators != null) { for (ConditionValidator validator : conditionValidators) { if (validator != null) { this.conditionValidators.put(validator.getServicedCondition(), validator); } } } this.statementValidators = new HashMap<>(); if (statementValidators != null) { for (StatementValidator validator : statementValidators) { if (validator != null) { this.statementValidators.put(validator.getServicedStatement(), validator); } } } } /** * Validates the assertion. */ @Override public ValidationResult validate(Assertion assertion, ValidationContext context) { try { check(this.validateID(assertion, context)); check(this.validateVersion(assertion, context)); check(this.validateIssueInstant(assertion, context)); check(this.validateIssuer(assertion, context)); check(this.validateSignature(assertion, context)); check(this.validateSubject(assertion, context)); check(this.validateConditions(assertion, context)); check(this.validateStatements(assertion, context)); } catch (ValidationResultException e) { return e.getResult(); } return ValidationResult.VALID; } /** * Validates that the {@code Assertion} object has an ID attribute. * * @param assertion * the assertion * @param context * the validation context * @return a validation result */ protected ValidationResult validateID(Assertion assertion, ValidationContext context) { if (!StringUtils.hasText(assertion.getID())) { context.setValidationFailureMessage("Missing ID attribute in Assertion"); return ValidationResult.INVALID; } return ValidationResult.VALID; } /** * Validates that the {@code Response} object has a valid Version attribute. * * @param assertion * the assertion * @param context * the validation context * @return a validation result */ protected ValidationResult validateVersion(Assertion assertion, ValidationContext context) { if (assertion.getVersion() == null || !assertion.getVersion().toString().equals(SAMLVersion.VERSION_20.toString())) { context.setValidationFailureMessage("Invalid SAML version in Assertion"); return ValidationResult.INVALID; } return ValidationResult.VALID; } /** * Validates that the {@code Assertion} object has a IssueInstant attribute and checks that its value is OK. If the * response that contained the assertion was previously validated the static context parameter * {@link #RESPONSE_ISSUE_INSTANT} should be passed. If so, the method checks that the assertion issue instant is not * after the response issue instant. Otherwise the method checks that the IssueInstant is not too old given the * {@link CoreValidatorParameters#MAX_AGE_MESSAGE} and {@link CoreValidatorParameters#RECEIVE_INSTANT} context * parameters. * * @param assertion * the response * @param context * the validation context * @return a validation result */ protected ValidationResult validateIssueInstant(Assertion assertion, ValidationContext context) { if (assertion.getIssueInstant() == null) { context.setValidationFailureMessage("Missing IssueInstant attribute in Assertion"); return ValidationResult.INVALID; } // Is the response issue instance specified? If so, we only check that the assertion issue instant // is before the response issue instant. In these cases we assume that the response issue instant // has been verified. // Long responseIssueInstant = (Long) context.getStaticParameters().get(RESPONSE_ISSUE_INSTANT); if (responseIssueInstant != null) { if (assertion.getIssueInstant().isAfter(responseIssueInstant)) { final String msg = String.format("Invalid Assertion - Its issue-instant (%s) is after the response message issue-instant (%s)", assertion.getIssueInstant(), new DateTime(responseIssueInstant, ISOChronology.getInstanceUTC())); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } } else { // Otherwise, we have to make more checks. final long receiveInstant = getReceiveInstant(context); final long issueInstant = assertion.getIssueInstant().getMillis(); final long maxAgeResponse = getMaxAgeReceivedMessage(context); final long allowedClockSkew = getAllowedClockSkew(context); // Too old? // if ((receiveInstant - issueInstant) > (maxAgeResponse + allowedClockSkew)) { final String msg = String.format("Received Assertion is too old - issue-instant: %s - receive-time: %s", assertion.getIssueInstant(), new DateTime(receiveInstant, ISOChronology.getInstanceUTC())); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } // Not yet valid? -> Clock skew is unacceptable. // if ((issueInstant - receiveInstant) > allowedClockSkew) { final String msg = String.format("Issue-instant of Assertion (%s) is newer than receive time (%s) - Non accepted clock skew", assertion.getIssueInstant(), new DateTime(receiveInstant, ISOChronology.getInstanceUTC())); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } } return ValidationResult.VALID; } /** * Ensures that the {@code Issuer} element is present and matches the expected issuer (if set in the context under the * {@link CoreValidatorParameters#EXPECTED_ISSUER} key). * * @param assertion * the assertion * @param context * the validation context * @return a validation result */ protected ValidationResult validateIssuer(Assertion assertion, ValidationContext context) { if (assertion.getIssuer() == null || assertion.getIssuer().getValue() == null) { context.setValidationFailureMessage("Missing Issuer element in Assertion"); return ValidationResult.INVALID; } String expectedIssuer = (String) context.getStaticParameters().get(CoreValidatorParameters.EXPECTED_ISSUER); if (expectedIssuer != null) { if (!assertion.getIssuer().getValue().equals(expectedIssuer)) { final String msg = String.format("Issuer of Assertion (%s) did not match expected issuer (%s)", assertion.getIssuer().getValue(), expectedIssuer); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } } else { log.warn("EXPECTED_ISSUER key not set - will not check issuer of Assertion"); } return ValidationResult.VALID; } /** * Validates the {@code Subject} element of the assertion. The default implementation returns * {@link ValidationResult#VALID} if there is no {@code Subject} element since it is optional according to the SAML * 2.0 Core specifications. * * @param assertion * the assertion * @param context * the validation context * @return a validation result */ protected ValidationResult validateSubject(Assertion assertion, ValidationContext context) { if (assertion.getSubject() == null) { // Assertions containing AuthnStatements must contain a Subject. // if (assertion.getAuthnStatements() != null && !assertion.getAuthnStatements().isEmpty()) { context.setValidationFailureMessage("Assertion contains AuthnStatement but no Subject - invalid"); return ValidationResult.INVALID; } log.debug("Assertion does not contain a Subject element - allowed by default assertion validator"); return ValidationResult.VALID; } final Subject subject = assertion.getSubject(); List confirmations = subject.getSubjectConfirmations(); if (confirmations == null || confirmations.isEmpty()) { log.debug("Assertion contains no SubjectConfirmations, default assertion validator skips subject confirmation"); return ValidationResult.VALID; } return this.validateSubjectConfirmations(assertion, confirmations, context); } /** * Validates the subject confirmations and for the one that is confirmed, it is saved in the validation context under * the {@link SAML2AssertionValidationParameters#CONFIRMED_SUBJECT_CONFIRMATION} key. * * @param assertion * the assertion * @param subjectConfirmations * the subject confirmations * @param context * the validation context * @return a validation result */ protected ValidationResult validateSubjectConfirmations(Assertion assertion, List subjectConfirmations, ValidationContext context) { for (SubjectConfirmation confirmation : subjectConfirmations) { SubjectConfirmationValidator validator = subjectConfirmationValidators.get(confirmation.getMethod()); if (validator != null) { try { ValidationResult r = validator.validate(confirmation, assertion, context); if (r == ValidationResult.VALID) { context.getDynamicParameters().put( SAML2AssertionValidationParameters.CONFIRMED_SUBJECT_CONFIRMATION, confirmation); return ValidationResult.VALID; } else { log.info("Validation of SubjectConfirmation with method '{}' failed - {}", confirmation.getMethod(), context .getValidationFailureMessage()); } } catch (AssertionValidationException e) { log.warn("Error while executing subject confirmation validation " + validator.getClass().getName(), e); } } } String msg = String.format("No subject confirmation methods were met for assertion with ID '%s'", assertion.getID()); log.debug(msg); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } /** * Validates the {@code Conditions} elements of the assertion. * * @param assertion * the assertion * @param context * the validation context * @return the validation result */ protected ValidationResult validateConditions(Assertion assertion, ValidationContext context) { Conditions conditions = assertion.getConditions(); if (conditions == null) { log.debug("Assertion contained no Conditions element - allowed by default assertion validator"); return ValidationResult.VALID; } ValidationResult timeboundsResult = this.validateConditionsTimeBounds(assertion, context); if (timeboundsResult != ValidationResult.VALID) { return timeboundsResult; } for (Condition condition : conditions.getConditions()) { ConditionValidator validator = conditionValidators.get(condition.getElementQName()); if (validator == null && condition.getSchemaType() != null) { validator = conditionValidators.get(condition.getSchemaType()); } if (validator == null) { final String msg = String.format("Unknown Condition '%s' of type '%s' in assertion '%s'", condition.getElementQName(), condition.getSchemaType(), assertion.getID()); log.warn(msg); if (isStrictValidation(context)) { context.setValidationFailureMessage(msg); return ValidationResult.INDETERMINATE; } else { continue; } } ValidationResult r; try { r = validator.validate(condition, assertion, context); } catch (AssertionValidationException e) { log.error("Failed Conditions validation - {}", e.getMessage()); log.debug("", e); context.setValidationFailureMessage(e.getMessage()); r = ValidationResult.INVALID; } if (r != ValidationResult.VALID) { String msg = String.format("Condition '%s' of type '%s' in assertion '%s' was not valid - %s.", condition.getElementQName(), condition.getSchemaType(), assertion.getID(), context.getValidationFailureMessage()); if (context.getValidationFailureMessage() != null) { msg = msg + ": " + context.getValidationFailureMessage(); } log.debug(msg); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } } return ValidationResult.VALID; } /** * Validates the NotBefore and NotOnOrAfter Conditions constraints on the assertion. * * @param assertion * the assertion whose conditions will be validated * @param context * current validation context * @return the result of the validation evaluation */ protected ValidationResult validateConditionsTimeBounds(Assertion assertion, ValidationContext context) { Conditions conditions = assertion.getConditions(); if (conditions == null) { return ValidationResult.VALID; } long clockSkew = getAllowedClockSkew(context); Long _receiveInstant = (Long) context.getStaticParameters().get(CoreValidatorParameters.RECEIVE_INSTANT); DateTime receiveInstant = _receiveInstant != null ? new DateTime(_receiveInstant, ISOChronology.getInstanceUTC()) : new DateTime(ISOChronology.getInstanceUTC()); DateTime notBefore = conditions.getNotBefore(); log.debug("Evaluating Conditions NotBefore '{}' against 'skewed now' time '{}'", notBefore, receiveInstant.plus(clockSkew)); if (notBefore != null && notBefore.isAfter(receiveInstant.plus(clockSkew))) { context.setValidationFailureMessage(String.format( "Assertion '%s' with NotBefore condition of '%s' is not yet valid", assertion.getID(), notBefore)); return ValidationResult.INVALID; } DateTime notOnOrAfter = conditions.getNotOnOrAfter(); log.debug("Evaluating Conditions NotOnOrAfter '{}' against 'skewed now' time '{}'", notOnOrAfter, receiveInstant.minus(clockSkew)); if (notOnOrAfter != null && notOnOrAfter.isBefore(receiveInstant.minus(clockSkew))) { context.setValidationFailureMessage(String.format( "Assertion '%s' with NotOnOrAfter condition of '%s' is no longer valid", assertion.getID(), notOnOrAfter)); return ValidationResult.INVALID; } return ValidationResult.VALID; } /** * Validates the statements of the assertion using the registered {@link StatementValidator} instance. * * @param assertion * the assertion to validate * @param context * the validation context * @return validation result */ protected ValidationResult validateStatements(Assertion assertion, ValidationContext context) { List statements = assertion.getStatements(); if (statements == null || statements.isEmpty()) { return ValidationResult.VALID; } StatementValidator validator; for (Statement statement : statements) { validator = statementValidators.get(statement.getElementQName()); if (validator == null && statement.getSchemaType() != null) { validator = statementValidators.get(statement.getSchemaType()); } if (validator != null) { ValidationResult result; try { result = validator.validate(statement, assertion, context); } catch (AssertionValidationException e) { log.error("Failed Statement validation - {}", e.getMessage()); log.debug("", e); context.setValidationFailureMessage(e.getMessage()); result = ValidationResult.INVALID; } if (result != ValidationResult.VALID) { return result; } } } return ValidationResult.VALID; } /** * Returns the Assertion issuer. */ @Override protected String getIssuer(Assertion signableObject) { return signableObject.getIssuer() != null ? signableObject.getIssuer().getValue() : null; } /** * Returns the Assertion ID. */ @Override protected String getID(Assertion signableObject) { return signableObject.getID(); } /** {@inheritDoc} */ @Override protected String getObjectName() { return "Assertion"; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy