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

se.litsec.opensaml.saml2.authentication.build.ExtendedAuthnRequestBuilder Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
/*
 * Copyright 2016-2018 Litsec AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package se.litsec.opensaml.saml2.authentication.build;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.ext.saml2mdattr.EntityAttributes;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml.saml2.metadata.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.shibboleth.utilities.java.support.security.RandomIdentifierGenerationStrategy;
import se.litsec.opensaml.core.SAMLObjectBuilderRuntimeException;
import se.litsec.opensaml.saml2.attribute.AttributeUtils;
import se.litsec.opensaml.saml2.core.build.AbstractAuthnRequestBuilder;
import se.litsec.opensaml.saml2.core.build.NameIDPolicyBuilder;
import se.litsec.opensaml.saml2.core.build.RequestedAuthnContextBuilder;
import se.litsec.opensaml.saml2.metadata.MetadataUtils;
import se.litsec.opensaml.saml2.metadata.build.IdpEntityDescriptorBuilder;
import se.litsec.opensaml.utils.ObjectUtils;

/**
 * The {@code ExtendedAuthnRequestBuilder} builds an {@code AuthnRequest} object given the metadata entry for the
 * Service Provider that sends the request and the metadata entry for the Identity Provider that is the recipient of the
 * request.
 * 

* The purpose with this builder is that the caller does not have to go through the SP and IdP metadata and create a * valid {@code AuthnRequest}. By invoking {@link #assignDefaults()} the {@code AuthnRequest} is built using values * found in the metadata entries. Any particular settings that are non-default should be assigned using the builder's * assigment methods, either before or after invoking {@link #assignDefaults()}. The builder will assume that the * "HTTP-Redirect" binding is used to send the request to the IdP (given that the IdP has an endpoint for this binding). * Should the caller want to use another binding (POST), the {@link #binding(String)} should be invoked before calling * {@link #assignDefaults()}. *

* * @author Martin Lindström ([email protected]) */ public class ExtendedAuthnRequestBuilder extends AbstractAuthnRequestBuilder { /** * If no binding for how the request is to be passed to the IdP we assume * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI}. */ public static final String DEFAULT_REQUEST_BINDING = SAMLConstants.SAML2_REDIRECT_BINDING_URI; /** If an ID attribute is generated by the builder it uses 24 characters for it. */ public static final int DEFAULT_ID_SIZE = 24; /** Logging instance. */ private Logger log = LoggerFactory.getLogger(ExtendedAuthnRequestBuilder.class); /** The SP metadata. */ final EntityDescriptor spMetadata; /** The IdP metadata. */ final EntityDescriptor idpMetadata; /** The binding of the request (redirect, POST, ...). */ private String binding; /** * Constructor initializing the builder with the metadata entry for the Service Provider that is creating the * authentication request and the metadata entry for the Identity Provider which is about to receive the request. * * @param spMetadata * the SP metadata * @param idpMetadata * the IdP metadata */ public ExtendedAuthnRequestBuilder(EntityDescriptor spMetadata, EntityDescriptor idpMetadata) { if (spMetadata == null) { throw new IllegalArgumentException("spMetadata must not be null"); } if (spMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS) == null) { throw new IllegalArgumentException("spMetadata does not contain a SPSSODescriptor"); } this.spMetadata = spMetadata; if (idpMetadata == null) { throw new IllegalArgumentException("idpMetadata must not be null"); } if (idpMetadata.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) == null) { throw new IllegalArgumentException("idpMetadata does not contain a IDPSSODescriptor"); } this.idpMetadata = idpMetadata; } /** * Calculates values based on the SP and IdP metadata and assigns them to the {@code AuthnRequest}. * * The following rules are automatically applied by the {@code assignDefaults()} method: *
    *
  • The version is set to 2.0.
  • *
  • An ID attribute is generated and assigned.
  • *
  • The {@code ProtocolBinding} is assigned to HTTP-POST.
  • *
  • The {@code Destination} attribute is assigned the value found in the IdP metadata's {@code SingleSignOnService} * element having a binding matching the binding that was assigned this builder.
  • *
  • The {@code Issuer} element is assigned the entityID found in the SP metadata.
  • *
  • The {@code NameIDPolicy} element is assigned by iterating over the declared {@code NameIDFormat} elements of * the SP metadata and using the first format that is also declared by the IdP. The {@code AllowCreate} is set to * {@code true}. *
  • *
* * @return the builder */ public ExtendedAuthnRequestBuilder assignDefaults() { final SPSSODescriptor spDescriptor = this.spMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS); final IDPSSODescriptor idpDescriptor = this.idpMetadata.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); // Version // this.version(SAMLVersion.VERSION_20.getMajorVersion(), SAMLVersion.VERSION_20.getMinorVersion()); // ID // If not set, we generate an identifier. // if (this.object().getID() == null) { log.debug("Generated an ID attribute and assigned it"); this.id(DEFAULT_ID_SIZE); } // ProtocolBinding // this.postProtocolBinding(); // Issuer // if (this.object().getIssuer() == null) { log.debug("Assigning entityID '{}' to the Issuer element", this.spMetadata.getEntityID()); this.issuer(this.spMetadata.getEntityID()); } // If the binding has not been set, we assign the default binding (the Destination attribute will // also be set). // if (this.binding == null) { this.binding(DEFAULT_REQUEST_BINDING); } // AssertionConsumerServiceURL (for POST binding) // if (this.object().getAssertionConsumerServiceURL() == null) { Optional serviceUrl = spDescriptor.getAssertionConsumerServices() .stream() .filter(a -> SAMLConstants.SAML2_POST_BINDING_URI.equals(a.getBinding())) .filter(IndexedEndpoint::isDefault) .map(AssertionConsumerService::getLocation) .findFirst(); if (!serviceUrl.isPresent()) { serviceUrl = spDescriptor.getAssertionConsumerServices() .stream() .filter(a -> SAMLConstants.SAML2_POST_BINDING_URI.equals(a.getBinding())) .sorted((a1, a2) -> a1.getIndex() != null ? (a2.getIndex() != null ? a1.getIndex().compareTo(a2.getIndex()) : -1) : (a2.getIndex() != null ? 1 : 0)) .map(AssertionConsumerService::getLocation) .findFirst(); } if (serviceUrl.isPresent()) { log.debug("Assigning URL '{}' to the AssertionConsumerServiceURL attribute", serviceUrl.get()); this.object().setAssertionConsumerServiceURL(serviceUrl.get()); } else { log.info("The AssertionConsumerServiceURL attribute could not be assigned automatically. " + "- Could not find a AssertionConsumerService element in the SP metadata that has the POST binding"); } } // NameIDPolicy // if (this.object().getNameIDPolicy() == null) { // We look at the declared NameIDFormats from the SP metadata and select the first one that is also declared by // the IdP. // Optional nameIDFormat = spDescriptor.getNameIDFormats() .stream() .filter(n -> idpDescriptor.getNameIDFormats() .stream() .anyMatch(in -> in.getFormat().equals(n.getFormat()))) .map(NameIDFormat::getFormat) .findFirst(); if (nameIDFormat.isPresent()) { log.debug("Assigning the '{}' Format to the NameIDPolicy element", nameIDFormat.get()); this.nameIDPolicy(NameIDPolicyBuilder.builder().allowCreate(true).format(nameIDFormat.get()).build()); } else { log.info("Could not assign the NameIDPolicy element automatically - no matching formats between SP and IdP"); } } return this; } /** * Generates an identifier of size {@code idSize} and assigns it to the {@code AuthnRequest}. * * @param idSize * the number of characters to be used in the ID * @return the builder */ public ExtendedAuthnRequestBuilder id(int idSize) { RandomIdentifierGenerationStrategy generator = new RandomIdentifierGenerationStrategy(idSize); super.id(generator.generateIdentifier()); return this; } /** * Assigns the {@code Destination} attribute and also updates the binding to use based on which of the IdP * {@code SingleSignService} elements that match the supplied destination value. *

* Using this builder it is not recommended to assign the {@code Destination} attribute. Instead assign the desired * binding ({@link #binding(String)}) and the {@code Destination} attribute will be automatically assigned. *

* * @see #binding(String) */ @Override public ExtendedAuthnRequestBuilder destination(String destination) { if (destination == null) { return super.destination(null); } Optional ssoService = this.idpMetadata.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) .getSingleSignOnServices() .stream() .filter(s -> destination.equals(s.getLocation())) .findFirst(); if (!ssoService.isPresent()) { String msg = String.format( "Metadata for IdP '%s' does not declare a SingleSignService element having its Location attribute set to '%s'", this.idpMetadata.getEntityID(), destination); log.error(msg); throw new SAMLObjectBuilderRuntimeException(msg); } log.debug("Assigning the Destination attribute to '{}' the setting the binding to '{}'", destination, ssoService.get().getBinding()); this.binding = ssoService.get().getBinding(); return super.destination(destination); } /** * Returns the binding URI to be used to this request, i.e., should the request be redirected to the IdP or should it * be posted? *

* The setting controls how the AuthnRequest is put together and which data that is read from the IdP metadata. *

* * @return the binding URI */ public String binding() { return this.binding; } /** * Assigns the URI that tells which binding (method) to use when transfering the AuthnRequest to the IdP. *

* The setting controls how the {@code AuthnRequest} is put together and which data that is read from the IdP * metadata. More specifically it assigns the {@code Destination} attribute to the address found in the IdP * {@code SingleSignOnService} element having this binding. *

* * @param binding * the binding URI * @return the builder * @throws SAMLObjectBuilderRuntimeException * is thrown if the IdP metadata does not define a {@code SingleSignOnService} element having the given * binding, which means that it does not support it, and it it thus meaningless to send this request using * this binding */ public ExtendedAuthnRequestBuilder binding(String binding) throws SAMLObjectBuilderRuntimeException { if (binding == null) { throw new IllegalArgumentException("binding must not be null"); } Optional destination = this.idpMetadata.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) .getSingleSignOnServices() .stream() .filter(s -> binding.equals(s.getBinding())) .map(SingleSignOnService::getLocation) .findFirst(); if (!destination.isPresent()) { String msg = String.format("Metadata for IdP '%s' does not declare a SingleSignOnService element having the '%s' binding", this.idpMetadata.getEntityID(), binding); log.error(msg); throw new SAMLObjectBuilderRuntimeException(msg); } log.debug("Assigning the '{}' binding and setting the Destination attribute to '{}'", binding, destination.get()); this.binding = binding; super.destination(destination.get()); return this; } /** * Assigns a {@code NameIDPolicy} element with the {@code Format} attribute assigned to {@code format} and its * {@code AllowCreate} attribute set to {@code true}. * * @param format * the format to assign * @return the builder * @throws SAMLObjectBuilderRuntimeException * if the IdP's metadata entry does not list the supplied format as supported */ public ExtendedAuthnRequestBuilder nameIDPolicyFormat(String format) throws SAMLObjectBuilderRuntimeException { if (format == null) { return this.nameIDPolicy(null); } boolean match = this.idpMetadata.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) .getNameIDFormats() .stream() .anyMatch(in -> in.getFormat().equals(format)); if (!match) { String msg = String.format("IdP '%s' does not support NameID of format '%s'", this.idpMetadata.getEntityID(), format); log.error(msg); throw new SAMLObjectBuilderRuntimeException(msg); } this.nameIDPolicy(NameIDPolicyBuilder.builder().allowCreate(true).format(format).build()); return this; } /** * A utility method that helps adding one or more Authentication context class reference URI(s) to the * {@code RequestedAuthnContext} element. The method will read the IdP's declared assuranceCertification URIs from its * metadata. * * @param onlyMatching * only add URIs that are also declared by the IdP in its metadata * @param failOnNoMatch * throw if none of our given URIs are declared by the IdP * @param uris * the URIs to add * @return the builder * @throws SAMLObjectBuilderRuntimeException * is thrown if {@code failOnNoMatch} is set and we don't get a match between given URIs and declared URIs */ public ExtendedAuthnRequestBuilder authnContextClassRefs(boolean onlyMatching, boolean failOnNoMatch, List uris) throws SAMLObjectBuilderRuntimeException { if (uris == null || uris.isEmpty()) { this.clearAuthnContextClassRefs(); return this; } // Get hold of the IdP:s assurance certification from its metadata. // Optional entityAttributes = MetadataUtils.getEntityAttributes(this.idpMetadata); Optional assuranceCertificationAttribute = entityAttributes.flatMap( entityAttributes1 -> entityAttributes1 .getAttributes() .stream() .filter(a -> IdpEntityDescriptorBuilder.ASSURANCE_CERTIFICATION_ATTRIBUTE_NAME.equals(a.getName())) .findFirst()); List assuranceUris = assuranceCertificationAttribute.map(AttributeUtils::getAttributeStringValues) .orElseGet(Collections::emptyList); // Get hold of which URIs to add // List urisToAdd = onlyMatching ? uris.stream().filter(assuranceUris::contains).collect(Collectors.toList()) : uris; // If we have no match and require it, fail or return ... // if (urisToAdd.isEmpty()) { String msg = assuranceUris.isEmpty() ? "IdP metadata does not specify any assuranceCertification URIs - failing to assign authentication context refs" : String.format("IdP metadata specified assurance URIs %s - call contained %s - no match", assuranceUris, uris); if (failOnNoMatch) { log.error(msg); throw new SAMLObjectBuilderRuntimeException(msg); } else if (onlyMatching) { log.warn(msg); this.clearAuthnContextClassRefs(); return this; } } // Setup a RequestedAuthnContext element if needed. // if (this.object().getRequestedAuthnContext() == null) { this.requestedAuthnContext( RequestedAuthnContextBuilder.builder().comparison(AuthnContextComparisonTypeEnumeration.EXACT).build()); } this.object().getRequestedAuthnContext().getAuthnContextClassRefs().clear(); log.debug("Adding URI(s) %s as AuthnContextClassRef elements to RequestedAuthnContext", urisToAdd); for (String uri : urisToAdd) { AuthnContextClassRef accr = ObjectUtils.createSamlObject(AuthnContextClassRef.class); accr.setAuthnContextClassRef(uri); this.object().getRequestedAuthnContext().getAuthnContextClassRefs().add(accr); } return this; } /** * @see #authnContextClassRefs(boolean, boolean, List) * * @param onlyMatching * only add URIs that are also declared by the IdP in its metadata * @param failOnNoMatch * throw if none of our given URIs are declared by the IdP * @param uris * the URIs to add * @return the builder * @throws SAMLObjectBuilderRuntimeException * is thrown if {@code failOnNoMatch} is set and we don't get a match between given URIs and declared URIs */ public ExtendedAuthnRequestBuilder authnContextClassRefs(boolean onlyMatching, boolean failOnNoMatch, String... uris) throws SAMLObjectBuilderRuntimeException { return this.authnContextClassRefs(onlyMatching, failOnNoMatch, uris != null ? Arrays.asList(uris) : null); } /** * Clears out the {@code AuthnContextClassRef} elements. */ private void clearAuthnContextClassRefs() { if (this.object().getRequestedAuthnContext() != null) { if (this.object().getRequestedAuthnContext().getAuthnContextDeclRefs().isEmpty()) { this.object().setRequestedAuthnContext(null); } else { this.object().getRequestedAuthnContext().getAuthnContextClassRefs().clear(); } } } /** {@inheritDoc} */ @Override protected ExtendedAuthnRequestBuilder getThis() { return this; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy