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

se.swedenconnect.opensaml.saml2.request.AbstractAuthnRequestGenerator Maven / Gradle / Ivy

/*
 * Copyright 2016-2021 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.request;

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.opensaml.messaging.encoder.MessageEncodingException;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.security.x509.X509Credential;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.impl.RandomIdentifierGenerationStrategy;
import se.swedenconnect.opensaml.common.utils.SamlLog;
import se.swedenconnect.opensaml.saml2.core.build.AuthnRequestBuilder;
import se.swedenconnect.opensaml.saml2.metadata.EntityDescriptorUtils;
import se.swedenconnect.opensaml.saml2.metadata.HolderOfKeyMetadataSupport;
import se.swedenconnect.opensaml.saml2.request.AuthnRequestGeneratorContext.HokRequirement;

/**
 * Abstract base class for generating AuthnRequest messages.
 * 
 * @author Martin Lindström ([email protected])
 */
public abstract class AbstractAuthnRequestGenerator extends AbstractInitializableComponent implements AuthnRequestGenerator {

  /** Logging instance. */
  private final Logger log = LoggerFactory.getLogger(AbstractAuthnRequestGenerator.class);

  /** The SP entityID. */
  private final String spEntityID;

  /** The SP signing credential. */
  private final X509Credential signCredential;

  /** The SP metadata. */
  private EntityDescriptor cachedSpMetadata;

  /** Generates ID. */
  private final RandomIdentifierGenerationStrategy idGenerator = new RandomIdentifierGenerationStrategy(20);

  /**
   * Constructor.
   * 
   * @param spEntityID
   *          the SP entityID
   * @param signCredential
   *          the signing credential
   */
  public AbstractAuthnRequestGenerator(final String spEntityID, final X509Credential signCredential) {
    this.spEntityID = Optional.ofNullable(spEntityID)
      .filter(e -> !StringUtils.isBlank(e))
      .orElseThrow(() -> new IllegalArgumentException("spEntityID must be set"));
    this.signCredential = signCredential;
    if (this.signCredential == null) {
      log.warn("No signing credential supplied - Generation will fail if the IdP requires signed requests");
    }
  }

  /** {@inheritDoc} */
  @Override
  protected void doInitialize() throws ComponentInitializationException {
    this.cachedSpMetadata = this.getSpMetadata();
    if (this.cachedSpMetadata == null) {
      throw new ComponentInitializationException("No SP metadata is available");
    }
  }

  /** {@inheritDoc} */
  @Override
  public RequestHttpObject generateAuthnRequest(final String idpEntityID,
      final String relayState, final AuthnRequestGeneratorContext context) throws RequestGenerationException {

    log.debug("Request to generate an AuthnRequest for {} ...", idpEntityID);

    final AuthnRequestGeneratorContext generatorContext = context != null
        ? context
        : new AuthnRequestGeneratorContext() {
        };

    final SPSSODescriptor spDescriptor = this.getSpMetadata().getSPSSODescriptor(SAMLConstants.SAML20P_NS);

    // First get hold of the IdP metadata ...
    //
    final EntityDescriptor idpMetadata = this.getIdpMetadata(idpEntityID);
    if (idpMetadata == null) {
      throw new RequestGenerationException("No metadata could be found for IdP " + idpEntityID);
    }

    // Make some checks about the holder of key requirement ...
    // If the hok-requirement states that HoK must be used, we ensure that the SP
    // has an AssertionConsumerService endpoint dedicated for this.
    //
    if (HokRequirement.REQUIRED.equals(context.getHokRequirement())) {
      if (HolderOfKeyMetadataSupport.getHokAssertionConsumerServices(spDescriptor).isEmpty()) {
        throw new RequestGenerationException(
          "Context Holder-of-key requirement states that HoK must be used, but SP does not "
              + "have a dedicated AssertionConsumerService endpoint for this");
      }
    }

    // Find out where to send the request and with which binding ...
    //
    final SingleSignOnService ssoService = this.getSingleSignOnService(idpMetadata, generatorContext);

    // Is this a HoK endpoint?
    boolean hokActive = HolderOfKeyMetadataSupport.HOK_WEBSSO_PROFILE_URI.equals(ssoService.getBinding());

    // OK, let's start building the AuthnRequest ...
    //
    AuthnRequestBuilder builder = AuthnRequestBuilder.builder();
    builder
      .id(this.idGenerator.generateIdentifier())
      .issuer(this.getSpEntityID())
      .issueInstant(Instant.now())
      .forceAuthn(generatorContext.getForceAuthnAttribute())
      .isPassive(generatorContext.getIsPassiveAttribute())
      .destination(ssoService.getLocation());

    // Ask the context callback about which AssertionConsumerService to use ...
    //
    final Object assertionConsumerService =
        generatorContext.getAssertionConsumerServiceResolver().apply(this.getPossibleAssertionConsumerServices(hokActive));
    if (assertionConsumerService != null) {
      if (assertionConsumerService instanceof String) {
        builder.assertionConsumerServiceURL((String) assertionConsumerService);
      }
      else if (assertionConsumerService instanceof Integer) {
        builder.assertionConsumerServiceIndex((Integer) assertionConsumerService);
      }
      else {
        throw new RequestGenerationException("Illegal return value from AssertionConsumerServiceResolver");
      }
    }

    // Ask the context callback about whether to include a AttributeConsumingServiceIndex, and if so, which.
    //
    if (!spDescriptor.getAttributeConsumingServices().isEmpty()) {
      builder.attributeConsumerServiceIndex(
        generatorContext.getAttributeConsumingServiceIndexResolver().apply(spDescriptor.getAttributeConsumingServices()));
    }

    // Get the intersection concerning NameID:s between the SP and IdP metadata
    // and invoke the builder for getting a NameIDPolicy.
    //
    final List idpFormats = idpMetadata.getIDPSSODescriptor(SAMLConstants.SAML20P_NS).getNameIDFormats();
    final List formats = spDescriptor.getNameIDFormats().stream()
      .filter(f -> idpFormats.stream().filter(idpf -> idpf.getURI().equals(f.getURI())).findFirst().isPresent())
      .collect(Collectors.toList());
    builder.nameIDPolicy(generatorContext.getNameIDPolicyBuilderFunction().apply(formats));

    // OK, time to build the RequestedAuthnContext element ...
    // We need to get hold of the assurance certifications of the IdP.
    //
    builder.requestedAuthnContext(generatorContext.getRequestedAuthnContextBuilderFunction()
      .apply(this.getAssuranceCertificationUris(idpMetadata, generatorContext), hokActive));

    // Add Scoping element (if implemented)
    this.addScoping(builder, generatorContext, idpMetadata);

    // Add Extensions element (if implemented)
    this.addExtensions(builder, generatorContext, idpMetadata);

    // OK, we are almost done. Build the AuthnRequest and let's invoke the customizer to give the
    // caller the possibility to add extensions and such ...
    //
    final AuthnRequest authnRequest = builder.build();
    generatorContext.getAuthnRequestCustomizer().accept(authnRequest);

    if (log.isTraceEnabled()) {
      log.trace("Created AuthnRequest: {}", SamlLog.toStringSafe(authnRequest));
    }

    return this.buildRequestHttpObject(
      authnRequest, relayState, generatorContext, this.getBinding(ssoService), ssoService.getLocation(), idpMetadata);
  }

  /**
   * Method that adds the {@code Scoping} element to the {@code AuthnRequest} being built.
   * 

* The default implementation does nothing. *

* * @param builder * the builder * @param context * the generator context * @param idpMetadata * the IdP metadata * @throws RequestGenerationException * for generation errors */ protected void addScoping(final AuthnRequestBuilder builder, final AuthnRequestGeneratorContext context, final EntityDescriptor idpMetadata) throws RequestGenerationException { // NO-OP } /** * Method that adds the {@code Extensions} element to the {@code AuthnRequest} being built. *

* The default implementation does nothing. *

* * @param builder * the builder * @param context * the generator context * @param idpMetadata * the IdP metadata * @throws RequestGenerationException * for generation errors */ protected void addExtensions(final AuthnRequestBuilder builder, final AuthnRequestGeneratorContext context, final EntityDescriptor idpMetadata) throws RequestGenerationException { // NO-OP } /** * Gets the assurance certification URI:s for the IdP metadata. *

* The default implementation returns all URI:s found in the metadata. *

* * @param idpMetadata * the IdP metadata * @param context * the context * @return a list of URI:s * @throws RequestGenerationException * for errors */ protected List getAssuranceCertificationUris(final EntityDescriptor idpMetadata, final AuthnRequestGeneratorContext context) throws RequestGenerationException { return EntityDescriptorUtils.getAssuranceCertificationUris(idpMetadata); } /** {@inheritDoc} */ @Override public String getSpEntityID() { return this.spEntityID; } /** {@inheritDoc} */ @Override public X509Credential getSignCredential() { return this.signCredential; } /** * Gets the metadata for the SP that this generator services. * * @return the SP metadata, or null if no metadata is found */ protected abstract EntityDescriptor getSpMetadata(); /** * Gets the IdP metadata for the given entityID. * * @param idpEntityID * the entityID for the IdP * @return the metadata or null if no metadata could be found */ protected abstract EntityDescriptor getIdpMetadata(final String idpEntityID); /** * Builds a request HTTP object (including signing). * * @param request * the actual request * @param relayState * the RelayState (may be null) * @param context * the request generation context * @param binding * the binding to use * @param destination * the destination URL * @param recipientMetadata * the recipient metadata * @return a request HTTP object * @throws RequestGenerationException * for errors during signing or encoding */ protected RequestHttpObject buildRequestHttpObject(final AuthnRequest request, final String relayState, final AuthnRequestGeneratorContext context, final String binding, final String destination, final EntityDescriptor recipientMetadata) throws RequestGenerationException { final X509Credential signCred = Optional.ofNullable(context.getOverrideSignCredential()).orElse(this.signCredential); try { if (SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(binding)) { // Redirect binding return new RedirectRequestHttpObject<>(request, relayState, signCred, destination, recipientMetadata, context.getSignatureSigningConfiguration()); } else if (SAMLConstants.SAML2_POST_BINDING_URI.equals(binding)) { // POST binding return new PostRequestHttpObject<>(request, relayState, signCred, destination, recipientMetadata, context.getSignatureSigningConfiguration()); } else { throw new RequestGenerationException("Unsupported binding: " + binding); } } catch (MessageEncodingException | SignatureException e) { String msg = "Failed to encode/sign request for transport"; log.error(msg, e); throw new RequestGenerationException(msg); } } /** * Extracts all possible SP AssertionConsumerService endpoints. * * @param hokActive * a flag that tells whether HoK is active or not * @return a list of possible endpoints */ protected List getPossibleAssertionConsumerServices(final boolean hokActive) { final SPSSODescriptor descriptor = this.getSpMetadata().getSPSSODescriptor(SAMLConstants.SAML20P_NS); if (hokActive) { return HolderOfKeyMetadataSupport.getHokAssertionConsumerServices(descriptor); } else { return descriptor.getAssertionConsumerServices().stream() .filter(a -> SAMLConstants.SAML2_POST_BINDING_URI.equals(a.getBinding())) .collect(Collectors.toList()); } } /** * Utility method that, given a {@link SingleSignOnService}, gets the binding URI (redirect/post). * * @param sso * the SingleSignOnService * @return the binding URI */ protected String getBinding(final SingleSignOnService sso) { if (HolderOfKeyMetadataSupport.HOK_WEBSSO_PROFILE_URI.equals(sso.getBinding())) { return sso.getUnknownAttributes().get(HolderOfKeyMetadataSupport.HOK_PROTOCOL_BINDING_ATTRIBUTE); } else { return sso.getBinding(); } } /** * Returns the {@code SingleSignOnService} element to use when sending the request to the IdP. The preferred binding * will be searched for first, and if this is not found, another binding that we support will be used. * * @param idp * the IdP metadata * @param context * context for generating * @return a SingleSignOnService object * @throws RequestGenerationException * if not valid endpoint can be found */ protected SingleSignOnService getSingleSignOnService(final EntityDescriptor idp, final AuthnRequestGeneratorContext context) throws RequestGenerationException { final IDPSSODescriptor descriptor = Optional.ofNullable(idp.getIDPSSODescriptor(SAMLConstants.SAML20P_NS)) .orElseThrow(() -> new RequestGenerationException("Invalid IdP metadata - missing IDPSSODescriptor")); if (HokRequirement.REQUIRED.equals(context.getHokRequirement()) || HokRequirement.IF_AVAILABLE.equals(context.getHokRequirement())) { SingleSignOnService ssoService = null; for (final SingleSignOnService sso : HolderOfKeyMetadataSupport.getHokSingleSignOnServices(descriptor)) { final String protocolBinding = sso.getUnknownAttributes().get(HolderOfKeyMetadataSupport.HOK_PROTOCOL_BINDING_ATTRIBUTE); if (context.getPreferredBinding().equals(protocolBinding)) { return sso; } else { ssoService = sso; } } if (ssoService != null) { return ssoService; } else if (HokRequirement.REQUIRED.equals(context.getHokRequirement())) { String msg = String.format("IdP '%s' does not specify endpoints for Holder-of-key - cannot send request", idp.getEntityID()); log.error(msg); throw new RequestGenerationException(msg); } else { // HokRequirement.IF_AVAILABLE log.info("IdP '%s' does not specify endpoints for Holder-of-key - using normal WebSSO Profile", idp.getEntityID()); } } SingleSignOnService ssoService = descriptor.getSingleSignOnServices() .stream() .filter(s -> context.getPreferredBinding().equals(s.getBinding())) .findFirst() .orElse(null); if (ssoService == null) { ssoService = descriptor.getSingleSignOnServices().stream() .filter(s -> SAMLConstants.SAML2_POST_BINDING_URI.equals(s.getBinding()) || SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(s.getBinding())) .findFirst() .orElse(null); } if (ssoService == null) { String msg = String.format( "IdP '%s' does not specify endpoints for POST or Redirect - cannot send request", idp.getEntityID()); log.error(msg); throw new RequestGenerationException(msg); } return ssoService; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy