se.litsec.opensaml.saml2.authentication.build.ExtendedAuthnRequestBuilder Maven / Gradle / Ivy
Show all versions of opensaml3-ext Show documentation
/*
* 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;
}
}