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

se.swedenconnect.opensaml.saml2.assertion.validation.AuthnStatementValidator Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016-2024 Sweden Connect
 *
 * 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.swedenconnect.opensaml.saml2.assertion.validation;

import jakarta.annotation.Nonnull;
import net.shibboleth.shared.primitive.DeprecationSupport;
import net.shibboleth.shared.primitive.DeprecationSupport.ObjectType;
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.SAML2AssertionValidationParameters;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.opensaml.saml.saml2.core.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.swedenconnect.opensaml.common.validation.AbstractObjectValidator;
import se.swedenconnect.opensaml.common.validation.CoreValidatorParameters;
import se.swedenconnect.opensaml.common.validation.ValidationSupport;
import se.swedenconnect.opensaml.common.validation.ValidationSupport.ValidationResultException;

import java.time.Duration;
import java.time.Instant;

/**
 * Core statement validator for {@link AuthnStatement}s.
 *
 * 

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

*
    *
  • {@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 #AUTHN_REQUEST_FORCE_AUTHN}: If the above {@link CoreValidatorParameters#AUTHN_REQUEST} is not assigned, * this parameter gives the {@code ForceAuthn} flag. This is used to determine if a valid assertion was issued based on * SSO/non-SSO.
  • *
  • {@link #AUTHN_REQUEST_ISSUE_INSTANT}: If the above {@link CoreValidatorParameters#AUTHN_REQUEST} is not assigned, * this parameter gives the issue instant of the authentication request. This is used to determine if a valid assertion * was issued based on SSO/non-SSO.
  • *
  • {@link #MAX_ACCEPTED_SSO_SESSION_TIME}: For SSO, we may want to assert that the authentication is not too old. If * so, this parameter gives the maximum accepted session time.
  • *
* * @author Martin Lindström ([email protected]) */ public class AuthnStatementValidator extends org.opensaml.saml.saml2.assertion.impl.AuthnStatementValidator { /** * Key for a validation context parameter. Carries a {@link Boolean} holding the value of the ForceAuthn flag from the * AuthnRequest. */ public static final String AUTHN_REQUEST_FORCE_AUTHN = CoreValidatorParameters.STD_PREFIX + ".AuthnRequestForceAuthn"; /** * Key for a validation context parameter. Carries a {@link Instant} holding the issuance time for the AuthnRequest. */ public static final String AUTHN_REQUEST_ISSUE_INSTANT = CoreValidatorParameters.STD_PREFIX + ".AuthnRequestIssueInstant"; /** * Key for a validation context parameter. Carries a {@link Duration} holding the maximum session time that we can * accept for SSO. */ public static final String MAX_ACCEPTED_SSO_SESSION_TIME = CoreValidatorParameters.STD_PREFIX + ".MaxAcceptedSsoSessionTime"; /** Class logger. */ private static final Logger log = LoggerFactory.getLogger(AuthnStatementValidator.class); /** {@inheritDoc} */ @Override @Nonnull public final ValidationResult validate(@Nonnull final Statement statement, @Nonnull final Assertion assertion, @Nonnull final ValidationContext context) throws AssertionValidationException { if (statement instanceof AuthnStatement) { return this.validate((AuthnStatement) statement, assertion, context); } else { throw new AssertionValidationException( "Illegal call - statement is of type " + statement.getClass().getSimpleName()); } } /** * Validates the {@link AuthnStatement}. * * @param statement the statement to validate * @param assertion the assertion containing the statement * @param context validation context * @return validation result * @throws AssertionValidationException for internal validation errors */ protected ValidationResult validate(final AuthnStatement statement, final Assertion assertion, final ValidationContext context) throws AssertionValidationException { try { ValidationSupport.check(this.validateAuthnInstant(statement, assertion, context)); ValidationSupport.check(this.validateSessionIndex(statement, assertion, context)); ValidationSupport.check(this.validateSessionNotOnOrAfter(statement, assertion, context)); ValidationSupport.check(this.validateSubjectLocality(statement, assertion, context)); ValidationSupport.check(this.validateAuthnContext(statement, assertion, context)); } catch (final AssertionValidationException e) { log.warn("Error during determining AuthnStatement validity", e); context.getValidationFailureMessages().add("AuthnStatement validation failure - " + e.getMessage()); return ValidationResult.INDETERMINATE; } catch (final ValidationResultException e) { return e.getResult(); } return ValidationResult.VALID; } /** * Validates the {@code AuthnInstant} of the {@code AuthnStatement}. * * @param statement the statement * @param assertion the assertion containing the statement * @param context validation context * @return validation result */ @Override @Nonnull protected ValidationResult validateAuthnInstant(@Nonnull final AuthnStatement statement, @Nonnull final Assertion assertion, @Nonnull final ValidationContext context) { if (statement.getAuthnInstant() == null) { context.getValidationFailureMessages().add("AuthnInstant of Assertion/@AuthnStatement is missing"); return ValidationResult.INVALID; } // Assert the authentication instant is not newer than the assertion issuance time. // if (statement.getAuthnInstant().isAfter(assertion.getIssueInstant())) { context.getValidationFailureMessages().add("AuthnInstant is after assertion issue instant - invalid"); return ValidationResult.INVALID; } // Make checks regarding SSO and session length ... // return this.validateSsoAndSession(statement.getAuthnInstant(), statement, assertion, context); } /** * Makes checks for SSO and session lengths. * * @param authnInstant the authentication instant * @param statement the statement * @param assertion the assertion containing the statement * @param context validation context * @return validation result */ protected ValidationResult validateSsoAndSession(final Instant authnInstant, final AuthnStatement statement, final Assertion assertion, final ValidationContext context) { // If we requested a forced authentication, we check that the authentication instant is not before // the issuance time of the request. // final Boolean forceAuthn = getForceAuthnFlag(context); final Instant authnRequestIssueInstant = getAuthnRequestIssueInstant(context); final Duration clockSkew = AbstractObjectValidator.getAllowedClockSkew(context); if (forceAuthn != null && forceAuthn) { if (authnRequestIssueInstant != null) { if (authnInstant.plus(clockSkew).isBefore(authnRequestIssueInstant)) { final String msg = String.format("Invalid Assertion. Force authentication was requested, but authentication " + "instant (%s) is before the issuance time of the authentication request (%s)", authnInstant, authnRequestIssueInstant); context.getValidationFailureMessages().add(msg); return ValidationResult.INVALID; } } else { log.warn("{} (or {}) not suppplied - cannot check SSO", AUTHN_REQUEST_ISSUE_INSTANT, CoreValidatorParameters.AUTHN_REQUEST); } } else { // Forced authentication was not requested. // If we have specified the MAX_ACCEPTED_SSO_SESSION_TIME parameter we make a check that the // SSO session at the issuing IdP is not greater than what we can accept. // final Duration maxSessionTime = getMaxAcceptedSsoSessionTime(context); if (maxSessionTime != null) { if (authnInstant.plus(maxSessionTime).isBefore(AbstractObjectValidator.getReceiveInstant(context))) { final String msg = String.format( "Session length violation. Authentication instant (%s) is too far back in time to be accepted by SP SSO policy", authnInstant); context.getValidationFailureMessages().add(msg); return ValidationResult.INVALID; } } } // From OpenSAML's implementation ... // final Duration maxTimeSinceAuthn = (Duration) context.getStaticParameters().get(SAML2AssertionValidationParameters.STMT_AUTHN_MAX_TIME); if (maxTimeSinceAuthn != null) { final Instant latestValid = authnInstant.plus(maxTimeSinceAuthn).plus(clockSkew); final Instant receiveInstant = AbstractObjectValidator.getReceiveInstant(context); if (receiveInstant.isAfter(latestValid)) { final String msg = String.format( "AuthnStatement/@AuthnInstant '%s' eval failed, now is after latest valid (including skew) '%s'", authnInstant, latestValid); context.getValidationFailureMessages().add(msg); return ValidationResult.INVALID; } } return ValidationResult.VALID; } /** * Gets the maximum time we allow for SSO sessions. * * @param context the validation context * @return the max time, or null if the time is not set */ protected static Duration getMaxAcceptedSsoSessionTime(final ValidationContext context) { final Object object = context.getStaticParameters().get(MAX_ACCEPTED_SSO_SESSION_TIME); if (object != null) { if (object instanceof Duration) { return (Duration) object; } else if (object instanceof Long) { DeprecationSupport.warn(ObjectType.CONFIGURATION, AUTHN_REQUEST_ISSUE_INSTANT, null, Duration.class.getName()); return Duration.ofMillis((Long) object); } } return null; } /** * Gets the {@code ForceAuthn} flag from the validation context. The method primarily checks for the * {@link #AUTHN_REQUEST_FORCE_AUTHN} parameter, and that does not exist, tries with the * {@link CoreValidatorParameters#AUTHN_REQUEST} parameter. * * @param context the validation context * @return the {@code ForceAuthn} flag or {@code null} if this is not set */ protected static Boolean getForceAuthnFlag(final ValidationContext context) { Boolean forceAuthn = (Boolean) context.getStaticParameters().get(AUTHN_REQUEST_FORCE_AUTHN); if (forceAuthn == null) { final AuthnRequest authnRequest = (AuthnRequest) context.getStaticParameters().get(CoreValidatorParameters.AUTHN_REQUEST); if (authnRequest != null) { forceAuthn = authnRequest.isForceAuthn(); } } return forceAuthn; } /** * Gets the issue instant of the {@code AuthnRequest} from the validation context. The method primarily checks for the * {@link #AUTHN_REQUEST_ISSUE_INSTANT} parameter, and that does not exist, tries with the * {@link CoreValidatorParameters#AUTHN_REQUEST} parameter. * * @param context the validation context * @return the issuance time or null if not set */ protected static Instant getAuthnRequestIssueInstant(final ValidationContext context) { final Object object = context.getStaticParameters().get(AUTHN_REQUEST_ISSUE_INSTANT); if (object != null) { if (object instanceof Instant) { return (Instant) object; } else if (object instanceof Long) { DeprecationSupport.warn(ObjectType.CONFIGURATION, AUTHN_REQUEST_ISSUE_INSTANT, null, Instant.class.getName()); return Instant.ofEpochMilli((Long) object); } } final AuthnRequest authnRequest = (AuthnRequest) context.getStaticParameters().get(CoreValidatorParameters.AUTHN_REQUEST); if (authnRequest != null) { return authnRequest.getIssueInstant(); } return null; } /** * Default implementation does not perform any checks and returns {@link ValidationResult#VALID}. * * @param statement the statement * @param assertion the assertion * @param context the validation context * @return validation result */ protected ValidationResult validateSessionIndex(final AuthnStatement statement, final Assertion assertion, final ValidationContext context) { return ValidationResult.VALID; } /** * Default implementation does not perform any checks and returns {@link ValidationResult#VALID}. * * @param statement the statement * @param assertion the assertion * @param context the validation context * @return validation result */ protected ValidationResult validateSessionNotOnOrAfter(final AuthnStatement statement, final Assertion assertion, final ValidationContext context) { return ValidationResult.VALID; } /** * Default implementation will only assert that the {@code AuthnContext} element is present. * * @param statement the statement * @param assertion the assertion * @param context the validation context * @return validation result */ @Override @Nonnull protected ValidationResult validateAuthnContext(@Nonnull final AuthnStatement statement, @Nonnull final Assertion assertion, @Nonnull final ValidationContext context) { if (statement.getAuthnContext() == null) { context.getValidationFailureMessages().add("AuthnContext element is missing in Assertion/@AuthnStatement"); return ValidationResult.INVALID; } return ValidationResult.VALID; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy