/*
 * Copyright 2004-present the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.security.saml2.provider.service.authentication;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import javax.xml.namespace.QName;

import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
import org.opensaml.saml.common.SignableSAMLObject;
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.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AttributeValue;
import org.opensaml.saml.saml2.core.AudienceRestriction;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute;
import org.opensaml.saml.saml2.core.EncryptedID;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.OneTimeUse;
import org.opensaml.saml.saml2.core.ProxyRestriction;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Status;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml.saml2.core.impl.ProxyRestrictionBuilder;
import org.opensaml.saml.saml2.core.impl.StatusBuilder;
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import tools.jackson.databind.json.JsonMapper;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.SecurityAssertions;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.jackson.SecurityJacksonModules;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.AssertionValidator;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSaml5Objects.CustomOpenSamlObject;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.util.StringUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

/**
 * Tests for {@link OpenSaml5AuthenticationProvider}
 *
 * @author Filip Hanik
 * @author Josh Cummings
 */
public class OpenSaml5AuthenticationProviderTests {

	private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";

	private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";

	private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";

	private final OpenSamlOperations saml = new OpenSaml5Template();

	private OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();

	private Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("name",
			Collections.emptyMap());

	private Saml2Authentication authentication = new Saml2Authentication(this.principal, "response",
			Collections.emptyList());

	@Test
	public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() {
		assertThat(this.provider.supports(Saml2AuthenticationToken.class))
			.withFailMessage(OpenSaml5AuthenticationProvider.class + "should support " + Saml2AuthenticationToken.class)
			.isTrue();
	}

	@Test
	public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() {
		assertThat(!this.provider.supports(Authentication.class))
			.withFailMessage(OpenSaml5AuthenticationProvider.class + "should not support " + Authentication.class)
			.isTrue();
	}

	@Test
	public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() {
		Assertion assertion = (Assertion) XMLObjectProviderRegistrySupport.getBuilderFactory()
			.getBuilder(Assertion.DEFAULT_ELEMENT_NAME)
			.buildObject(Assertion.DEFAULT_ELEMENT_NAME);
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider
				.authenticate(new Saml2AuthenticationToken(verifying(registration()).build(), serialize(assertion))))
			.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
	}

	@Test
	public void authenticateWhenXmlErrorThenThrowAuthenticationException() {
		Saml2AuthenticationToken token = new Saml2AuthenticationToken(verifying(registration()).build(), "invalid xml");
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
	}

	@Test
	public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
		Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
		response.getAssertions().add(assertion());
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.INVALID_DESTINATION));
	}

	@Test
	public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() {
		Saml2AuthenticationToken token = token();
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response."));
	}

	@Test
	public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
		Response response = response();
		response.getAssertions().add(assertion());
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE));
	}

	@Test
	public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() {
		Response response = response();
		Assertion assertion = assertion();
		assertion.getSubject()
			.getSubjectConfirmations()
			.get(0)
			.getSubjectConfirmationData()
			.setNotOnOrAfter(Instant.now().minus(Duration.ofDays(3)));
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.INVALID_ASSERTION));
	}

	@Test
	public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
		Response response = response();
		Assertion assertion = assertion();
		assertion.setSubject(null);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND));
	}

	@Test
	public void authenticateWhenUsernameMissingThenThrowAuthenticationException() {
		Response response = response();
		Assertion assertion = assertion();
		assertion.getSubject().getNameID().setValue(null);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND));
	}

	@Test
	public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() {
		Response response = response();
		Assertion assertion = assertion();
		assertion.getSubject()
			.getSubjectConfirmations()
			.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		this.provider.authenticate(token);
	}

	@Test
	public void evaluateInResponseToSucceedsWhenInResponseToInResponseAndAssertionsMatchRequestID() {
		Response response = response();
		response.setInResponseTo("SAML2");
		response.getAssertions().add(signed(assertion("SAML2")));
		response.getAssertions().add(signed(assertion("SAML2")));
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2");
		Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
		this.provider.authenticate(token);
	}

	@Test
	public void evaluateInResponseToSucceedsWhenInResponseToInAssertionOnlyMatchRequestID() {
		Response response = response();
		response.getAssertions().add(signed(assertion()));
		response.getAssertions().add(signed(assertion("SAML2")));
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2");
		Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
		this.provider.authenticate(token);
	}

	@Test
	public void evaluateInResponseToFailsWhenInResponseToInAssertionMismatchWithRequestID() {
		Response response = response();
		response.setInResponseTo("SAML2");
		response.getAssertions().add(signed(assertion("SAML2")));
		response.getAssertions().add(signed(assertion("BAD")));
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2");
		Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.withStackTraceContaining("invalid_assertion");
	}

	@Test
	public void evaluateInResponseToFailsWhenInResponseToInAssertionOnlyAndMismatchWithRequestID() {
		Response response = response();
		response.getAssertions().add(signed(assertion()));
		response.getAssertions().add(signed(assertion("BAD")));
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2");
		Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.withStackTraceContaining("invalid_assertion");
	}

	@Test
	public void evaluateInResponseToFailsWhenInResponseInToResponseMismatchWithRequestID() {
		Response response = response();
		response.setInResponseTo("BAD");
		response.getAssertions().add(signed(assertion("SAML2")));
		response.getAssertions().add(signed(assertion("SAML2")));
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2");
		Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.withStackTraceContaining("invalid_in_response_to");
	}

	@Test
	public void evaluateInResponseToFailsWhenInResponseToInResponseButNoSavedRequest() {
		Response response = response();
		response.setInResponseTo("BAD");
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.withStackTraceContaining("invalid_in_response_to");
	}

	@Test
	public void evaluateInResponseToSucceedsWhenNoInResponseToInResponseOrAssertions() {
		Response response = response();
		response.getAssertions().add(signed(assertion()));
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2");
		Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
		this.provider.authenticate(token);
	}

	@Test
	public void authenticateWhenAssertionContainsAttributesThenItSucceeds() {
		Response response = response();
		Assertion assertion = assertion();
		List<AttributeStatement> attributes = attributeStatements();
		assertion.getAttributeStatements().addAll(attributes);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		Authentication authentication = this.provider.authenticate(token);
		Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
		Map<String, Object> expected = new LinkedHashMap<>();
		expected.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com"));
		expected.put("name", Collections.singletonList("John Doe"));
		expected.put("age", Collections.singletonList(21));
		expected.put("website", Collections.singletonList("https://johndoe.com/"));
		expected.put("registered", Collections.singletonList(true));
		Instant registeredDate = Instant.parse("1970-01-01T00:00:00Z");
		expected.put("registeredDate", Collections.singletonList(registeredDate));
		expected.put("role", Arrays.asList("RoleOne", "RoleTwo")); // gh-11042
		assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe");
		assertThat(principal.getAttributes()).isEqualTo(expected);
		assertThat(principal.getSessionIndexes()).contains("session-index");
	}

	// gh-11785
	@Test
	public void deserializeWhenAssertionContainsAttributesThenWorks() throws Exception {
		ClassLoader loader = getClass().getClassLoader();
		JsonMapper mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build();
		Response response = response();
		Assertion assertion = assertion();
		List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
		assertion.getAttributeStatements().addAll(attributes);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		Authentication authentication = this.provider.authenticate(token);
		String result = mapper.writeValueAsString(authentication);
		mapper.readValue(result, Authentication.class);
	}

	@Test
	public void authenticateWhenAssertionContainsCustomAttributesThenItSucceeds() {
		Response response = response();
		Assertion assertion = assertion();
		AttributeStatement attribute = TestOpenSamlObjects.customAttributeStatement("Address",
				TestCustomOpenSaml5Objects.instance());
		assertion.getAttributeStatements().add(attribute);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		Authentication authentication = this.provider.authenticate(token);
		Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
		CustomOpenSamlObject address = (CustomOpenSamlObject) principal.getAttribute("Address").get(0);
		assertThat(address.getStreet()).isEqualTo("Test Street");
		assertThat(address.getStreetNumber()).isEqualTo("1");
		assertThat(address.getZIP()).isEqualTo("11111");
		assertThat(address.getCity()).isEqualTo("Test City");
	}

	@Test
	public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() {
		Response response = response();
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE, "Did not decrypt response"));
	}

	@Test
	public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() {
		Response response = response();
		Assertion assertion = TestOpenSamlObjects.signed(assertion(),
				TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration())));
		this.provider.authenticate(token);
	}

	// gh-16367
	@Test
	public void authenticateWhenEncryptedAssertionWithSignatureThenEncryptedAssertionStillAvailable() {
		Response response = response();
		Assertion assertion = TestOpenSamlObjects.signed(assertion(),
				TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration())));
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setResponseValidator((t) -> {
			assertThat(t.getResponse().getEncryptedAssertions()).isNotEmpty();
			return Saml2ResponseValidatorResult.success();
		});
		provider.authenticate(token);
	}

	@Test
	public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() {
		Response response = response();
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration())));
		this.provider.authenticate(token);
	}

	@Test
	public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() {
		Response response = response();
		Assertion assertion = assertion();
		NameID nameId = assertion.getSubject().getNameID();
		EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		assertion.getSubject().setNameID(null);
		assertion.getSubject().setEncryptedID(encryptedID);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
		this.provider.authenticate(token);
	}

	// gh-16367
	@Test
	public void authenticateWhenEncryptedNameIdWithSignatureThenEncryptedNameIdStillAvailable() {
		Response response = response();
		Assertion assertion = assertion();
		NameID nameId = assertion.getSubject().getNameID();
		EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		assertion.getSubject().setNameID(null);
		assertion.getSubject().setEncryptedID(encryptedID);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setAssertionValidator((t) -> {
			assertThat(t.getAssertion().getSubject().getEncryptedID()).isNotNull();
			return Saml2ResponseValidatorResult.success();
		});
		provider.authenticate(token);
	}

	@Test
	public void authenticateWhenEncryptedAttributeThenDecrypts() {
		Response response = response();
		Assertion assertion = assertion();
		EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
		statement.getEncryptedAttributes().add(attribute);
		assertion.getAttributeStatements().add(statement);
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration())));
		Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token);
		Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
		assertThat(principal.getAttribute("name")).containsExactly("value");
	}

	// gh-16367
	@Test
	public void authenticateWhenEncryptedAttributeThenEncryptedAttributesStillAvailable() {
		Response response = response();
		Assertion assertion = assertion();
		EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
		statement.getEncryptedAttributes().add(attribute);
		assertion.getAttributeStatements().add(statement);
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration())));
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setAssertionValidator((t) -> {
			assertThat(t.getAssertion().getAttributeStatements().get(0).getEncryptedAttributes()).isNotEmpty();
			return Saml2ResponseValidatorResult.success();
		});
		provider.authenticate(token);
	}

	@Test
	public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() {
		Response response = response();
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData"));
	}

	@Test
	public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() {
		Response response = response();
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(signed(response), registration()
			.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyPrivateCredential())));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData"));
	}

	@Test
	public void authenticateWhenAuthenticationHasDetailsThenSucceeds() {
		Response response = response();
		Assertion assertion = assertion();
		assertion.getSubject()
			.getSubjectConfirmations()
			.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		token.setDetails("some-details");
		Authentication authentication = this.provider.authenticate(token);
		assertThat(authentication.getDetails()).isEqualTo("some-details");
	}

	@Test
	public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
		Response response = response();
		Assertion assertion = TestOpenSamlObjects.signed(assertion(),
				TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
		EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
				TestSaml2X509Credentials.assertingPartyEncryptingCredential());
		response.getEncryptedAssertions().add(encryptedAssertion);
		Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration())));
		Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token);
		// the following code will throw an exception if authentication isn't serializable
		ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
		objectOutputStream.writeObject(authentication);
		objectOutputStream.flush();
	}

	@Test
	public void createDefaultAssertionValidatorWhenAssertionThenValidates() {
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
		Assertion assertion = response.getAssertions().get(0);
		OpenSaml5AuthenticationProvider.AssertionToken assertionToken = new OpenSaml5AuthenticationProvider.AssertionToken(
				assertion, token());
		assertThat(
				OpenSaml5AuthenticationProvider.createDefaultAssertionValidator().convert(assertionToken).hasErrors())
			.isFalse();
	}

	@Test
	public void authenticateWhenDelegatingToDefaultAssertionValidatorThenUses() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		// @formatter:off
		provider.setAssertionValidator((assertionToken) -> OpenSaml5AuthenticationProvider
				.createDefaultAssertionValidator((token) -> new ValidationContext())
				.convert(assertionToken)
				.concat(new Saml2Error("wrong error", "wrong error"))
		);
		// @formatter:on
		Response response = response();
		Assertion assertion = assertion();
		OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
		assertion.getConditions().getConditions().add(oneTimeUse);
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		// @formatter:off
		assertThatExceptionOfType(Saml2AuthenticationException.class)
				.isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class)
				.satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_ASSERTION));
		// @formatter:on
	}

	// gh-11675
	@Test
	public void authenticateWhenUsingCustomAssertionValidatorThenUses() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		Consumer<Map<String, Object>> validationParameters = mock(Consumer.class);
		// @formatter:off
		provider.setAssertionValidator(OpenSaml5AuthenticationProvider
				.createDefaultAssertionValidatorWithParameters(validationParameters));
		// @formatter:on
		Response response = response();
		Assertion assertion = assertion();
		OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
		assertion.getConditions().getConditions().add(oneTimeUse);
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		provider.authenticate(token);
		verify(validationParameters).accept(any());
	}

	@Test
	public void authenticateWhenCustomAssertionValidatorThenUses() {
		Converter<OpenSaml5AuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> validator = mock(
				Converter.class);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		// @formatter:off
		provider.setAssertionValidator((assertionToken) -> OpenSaml5AuthenticationProvider.createDefaultAssertionValidator()
				.convert(assertionToken)
				.concat(validator.convert(assertionToken))
		);
		// @formatter:on
		Response response = response();
		Assertion assertion = assertion();
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		given(validator.convert(any(OpenSaml5AuthenticationProvider.AssertionToken.class)))
			.willReturn(Saml2ResponseValidatorResult.success());
		provider.authenticate(token);
		verify(validator).convert(any(OpenSaml5AuthenticationProvider.AssertionToken.class));
	}

	@Test
	public void authenticateWhenAssertionValidatorListThenUses() throws Exception {
		ConditionValidator custom = mock(ConditionValidator.class);
		given(custom.getServicedCondition()).willReturn(AudienceRestriction.DEFAULT_ELEMENT_NAME);
		given(custom.validate(any(), any(), any())).willReturn(ValidationResult.INVALID);
		AssertionValidator validator = AssertionValidator.builder().conditionValidators((c) -> c.add(custom)).build();
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setAssertionValidator(validator);
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion((r) -> r.getAssertions()
			.get(0)
			.getConditions()
			.getConditions()
			.add(build(AudienceRestriction.DEFAULT_ELEMENT_NAME)));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token))
			.withMessageContaining("AudienceRestriction");
		verify(custom).validate(any(), any(), any());
	}

	@Test
	public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success());
		Response response = response();
		Assertion assertion = assertion();
		TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(),
				RELYING_PARTY_ENTITY_ID); // broken
		// signature
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		// @formatter:off
		assertThatExceptionOfType(Saml2AuthenticationException.class)
				.isThrownBy(() -> provider.authenticate(token))
				.satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE));
		// @formatter:on
	}

	@Test
	public void authenticateWhenValidationContextCustomizedThenUsers() {
		Map<String, Object> parameters = new HashMap<>();
		parameters.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton("blah"));
		ValidationContext context = mock(ValidationContext.class);
		given(context.getStaticParameters()).willReturn(parameters);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setAssertionValidator(
				OpenSaml5AuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context));
		Response response = response();
		Assertion assertion = assertion();
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		// @formatter:off
		assertThatExceptionOfType(Saml2AuthenticationException.class)
				.isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class)
				.satisfies((error) -> assertThat(error).hasMessageContaining("Invalid assertion"));
		// @formatter:on
		verify(context, atLeastOnce()).getStaticParameters();
	}

	@Test
	public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception {
		Response response = response();
		Assertion assertion = TestOpenSamlObjects.signed(assertion(),
				TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID,
				SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		this.provider.authenticate(token);
	}

	@Test
	public void setAssertionValidatorWhenNullThenIllegalArgument() {
		// @formatter:off
		assertThatIllegalArgumentException()
				.isThrownBy(() -> this.provider.setAssertionValidator(null));
		// @formatter:on
	}

	@Test
	public void createDefaultResponseAuthenticationConverterWhenResponseThenConverts() {
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		ResponseToken responseToken = new ResponseToken(response, token);
		Saml2Authentication authentication = OpenSaml5AuthenticationProvider
			.createDefaultResponseAuthenticationConverter()
			.convert(responseToken);
		assertThat(authentication.getName()).isEqualTo("test@saml.user");
	}

	@Test
	public void authenticateWhenResponseAuthenticationConverterConfiguredThenUses() {
		Converter<ResponseToken, Saml2Authentication> authenticationConverter = mock(Converter.class);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setResponseAuthenticationConverter(authenticationConverter);
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		provider.authenticate(token);
		verify(authenticationConverter).convert(any());
	}

	@Test
	public void authenticateWhenResponseAuthenticationConverterComponentConfiguredThenUses() {
		Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter = mock(Converter.class);
		given(grantedAuthoritiesConverter.convert(any())).willReturn(AuthorityUtils.createAuthorityList("CUSTOM"));
		ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
		authenticationConverter.setGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setResponseAuthenticationConverter(authenticationConverter);
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		Authentication authentication = provider.authenticate(token);
		SecurityAssertions.assertThat(authentication).hasAuthority("CUSTOM");
		verify(grantedAuthoritiesConverter).convert(any());
	}

	@Test
	public void authenticateWhenValidateResponseAfterAssertionsThenCanHaveResponseAuthenticationConverterThatDoesntNeedANameID() {
		Converter<ResponseToken, Saml2Authentication> responseAuthenticationConverter = mock(Converter.class);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setValidateResponseAfterAssertions(true);
		provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
		Response response = TestOpenSamlObjects
			.signedResponseWithOneAssertion((r) -> r.getAssertions().get(0).setSubject(null));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		provider.authenticate(token);
		verify(responseAuthenticationConverter).convert(any());
	}

	@Test
	public void authenticateWhenValidateResponseBeforeAssertionsThenMustHaveNameID() {
		Converter<ResponseToken, Saml2Authentication> responseAuthenticationConverter = mock(Converter.class);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setValidateResponseAfterAssertions(false);
		provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
		Response response = TestOpenSamlObjects
			.signedResponseWithOneAssertion((r) -> r.getAssertions().get(0).setSubject(null));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token));
		verifyNoInteractions(responseAuthenticationConverter);
	}

	@Test
	public void setResponseAuthenticationConverterWhenNullThenIllegalArgument() {
		// @formatter:off
		assertThatIllegalArgumentException()
				.isThrownBy(() -> this.provider.setResponseAuthenticationConverter(null));
		// @formatter:on
	}

	@Test
	public void setResponseElementsDecrypterWhenNullThenIllegalArgument() {
		assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setResponseElementsDecrypter(null));
	}

	@Test
	public void setAssertionElementsDecrypterWhenNullThenIllegalArgument() {
		assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setAssertionElementsDecrypter(null));
	}

	@Test
	public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() {
		Response response = response();
		Assertion assertion = assertion();
		response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject());
		TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
				RELYING_PARTY_ENTITY_ID);
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		this.provider
			.setResponseElementsDecrypter((tuple) -> tuple.getResponse().getAssertions().add(signed(assertion)));
		Authentication authentication = this.provider.authenticate(token);
		assertThat(authentication.getName()).isEqualTo("test@saml.user");
	}

	@Test
	public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() {
		Response response = response();
		Assertion assertion = assertion();
		EncryptedID id = new EncryptedIDBuilder().buildObject();
		id.setEncryptedData(new EncryptedDataBuilder().buildObject());
		assertion.getSubject().setEncryptedID(id);
		response.getAssertions().add(signed(assertion));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		this.provider.setAssertionElementsDecrypter((tuple) -> {
			NameID name = new NameIDBuilder().buildObject();
			name.setValue("decrypted name");
			tuple.getAssertion().getSubject().setNameID(name);
		});
		Authentication authentication = this.provider.authenticate(token);
		assertThat(authentication.getName()).isEqualTo("decrypted name");
	}

	@Test
	public void authenticateWhenResponseStatusIsNotSuccessThenFails() {
		Response response = TestOpenSamlObjects
			.signedResponseWithOneAssertion((r) -> r.setStatus(TestOpenSamlObjects.status(StatusCode.AUTHN_FAILED)));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class)
			.isThrownBy(() -> this.provider.authenticate(token))
			.satisfies(errorOf(Saml2ErrorCodes.INVALID_RESPONSE, "Invalid status"));
	}

	@Test
	public void authenticateWhenResponseStatusIsSuccessThenSucceeds() {
		Response response = TestOpenSamlObjects
			.signedResponseWithOneAssertion((r) -> r.setStatus(TestOpenSamlObjects.successStatus()));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		Authentication authentication = this.provider.authenticate(token);
		assertThat(authentication.getName()).isEqualTo("test@saml.user");
	}

	@Test
	public void setResponseValidatorWhenNullThenIllegalArgument() {
		assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setResponseValidator(null));
	}

	@Test
	public void authenticateWhenCustomResponseValidatorThenUses() {
		Converter<OpenSaml5AuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> validator = mock(
				Converter.class);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		// @formatter:off
		provider.setResponseValidator((responseToken) -> OpenSaml5AuthenticationProvider.createDefaultResponseValidator()
				.convert(responseToken)
				.concat(validator.convert(responseToken))
		);
		// @formatter:on
		Response response = response();
		Assertion assertion = assertion();
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		given(validator.convert(any(OpenSaml5AuthenticationProvider.ResponseToken.class)))
			.willReturn(Saml2ResponseValidatorResult.success());
		provider.authenticate(token);
		verify(validator).convert(any(OpenSaml5AuthenticationProvider.ResponseToken.class));
	}

	@Test
	public void authenticateWhenCustomSetOfResponseValidatorsThenUses() {
		Converter<OpenSaml5AuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> validator = mock(
				Converter.class);
		given(validator.convert(any()))
			.willReturn(Saml2ResponseValidatorResult.failure(new Saml2Error("error", "description")));
		ResponseValidator responseValidator = new ResponseValidator(validator);
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setResponseValidator(responseValidator);
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token))
			.withMessageContaining("description");
		verify(validator).convert(any());
	}

	@Test
	public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatusCodes() {
		Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token();

		Status parentStatus = new StatusBuilder().buildObject();
		StatusCode parentStatusCode = new StatusCodeBuilder().buildObject();
		parentStatusCode.setValue(StatusCode.AUTHN_FAILED);
		StatusCode childStatusCode = new StatusCodeBuilder().buildObject();
		childStatusCode.setValue(StatusCode.NO_PASSIVE);
		parentStatusCode.setStatusCode(childStatusCode);
		parentStatus.setStatusCode(parentStatusCode);

		Response response = TestOpenSamlObjects.response();
		response.setStatus(parentStatus);
		response.setIssuer(TestOpenSamlObjects.issuer("mockedIssuer"));

		Converter<ResponseToken, Saml2ResponseValidatorResult> validator = OpenSaml5AuthenticationProvider
			.createDefaultResponseValidator();
		Saml2ResponseValidatorResult result = validator.convert(new ResponseToken(response, token));

		String expectedErrorMessage = String.format("Invalid status [%s] for SAML response",
				parentStatusCode.getValue());
		assertThat(
				result.getErrors().stream().anyMatch((error) -> error.getDescription().contains(expectedErrorMessage)))
			.isTrue();
		assertThat(result.getErrors()
			.stream()
			.noneMatch((error) -> error.getDescription().contains(childStatusCode.getValue()))).isTrue();
	}

	@Test
	public void authenticateWhenResponseStatusIsNotSuccessThenReturnParentAndChildStatusCode() {
		Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token();
		Status parentStatus = new StatusBuilder().buildObject();
		StatusCode parentStatusCode = new StatusCodeBuilder().buildObject();
		parentStatusCode.setValue(StatusCode.REQUESTER);
		StatusCode childStatusCode = new StatusCodeBuilder().buildObject();
		childStatusCode.setValue(StatusCode.NO_PASSIVE);
		parentStatusCode.setStatusCode(childStatusCode);
		parentStatus.setStatusCode(parentStatusCode);

		Response response = TestOpenSamlObjects.response();
		response.setStatus(parentStatus);
		response.setIssuer(TestOpenSamlObjects.issuer("mockedIssuer"));

		Converter<ResponseToken, Saml2ResponseValidatorResult> validator = OpenSaml5AuthenticationProvider
			.createDefaultResponseValidator();
		Saml2ResponseValidatorResult result = validator.convert(new ResponseToken(response, token));

		String expectedParentErrorMessage = String.format("Invalid status [%s] for SAML response",
				parentStatusCode.getValue());
		String expectedChildErrorMessage = String.format("Invalid status [%s] for SAML response",
				childStatusCode.getValue());
		assertThat(result.getErrors()
			.stream()
			.anyMatch((error) -> error.getDescription().contains(expectedParentErrorMessage))).isTrue();
		assertThat(result.getErrors()
			.stream()
			.anyMatch((error) -> error.getDescription().contains(expectedChildErrorMessage))).isTrue();
	}

	@Test
	public void authenticateWhenAssertionIssuerNotValidThenFailsWithInvalidIssuer() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		Response response = response();
		Assertion assertion = assertion();
		assertion.setIssuer(TestOpenSamlObjects.issuer("https://invalid.idp.test/saml2/idp"));
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token))
			.withMessageContaining("did not match any valid issuers");
	}

	// gh-14931
	@Test
	public void authenticateWhenAssertionHasProxyRestrictionThenParses() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		Response response = response();
		Assertion assertion = assertion();
		ProxyRestriction condition = new ProxyRestrictionBuilder().buildObject();
		assertion.getConditions().getConditions().add(condition);
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		provider.authenticate(token);
	}

	// gh-15022
	@Test
	public void authenticateWhenClockSkewThenVerifiesSignature() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		provider.setAssertionValidator(OpenSaml5AuthenticationProvider.createDefaultAssertionValidatorWithParameters(
				(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(10))));
		Response response = response();
		Assertion assertion = assertion();
		assertion.setIssueInstant(Instant.now().plus(Duration.ofMinutes(9)));
		response.getAssertions().add(assertion);
		Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
		provider.authenticate(token);
	}

	// gh-16989
	@Test
	public void authenticateWhenNullIssuerThenNoNullPointer() {
		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion((r) -> r.setIssuer(null));
		Saml2AuthenticationToken token = token(response, verifying(registration()));
		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token));
	}

	@Test
	public void authenticateWhenSuccessThenIssuesFactor() {
		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
		Authentication request = token(response, verifying(registration()));
		Authentication result = this.provider.authenticate(request);
		SecurityAssertions.assertThat(result).hasAuthority(FactorGrantedAuthority.SAML_RESPONSE_AUTHORITY);
	}

	private <T extends XMLObject> T build(QName qName) {
		return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
	}

	private String serialize(XMLObject object) {
		return this.saml.serialize(object).serialize();
	}

	private Consumer<Saml2AuthenticationException> errorOf(String errorCode) {
		return errorOf(errorCode, null);
	}

	private Consumer<Saml2AuthenticationException> errorOf(String errorCode, String description) {
		return (ex) -> {
			assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(errorCode);
			if (StringUtils.hasText(description)) {
				assertThat(ex.getSaml2Error().getDescription()).contains(description);
			}
		};
	}

	private Response response() {
		Response response = TestOpenSamlObjects.response();
		response.setIssueInstant(Instant.now());
		return response;
	}

	private Response response(String destination, String issuerEntityId) {
		Response response = TestOpenSamlObjects.response(destination, issuerEntityId);
		response.setIssueInstant(Instant.now());
		return response;
	}

	private Assertion assertion(String inResponseTo) {
		Assertion assertion = TestOpenSamlObjects.assertion();
		assertion.setIssueInstant(Instant.now());
		for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
			SubjectConfirmationData data = confirmation.getSubjectConfirmationData();
			data.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000)));
			data.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000)));
			if (StringUtils.hasText(inResponseTo)) {
				data.setInResponseTo(inResponseTo);
			}
		}
		Conditions conditions = assertion.getConditions();
		conditions.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000)));
		conditions.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000)));
		return assertion;
	}

	private Assertion assertion() {
		return assertion(null);
	}

	private <T extends SignableSAMLObject> T signed(T toSign) {
		TestOpenSamlObjects.signed(toSign, TestSaml2X509Credentials.assertingPartySigningCredential(),
				RELYING_PARTY_ENTITY_ID);
		return toSign;
	}

	private List<AttributeStatement> attributeStatements() {
		List<AttributeStatement> attributeStatements = TestOpenSamlObjects.attributeStatements();
		AttributeBuilder attributeBuilder = new AttributeBuilder();
		Attribute registeredDateAttr = attributeBuilder.buildObject();
		registeredDateAttr.setName("registeredDate");
		XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,
				XSDateTime.TYPE_NAME);
		registeredDate.setValue(Instant.parse("1970-01-01T00:00:00Z"));
		registeredDateAttr.getAttributeValues().add(registeredDate);
		attributeStatements.iterator().next().getAttributes().add(registeredDateAttr);
		return attributeStatements;
	}

	private Saml2AuthenticationToken token() {
		Response response = response();
		RelyingPartyRegistration registration = verifying(registration()).build();
		return new Saml2AuthenticationToken(registration, serialize(response));
	}

	private Saml2AuthenticationToken token(Response response, RelyingPartyRegistration.Builder registration) {
		return new Saml2AuthenticationToken(registration.build(), serialize(response));
	}

	private Saml2AuthenticationToken token(Response response, RelyingPartyRegistration.Builder registration,
			AbstractSaml2AuthenticationRequest authenticationRequest) {
		return new Saml2AuthenticationToken(registration.build(), serialize(response), authenticationRequest);
	}

	private AbstractSaml2AuthenticationRequest mockedStoredAuthenticationRequest(String requestId) {
		AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mock(AbstractSaml2AuthenticationRequest.class);
		given(mockAuthenticationRequest.getId()).willReturn(requestId);
		return mockAuthenticationRequest;
	}

	private RelyingPartyRegistration.Builder registration() {
		return TestRelyingPartyRegistrations.noCredentials()
			.entityId(RELYING_PARTY_ENTITY_ID)
			.assertionConsumerServiceLocation(DESTINATION)
			.assertingPartyMetadata((party) -> party.entityId(ASSERTING_PARTY_ENTITY_ID));
	}

	private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
		return builder.assertingPartyMetadata((party) -> party
			.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
	}

	private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) {
		return builder
			.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential()));
	}

}
