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

org.opensaml.saml.saml2.assertion.SAML20AssertionValidator Maven / Gradle / Ivy

There is a newer version: 4.0.1
Show newest version
/*
 * Licensed to the University Corporation for Advanced Internet Development,
 * Inc. (UCAID) under one or more contributor license agreements.  See the
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID 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.opensaml.saml.saml2.assertion;

import java.util.Collection;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.xml.namespace.QName;

import net.shibboleth.utilities.java.support.collection.LazyMap;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;

import org.joda.time.DateTime;
import org.joda.time.chrono.ISOChronology;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
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.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.security.SecurityException;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;

/** 
 * A component capable of performing core validation of SAML version 2.0 {@link Assertion} instances.
 * 
 * 

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

    *
  • * {@link SAML2AssertionValidationParameters#SIGNATURE_REQUIRED}: * Optional. * If not supplied, defaults to 'true'. If an Assertion is signed, the signature is always evaluated * and the result factored into the overall validation result, regardless of the value of this setting. *
  • *
  • * {@link SAML2AssertionValidationParameters#SIGNATURE_VALIDATION_CRITERIA_SET}: * Optional. * If not supplied, a minimal criteria set will be constructed which contains an {@link EntityIDCriteria} * containing the Assertion Issuer entityID, and a {@link UsageCriteria} of {@link UsageType#SIGNING}. * If it is supplied, but either of those criteria are absent from the criteria set, they will be added * with the above values. *
  • *
  • * {@link SAML2AssertionValidationParameters#CLOCK_SKEW}: * Optional. * If not present the default clock skew of {@link SAML20AssertionValidator#DEFAULT_CLOCK_SKEW} milliseconds * will be used. *
  • *
*

* *

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

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

* * */ public class SAML20AssertionValidator { /** Default clock skew; {@value} milliseconds. */ public static final long DEFAULT_CLOCK_SKEW = 5 * 60 * 1000; /** Class logger. */ private final Logger log = LoggerFactory.getLogger(SAML20AssertionValidator.class); /** Registered {@link Condition} validators. */ private LazyMap conditionValidators; /** Registered {@link SubjectConfirmation} validators. */ private LazyMap subjectConfirmationValidators; /** Registered {@link Statement} validators. */ private LazyMap statementValidators; /** Trust engine for signature evaluation. */ private SignatureTrustEngine trustEngine; /** SAML signature profile validator.*/ private SignaturePrevalidator signaturePrevalidator; /** * Constructor. * * @param newConditionValidators validators used to validate the {@link Condition}s within the assertion * @param newConfirmationValidators validators used to validate {@link SubjectConfirmation} methods within the * assertion * @param newStatementValidators validators used to validate {@link Statement}s within the assertion * @param newTrustEngine the trust used to validate the Assertion signature * @param newSignaturePrevalidator the signature pre-validator used to pre-validate the Assertion signature */ public SAML20AssertionValidator(@Nullable final Collection newConditionValidators, @Nullable final Collection newConfirmationValidators, @Nullable final Collection newStatementValidators, @Nullable final SignatureTrustEngine newTrustEngine, @Nullable final SignaturePrevalidator newSignaturePrevalidator) { conditionValidators = new LazyMap<>(); if (newConditionValidators != null) { for (final ConditionValidator validator : newConditionValidators) { if (validator != null) { conditionValidators.put(validator.getServicedCondition(), validator); } } } subjectConfirmationValidators = new LazyMap<>(); if (newConfirmationValidators != null) { for (final SubjectConfirmationValidator validator : newConfirmationValidators) { if (validator != null) { subjectConfirmationValidators.put(validator.getServicedMethod(), validator); } } } statementValidators = new LazyMap<>(); if (newStatementValidators != null) { for (final StatementValidator validator : newStatementValidators) { if (validator != null) { statementValidators.put(validator.getServicedStatement(), validator); } } } trustEngine = newTrustEngine; signaturePrevalidator = newSignaturePrevalidator; } /** * Gets the clock skew from the {@link ValidationContext#getStaticParameters()} parameters. If the parameter is not * set or is not a positive {@link Long} then the {@link #DEFAULT_CLOCK_SKEW} is used. * * @param context current validation context * * @return the clock skew */ public static long getClockSkew(@Nonnull final ValidationContext context) { long clockSkew = DEFAULT_CLOCK_SKEW; if (context.getStaticParameters().containsKey(SAML2AssertionValidationParameters.CLOCK_SKEW)) { try { clockSkew = (Long) context.getStaticParameters().get(SAML2AssertionValidationParameters.CLOCK_SKEW); if (clockSkew < 1) { clockSkew = DEFAULT_CLOCK_SKEW; } } catch (final ClassCastException e) { clockSkew = DEFAULT_CLOCK_SKEW; } } return clockSkew; } /** * Validate the supplied SAML 2 {@link Assertion}, using the parameters from the supplied {@link ValidationContext}. * * @param assertion the assertion being evaluated * @param context the current validation context * * @return the validation result * * @throws AssertionValidationException if there is a fatal error evaluating the validity of the assertion */ @Nonnull public ValidationResult validate(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { log(assertion, context); ValidationResult result = validateVersion(assertion, context); if (result != ValidationResult.VALID) { return result; } result = validateSignature(assertion, context); if (result != ValidationResult.VALID) { return result; } result = validateConditions(assertion, context); if (result != ValidationResult.VALID) { return result; } result = validateSubjectConfirmation(assertion, context); if (result != ValidationResult.VALID) { return result; } return validateStatements(assertion, context); } /** * Log the Assertion which is being validated, along with the supplied validation context parameters. * * @param assertion the SAML 2 Assertion being validated * @param context */ protected void log(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) { if (log.isTraceEnabled()) { try { final Element dom = XMLObjectSupport.marshall(assertion); log.trace("SAML 2 Assertion being validated:\n{}", SerializeSupport.prettyPrintXML(dom)); } catch (final MarshallingException e) { log.error("Unable to marshall SAML 2 Assertion for logging purposes", e); } log.trace("SAML 2 Assertion ValidationContext - static parameters: {}", context.getStaticParameters()); log.trace("SAML 2 Assertion ValidationContext - dynamic parameters: {}", context.getDynamicParameters()); } } /** * Validates that the assertion is a {@link SAMLVersion#VERSION_20} assertion. * * @param assertion the assertion to validate * @param context current validation context * * @return result of the validation evaluation * * @throws AssertionValidationException thrown if there is a problem validating the version */ @Nonnull protected ValidationResult validateVersion(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { if (assertion.getVersion() != SAMLVersion.VERSION_20) { context.setValidationFailureMessage(String.format( "Assertion '%s' is not a SAML 2.0 version Assertion", assertion.getID())); return ValidationResult.INVALID; } return ValidationResult.VALID; } /** * Validates the signature of the assertion, if it is signed. * * @param token assertion whose signature will be validated * @param context current validation context * * @return the result of the signature validation * * @throws AssertionValidationException thrown if there is a problem determining the validity of the signature */ @Nonnull protected ValidationResult validateSignature(@Nonnull final Assertion token, @Nonnull final ValidationContext context) throws AssertionValidationException { Boolean signatureRequired = (Boolean) context.getStaticParameters().get( SAML2AssertionValidationParameters.SIGNATURE_REQUIRED); if (signatureRequired == null) { signatureRequired = Boolean.TRUE; } // Validate params and requirements if (!token.isSigned()) { if (signatureRequired) { context.setValidationFailureMessage("Assertion was required to be signed, but was not"); return ValidationResult.INVALID; } else { log.debug("Assertion was not required to be signed, and was not signed. " + "Skipping further signature evaluation"); return ValidationResult.VALID; } } if (trustEngine == null) { log.warn("Signature validation was necessary, but no signature trust engine was available"); context.setValidationFailureMessage("Assertion signature could not be evaluated due to internal error"); return ValidationResult.INDETERMINATE; } return performSignatureValidation(token, context); } /** * Handle the actual signature validation. * * @param token assertion whose signature will be validated * @param context current validation context * * @return the validation result * * @throws AssertionValidationException thrown if there is a problem determining the validity of the signature */ @Nonnull protected ValidationResult performSignatureValidation(@Nonnull final Assertion token, @Nonnull final ValidationContext context) throws AssertionValidationException { final Signature signature = token.getSignature(); String tokenIssuer = null; if (token.getIssuer() != null) { tokenIssuer = token.getIssuer().getValue(); } log.debug("Attempting signature validation on Assertion '{}' from Issuer '{}'", token.getID(), tokenIssuer); try { signaturePrevalidator.validate(signature); } catch (final SignatureException e) { final String msg = String.format("Assertion Signature failed pre-validation: %s", e.getMessage()); log.warn(msg); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } final CriteriaSet criteriaSet = getSignatureValidationCriteriaSet(token, context); try { if (trustEngine.validate(signature, criteriaSet)) { log.debug("Validation of signature of Assertion '{}' from Issuer '{}' was successful", token.getID(), tokenIssuer); return ValidationResult.VALID; } else { final String msg = String.format( "Signature of Assertion '%s' from Issuer '%s' was not valid", token.getID(), tokenIssuer); log.warn(msg); context.setValidationFailureMessage(msg); return ValidationResult.INVALID; } } catch (final SecurityException e) { final String msg = String.format( "A problem was encountered evaluating the signature over Assertion with ID '%s': %s", token.getID(), e.getMessage()); log.warn(msg); context.setValidationFailureMessage(msg); return ValidationResult.INDETERMINATE; } } /** * Get the criteria set that will be used in evaluating the Assertion signature via the supplied trust engine. * * @param token assertion whose signature will be validated * @param context current validation context * @return the criteria set to use */ @Nonnull protected CriteriaSet getSignatureValidationCriteriaSet(@Nonnull final Assertion token, @Nonnull final ValidationContext context) { CriteriaSet criteriaSet = (CriteriaSet) context.getStaticParameters().get( SAML2AssertionValidationParameters.SIGNATURE_VALIDATION_CRITERIA_SET); if (criteriaSet == null) { criteriaSet = new CriteriaSet(); } if (!criteriaSet.contains(EntityIdCriterion.class)) { String issuer = null; if (token.getIssuer() != null) { issuer = StringSupport.trimOrNull(token.getIssuer().getValue()); } if (issuer != null) { criteriaSet.add(new EntityIdCriterion(issuer)); } } if (!criteriaSet.contains(UsageCriterion.class)) { criteriaSet.add(new UsageCriterion(UsageType.SIGNING)); } return criteriaSet; } /** * Validates the conditions on the assertion. Condition validators are looked up by the element QName and, if * present, the schema type of the condition. If no validator can be found for the Condition the validation process * fails. * * @param assertion the assertion whose conditions will be validated * @param context current validation context * * @return the result of the validation evaluation * * @throws AssertionValidationException thrown if there is a problem determining the validity of the conditions */ @Nonnull protected ValidationResult validateConditions(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { final Conditions conditions = assertion.getConditions(); if (conditions == null) { log.debug("Assertion contained no Conditions element"); return ValidationResult.VALID; } final ValidationResult timeboundsResult = validateConditionsTimeBounds(assertion, context); if (timeboundsResult != ValidationResult.VALID) { return timeboundsResult; } ConditionValidator validator; for (final Condition condition : conditions.getConditions()) { 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.debug(msg); context.setValidationFailureMessage(msg); return ValidationResult.INDETERMINATE; } if (validator.validate(condition, assertion, context) != ValidationResult.VALID) { String msg = String.format( "Condition '%s' of type '%s' in assertion '%s' was not valid.", condition.getElementQName(), condition.getSchemaType(), assertion.getID()); 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 * * @throws AssertionValidationException thrown if there is a problem determining the validity of the conditions */ @Nonnull protected ValidationResult validateConditionsTimeBounds(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { final Conditions conditions = assertion.getConditions(); if (conditions == null) { return ValidationResult.VALID; } final DateTime now = new DateTime(ISOChronology.getInstanceUTC()); final long clockSkew = getClockSkew(context); final DateTime notBefore = conditions.getNotBefore(); log.debug("Evaluating Conditions NotBefore '{}' against 'skewed now' time '{}'", notBefore, now.plus(clockSkew)); if (notBefore != null && notBefore.isAfter(now.plus(clockSkew))) { context.setValidationFailureMessage(String.format( "Assertion '%s' with NotBefore condition of '%s' is not yet valid", assertion.getID(), notBefore)); return ValidationResult.INVALID; } final DateTime notOnOrAfter = conditions.getNotOnOrAfter(); log.debug("Evaluating Conditions NotOnOrAfter '{}' against 'skewed now' time '{}'", notOnOrAfter, now.minus(clockSkew)); if (notOnOrAfter != null && notOnOrAfter.isBefore(now.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 subject confirmations of the assertion. Validators are looked up by the subject confirmation * method. If any one subject confirmation is met the subject is considered confirmed per the SAML specification. * * @param assertion assertion whose subject is being confirmed * @param context current validation context * * @return the result of the validation * * @throws AssertionValidationException thrown if there is a problem determining the validity the subject */ @Nonnull protected ValidationResult validateSubjectConfirmation(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { final Subject assertionSubject = assertion.getSubject(); if (assertionSubject == null) { log.debug("Assertion contains no Subject, skipping subject confirmation"); return ValidationResult.VALID; } final List confirmations = assertionSubject.getSubjectConfirmations(); if (confirmations == null || confirmations.isEmpty()) { log.debug("Assertion contains no SubjectConfirmations, skipping subject confirmation"); return ValidationResult.VALID; } log.debug("Assertion contains at least 1 SubjectConfirmation, proceeding with subject confirmation"); for (final SubjectConfirmation confirmation : confirmations) { final SubjectConfirmationValidator validator = subjectConfirmationValidators.get(confirmation.getMethod()); if (validator != null) { try { if (validator.validate(confirmation, assertion, context) == ValidationResult.VALID) { context.getDynamicParameters().put( SAML2AssertionValidationParameters.CONFIRMED_SUBJECT_CONFIRMATION, confirmation); return ValidationResult.VALID; } } catch (final AssertionValidationException e) { log.warn("Error while executing subject confirmation validation " + validator.getClass().getName(), e); } } } final 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 statements within the assertion. Validators are looked up by the Statement's element QName or, if * present, its schema type. Any statement for which a validator can not be found is simply ignored. * * @param assertion assertion whose statements are being validated * @param context current validation context * * @return result of the validation * * @throws AssertionValidationException thrown if there is a problem determining the validity the statements */ @Nonnull protected ValidationResult validateStatements(@Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { final List statements = assertion.getStatements(); if (statements == null || statements.isEmpty()) { return ValidationResult.VALID; } ValidationResult result; StatementValidator validator; for (final Statement statement : statements) { validator = statementValidators.get(statement.getElementQName()); if (validator == null && statement.getSchemaType() != null) { validator = statementValidators.get(statement.getSchemaType()); } if (validator != null) { result = validator.validate(statement, assertion, context); if (result != ValidationResult.VALID) { return result; } } } return ValidationResult.VALID; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy