9

I am using Spring SAML in a multi-tenant application to provide SSO. Different tenants use different urls to access the application, and each has a separate Identity Provider configured. How do I automatically assign the correct Identity Provider given the url used to access the application?

Example:

Tenant 1: http://tenant1.myapp.com

Tenant 2: http://tenant2.myapp.com

I saw that I can add a parameter idp to the url (http://tenant1.myapp.com?idp=my.idp.entityid.com) and the SAMLContextProvider will pick the identity provider with that entity id. I developed a database-backed MetadataProvider that takes the tenant hostname as initialisation parameter to fetch the metadata for that tenant form the database linked to that hostname. Now I think I need some way to iterate over the metadata providers to link entityId of the metadata to the hostname. I don't see how I can fetch the entityId of the metadata, though. That would solve my problem.

MarcFasel
  • 1,080
  • 10
  • 19

2 Answers2

8

You can see how to parse available entityIDs out of a MetadataProvider in method MetadataManager#parseProvider. Note that generally each provider can supply multiple IDP and SP definitions, not just one.

Alternatively, you could further extend the ExtendedMetadataDelegate with your own class, include whatever additional metadata (like entityId) you wish, and then simply retype MetadataProvider to your customized class and get information from there when iterating data through the MetadataManager.

If I were you, I'd take a little bit different approach though. I would extend SAMLContextProviderImpl, override method populatePeerEntityId and perform all the matching of hostname/IDP there. See the original method for details.

Vladimír Schäfer
  • 15,375
  • 2
  • 51
  • 71
  • 3
    I created my own SAMLContextProvider and overrode the populatePeerIdentityId. That worked great. Once I was done I realised that the SAMLContextProvider is only used during SP initiated SSO. We mostly use IDP initiated SSO, so I needed to cover that as well. I ended up checking the peerEntityID of the incoming message against the IDP entityID that is configured for that tenant in my custom SAMLAuthenticationProvider. – MarcFasel Oct 28 '15 at 06:43
  • 1
    This feature of mapping identity provider to service provider is key to support multi-tenancy. Is this planned in upcoming releases? – MarcFasel Oct 28 '15 at 06:54
  • We'll see, the project depends on my free time (it's not sponsored by anyone) and there's not much of it. Improving multi-tenancy is something I would like to get done. – Vladimír Schäfer Oct 28 '15 at 09:04
4

At the time of writing, Spring SAML is at version 1.0.1.FINAL. It does not support multi-tenancy cleanly out of the box. I found another way to achieve multi-tenancy apart from the suggestions given by Vladimir above. It's very simple and straight-forward and does not require extension of any Spring SAML classes. Furthermore, it utilizes Spring SAML's in-built handling of aliases in CachingMetadataManager.

In your controller, capture the tenant name from the request and create an ExtendedMetadata object using the tenant name as the alias. Next create an ExtendedMetadataDelegate out of the ExtendedMetadata and initialize it. Parse the entity ids out of it and check if they exist in MetadataManager. If they don't exist, add the provider and refresh metadata. Then get the entity id from MetadataManager using getEntityIdForAlias().

Here is the code for the controller. There are comments inline explaining some caveats:

@Controller
public class SAMLController {

    @Autowired
    MetadataManager metadataManager;

    @Autowired
    ParserPool parserPool;

    @RequestMapping(value = "/login.do", method = RequestMethod.GET)
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response, @RequestParam String tenantName)
                                                        throws MetadataProviderException, ServletException, IOException{
        //load metadata url using tenant name
        String tenantMetadataURL = loadTenantMetadataURL(tenantName);

        //Deprecated constructor, needs to change
        HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(tenantMetadataURL, 15000);
        httpMetadataProvider.setParserPool(parserPool);

        //Create extended metadata using tenant name as the alias
        ExtendedMetadata metadata = new ExtendedMetadata();
        metadata.setLocal(true);
        metadata.setAlias(tenantName);

        //Create metadata provider and initialize it
        ExtendedMetadataDelegate metadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider, metadata);
        metadataDelegate.initialize();

        //getEntityIdForAlias() in MetadataManager must only be called after the metadata provider
        //is added and the metadata is refreshed. Otherwise, the alias will be mapped to a null
        //value. The following code is a roundabout way to figure out whether the provider has already
        //been added or not. 

        //The method parseProvider() has protected scope in MetadataManager so it was copied here         
        Set<String> newEntityIds = parseProvider(metadataDelegate);
        Set<String> existingEntityIds = metadataManager.getIDPEntityNames();

        //If one or more IDP entity ids do not exist in metadata manager, assume it's a new provider.
        //If we always add a provider without this check, the initialize methods in refreshMetadata()
        //ignore the provider in case of a duplicate but the duplicate still gets added to the list
        //of providers because of the call to the superclass method addMetadataProvider(). Might be a bug.
        if(!existingEntityIds.containsAll(newEntityIds)) {
            metadataManager.addMetadataProvider(metadataDelegate);
            metadataManager.refreshMetadata();
        }

        String entityId = metadataManager.getEntityIdForAlias(tenantName);

        return new ModelAndView("redirect:/saml/login?idp=" + URLEncoder.encode(entityId, "UTF-8"));
    }

    private Set<String> parseProvider(MetadataProvider provider) throws MetadataProviderException {
        Set<String> result = new HashSet<String>();

        XMLObject object = provider.getMetadata();
        if (object instanceof EntityDescriptor) {
            addDescriptor(result, (EntityDescriptor) object);
        } else if (object instanceof EntitiesDescriptor) {
            addDescriptors(result, (EntitiesDescriptor) object);
        }

        return result;

    }

    private void addDescriptors(Set<String> result, EntitiesDescriptor descriptors) throws MetadataProviderException {
        if (descriptors.getEntitiesDescriptors() != null) {
            for (EntitiesDescriptor descriptor : descriptors.getEntitiesDescriptors()) {
                addDescriptors(result, descriptor);
            }
        }

        if (descriptors.getEntityDescriptors() != null) {
            for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
                addDescriptor(result, descriptor);
            }
        }
    }

    private void addDescriptor(Set<String> result, EntityDescriptor descriptor) throws MetadataProviderException {
        String entityID = descriptor.getEntityID();
        result.add(entityID);
    }
}

I believe this directly solves the OP's problem of figuring out how to get the IDP for a given tenant. But this will work only for IDPs with a single entity id.

Marc-Andre
  • 912
  • 1
  • 15
  • 34
Pakman
  • 41
  • 4
  • Just want to point out that this solution doesn't work in a clustered environment unless you have sticky sessions for your users.... The initial request to /login.do adds the metadata provider to the JVM associated with that request, however the user could return to the app on another JVM which isn't aware of the IDP that started the authentication process... – danw Oct 06 '17 at 04:01
  • Hello Mr @danw , what would be a workaround for clustered environments but allowing dynamic registration? – Jeancarlo Fontalvo Nov 11 '22 at 14:33