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

org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder Maven / Gradle / Ivy

There is a newer version: 8.16.1
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */
package org.elasticsearch.xpack.security.authc.saml;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.util.Maps;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.AttributeConsumingService;
import org.opensaml.saml.saml2.metadata.ContactPerson;
import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration;
import org.opensaml.saml.saml2.metadata.EmailAddress;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.GivenName;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.Organization;
import org.opensaml.saml.saml2.metadata.OrganizationDisplayName;
import org.opensaml.saml.saml2.metadata.OrganizationName;
import org.opensaml.saml.saml2.metadata.OrganizationURL;
import org.opensaml.saml.saml2.metadata.RequestedAttribute;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.ServiceName;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.SurName;
import org.opensaml.saml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
import org.opensaml.saml.saml2.metadata.impl.AttributeConsumingServiceBuilder;
import org.opensaml.saml.saml2.metadata.impl.ContactPersonBuilder;
import org.opensaml.saml.saml2.metadata.impl.EmailAddressBuilder;
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.GivenNameBuilder;
import org.opensaml.saml.saml2.metadata.impl.KeyDescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.NameIDFormatBuilder;
import org.opensaml.saml.saml2.metadata.impl.OrganizationBuilder;
import org.opensaml.saml.saml2.metadata.impl.OrganizationDisplayNameBuilder;
import org.opensaml.saml.saml2.metadata.impl.OrganizationNameBuilder;
import org.opensaml.saml.saml2.metadata.impl.OrganizationURLBuilder;
import org.opensaml.saml.saml2.metadata.impl.RequestedAttributeBuilder;
import org.opensaml.saml.saml2.metadata.impl.SPSSODescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.ServiceNameBuilder;
import org.opensaml.saml.saml2.metadata.impl.SingleLogoutServiceBuilder;
import org.opensaml.saml.saml2.metadata.impl.SurNameBuilder;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.x509.X509Credential;
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.impl.KeyInfoBuilder;

import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Constructs SAML Metadata to describe a Service Provider.
 * This metadata is used to configure Identity Providers that will interact with the Service Provider.
 */
public class SamlSpMetadataBuilder {

    private final Locale locale;
    private final String entityId;
    private final Map attributeNames;
    private final List contacts;

    private String serviceName;
    private String nameIdFormat;
    private String assertionConsumerServiceUrl;
    private String singleLogoutServiceUrl;
    private Boolean authnRequestsSigned;
    private X509Certificate signingCertificate;
    private List encryptionCertificates = new ArrayList<>();
    private OrganizationInfo organization;

    /**
     * @param locale   The locale to use for element that require {@code xml:lang} attributes
     * @param entityId The URI for the Service Provider entity
     */
    public SamlSpMetadataBuilder(Locale locale, String entityId) {
        this.locale = locale;
        this.entityId = entityId;
        this.attributeNames = new LinkedHashMap<>();
        this.contacts = new ArrayList<>();
        this.serviceName = "Elasticsearch";
        this.nameIdFormat = null;
        this.authnRequestsSigned = Boolean.FALSE;
    }

    /**
     * The format that the service provider expects for incoming NameID element.
     */
    public SamlSpMetadataBuilder nameIdFormat(String nameIdFormat) {
        this.nameIdFormat = nameIdFormat;
        return this;
    }

    /**
     * The name of the service, for use in a {@link AttributeConsumingService}
     */
    public SamlSpMetadataBuilder serviceName(String serviceName) {
        this.serviceName = serviceName;
        return this;
    }

    /**
     * Request a named attribute be provided as part of assertions. Specified  in a {@link AttributeConsumingService}
     */
    public SamlSpMetadataBuilder withAttribute(String friendlyName, String name) {
        if (Strings.isNullOrEmpty(name)) {
            throw new IllegalArgumentException("Attribute name cannot be empty (friendly name was [" + friendlyName + "])");
        }
        this.attributeNames.put(name, friendlyName);
        return this;
    }

    /**
     * The (POST) URL to be used to accept SAML assertions (authentication results)
     */
    public SamlSpMetadataBuilder assertionConsumerServiceUrl(String acsUrl) {
        this.assertionConsumerServiceUrl = acsUrl;
        return this;
    }

    /**
     * The (GET/Redirect) URL to be used to handle SAML logout / session termination
     */
    public SamlSpMetadataBuilder singleLogoutServiceUrl(String slsUrl) {
        this.singleLogoutServiceUrl = slsUrl;
        return this;
    }

    /**
     * Whether this Service Provider signs {@link org.opensaml.saml.saml2.core.AuthnRequest} messages.
     */
    public SamlSpMetadataBuilder authnRequestsSigned(Boolean authnRequestsSigned) {
        this.authnRequestsSigned = authnRequestsSigned;
        return this;
    }

    /**
     * The certificate that the service provider users to sign SAML requests.
     */
    public SamlSpMetadataBuilder signingCertificate(X509Certificate signingCertificate) {
        this.signingCertificate = signingCertificate;
        return this;
    }

    /**
     * The certificate credential that should be used to send encrypted data to the service provider.
     */
    public SamlSpMetadataBuilder signingCredential(X509Credential credential) {
        return signingCertificate(credential == null ? null : credential.getEntityCertificate());
    }

    /**
     * The certificate that should be used to send encrypted data to the service provider.
     */
    public SamlSpMetadataBuilder encryptionCertificates(Collection encryptionCertificates) {
        if (encryptionCertificates != null) {
            this.encryptionCertificates.addAll(encryptionCertificates);
        }
        return this;
    }

    /**
     * The certificate credential that should be used to send encrypted data to the service provider.
     */
    public SamlSpMetadataBuilder encryptionCredentials(Collection credentials) {
        return encryptionCertificates(
            credentials == null
                ? Collections.emptyList()
                : credentials.stream().map(credential -> credential.getEntityCertificate()).collect(Collectors.toList())
        );
    }

    /**
     * The organisation that operates the service provider
     */
    public SamlSpMetadataBuilder organization(OrganizationInfo organization) {
        this.organization = organization;
        return this;
    }

    /**
     * The organisation that operates the service provider
     */
    public SamlSpMetadataBuilder organization(String orgName, String displayName, String url) {
        return organization(new OrganizationInfo(orgName, displayName, url));
    }

    /**
     * A contact within the organisation that operates the service provider
     */
    public SamlSpMetadataBuilder withContact(ContactInfo contact) {
        this.contacts.add(contact);
        return this;
    }

    /**
     * A contact within the organisation that operates the service provider
     *
     * @param type Must be one of the standard types on {@link ContactPersonTypeEnumeration}
     */
    public SamlSpMetadataBuilder withContact(String type, String givenName, String surName, String email) {
        return withContact(new ContactInfo(ContactInfo.getType(type), givenName, surName, email));
    }

    /**
     * Constructs an {@link EntityDescriptor} that contains a single {@link SPSSODescriptor}.
     */
    public EntityDescriptor build() throws Exception {
        final SPSSODescriptor spRoleDescriptor = new SPSSODescriptorBuilder().buildObject();
        spRoleDescriptor.removeAllSupportedProtocols();
        spRoleDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
        spRoleDescriptor.setWantAssertionsSigned(true);
        spRoleDescriptor.setAuthnRequestsSigned(this.authnRequestsSigned);

        if (Strings.isNullOrEmpty(nameIdFormat) == false) {
            spRoleDescriptor.getNameIDFormats().add(buildNameIdFormat());
        }
        spRoleDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService());
        if (attributeNames.size() > 0) {
            spRoleDescriptor.getAttributeConsumingServices().add(buildAttributeConsumerService());
        }
        if (Strings.hasText(singleLogoutServiceUrl)) {
            spRoleDescriptor.getSingleLogoutServices().add(buildSingleLogoutService());
        }

        spRoleDescriptor.getKeyDescriptors().addAll(buildKeyDescriptors());

        final EntityDescriptor descriptor = new EntityDescriptorBuilder().buildObject();
        descriptor.setEntityID(this.entityId);
        descriptor.getRoleDescriptors().add(spRoleDescriptor);
        if (organization != null) {
            descriptor.setOrganization(buildOrganization());
        }
        if (contacts.size() > 0) {
            contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c)));
        }

        return descriptor;
    }

    private NameIDFormat buildNameIdFormat() {
        if (Strings.isNullOrEmpty(nameIdFormat)) {
            throw new IllegalStateException("NameID format has not been specified");
        }
        final NameIDFormat format = new NameIDFormatBuilder().buildObject();
        format.setURI(this.nameIdFormat);
        return format;
    }

    private AssertionConsumerService buildAssertionConsumerService() {
        if (Strings.isNullOrEmpty(assertionConsumerServiceUrl)) {
            throw new IllegalStateException("AssertionConsumerService URL has not been specified");
        }
        final AssertionConsumerService acs = new AssertionConsumerServiceBuilder().buildObject();
        acs.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
        acs.setIndex(1);
        acs.setIsDefault(Boolean.TRUE);
        acs.setLocation(assertionConsumerServiceUrl);
        return acs;
    }

    private AttributeConsumingService buildAttributeConsumerService() {
        final AttributeConsumingService service = new AttributeConsumingServiceBuilder().buildObject();
        service.setIndex(1);
        service.setIsDefault(true);
        service.getNames().add(buildServiceName());
        attributeNames.forEach(
            (name, friendlyName) -> { service.getRequestedAttributes().add(buildRequestedAttribute(friendlyName, name)); }
        );
        return service;
    }

    private ServiceName buildServiceName() {
        final ServiceName name = new ServiceNameBuilder().buildObject();
        name.setValue(serviceName);
        name.setXMLLang(locale.toLanguageTag());
        return name;
    }

    private static RequestedAttribute buildRequestedAttribute(String friendlyName, String name) {
        final RequestedAttribute attribute = new RequestedAttributeBuilder().buildObject();
        if (Strings.hasText(friendlyName)) {
            attribute.setFriendlyName(friendlyName);
        }
        attribute.setName(name);
        attribute.setNameFormat(RequestedAttribute.URI_REFERENCE);
        return attribute;
    }

    private SingleLogoutService buildSingleLogoutService() {
        final SingleLogoutService service = new SingleLogoutServiceBuilder().buildObject();
        // The draft Interoperable SAML 2 profile requires redirect binding.
        // That's annoying, because they require POST binding for the ACS so now SPs need to
        // support 2 bindings that have different signature passing rules, etc. *sigh*
        service.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
        service.setLocation(singleLogoutServiceUrl);
        return service;
    }

    private List buildKeyDescriptors() throws CertificateEncodingException {
        if (encryptionCertificates.isEmpty() && signingCertificate == null) {
            return Collections.emptyList();
        }
        if (encryptionCertificates.size() == 1 && Objects.equals(encryptionCertificates.get(0), signingCertificate)) {
            return Collections.singletonList(buildKeyDescriptor(encryptionCertificates.get(0), UsageType.UNSPECIFIED));
        }
        List keys = new ArrayList<>();
        if (signingCertificate != null) {
            keys.add(buildKeyDescriptor(signingCertificate, UsageType.SIGNING));
        }
        for (X509Certificate encryptionCertificate : encryptionCertificates) {
            keys.add(buildKeyDescriptor(encryptionCertificate, UsageType.ENCRYPTION));
        }
        return keys;
    }

    private static KeyDescriptor buildKeyDescriptor(X509Certificate certificate, UsageType usageType) throws CertificateEncodingException {
        final KeyDescriptor descriptor = new KeyDescriptorBuilder().buildObject();
        descriptor.setUse(usageType);
        final KeyInfo keyInfo = new KeyInfoBuilder().buildObject();
        KeyInfoSupport.addCertificate(keyInfo, certificate);
        descriptor.setKeyInfo(keyInfo);
        return descriptor;
    }

    private Organization buildOrganization() {
        final String lang = locale.toLanguageTag();
        final OrganizationName name = new OrganizationNameBuilder().buildObject();
        name.setValue(this.organization.organizationName);
        name.setXMLLang(lang);
        final OrganizationDisplayName displayName = new OrganizationDisplayNameBuilder().buildObject();
        displayName.setValue(this.organization.displayName);
        displayName.setXMLLang(lang);
        final OrganizationURL url = new OrganizationURLBuilder().buildObject();
        url.setURI(this.organization.url);
        url.setXMLLang(lang);

        final Organization org = new OrganizationBuilder().buildObject();
        org.getOrganizationNames().add(name);
        org.getDisplayNames().add(displayName);
        org.getURLs().add(url);
        return org;
    }

    private static ContactPerson buildContact(ContactInfo contact) {
        final GivenName givenName = new GivenNameBuilder().buildObject();
        givenName.setValue(contact.givenName);
        final SurName surName = new SurNameBuilder().buildObject();
        surName.setValue(contact.surName);
        final EmailAddress email = new EmailAddressBuilder().buildObject();
        email.setURI(contact.email);

        final ContactPerson person = new ContactPersonBuilder().buildObject();
        person.setType(contact.type);
        person.setGivenName(givenName);
        person.setSurName(surName);
        person.getEmailAddresses().add(email);
        return person;
    }

    public static class OrganizationInfo {
        public final String organizationName;
        public final String displayName;
        public final String url;

        public OrganizationInfo(String organizationName, String displayName, String url) {
            if (Strings.isNullOrEmpty(organizationName)) {
                throw new IllegalArgumentException("Organization Name is required");
            }
            if (Strings.isNullOrEmpty(displayName)) {
                throw new IllegalArgumentException("Organization Display Name is required");
            }
            if (Strings.isNullOrEmpty(url)) {
                throw new IllegalArgumentException("Organization URL is required");
            }
            this.organizationName = organizationName;
            this.displayName = displayName;
            this.url = url;
        }
    }

    public static class ContactInfo {
        static final Map TYPES = Stream.of(
            ContactPersonTypeEnumeration.ADMINISTRATIVE,
            ContactPersonTypeEnumeration.BILLING,
            ContactPersonTypeEnumeration.SUPPORT,
            ContactPersonTypeEnumeration.TECHNICAL,
            ContactPersonTypeEnumeration.OTHER
        ).collect(Maps.toUnmodifiableOrderedMap(ContactPersonTypeEnumeration::toString, Function.identity()));

        public final ContactPersonTypeEnumeration type;
        public final String givenName;
        public final String surName;
        public final String email;

        public ContactInfo(ContactPersonTypeEnumeration type, String givenName, String surName, String email) {
            this.type = Objects.requireNonNull(type, "Contact Person Type is required");
            this.givenName = givenName;
            this.surName = surName;
            this.email = email;
        }

        private static ContactPersonTypeEnumeration getType(String name) {
            final ContactPersonTypeEnumeration type = TYPES.get(name.toLowerCase(Locale.ROOT));
            if (type == null) {
                throw new IllegalArgumentException(
                    "Invalid contact type " + name + " allowed values are " + Strings.collectionToCommaDelimitedString(TYPES.keySet())
                );
            }
            return type;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy