Search code examples
sustainsys-saml2

Sustainsys.Saml2 MVC multitenant implementation in .NET 4.8 IDX10214: Audience validation failed


We recently migrated our applications from BasicAuth to SAML2. So we have a multitenant MVC implementation in .NET 4.8 of Sustaninsys.SAML2 which is working fine on low load. But if many users sign in at the same time, something is getting mixed up and this error is thrown in ACS method when running the AcsCommand:

IDX10214: Audience validation failed. Audiences: 'tenant_a'. Did not match: validationParameters.ValidAudience: 'tenant_b' or validationParameters.ValidAudiences: 'null'.

When going through logs it looks like sessions / cookies or whatever get mixed up. So EntityId and ReturnUrl of user1 for tenant_a is suddenly routed to user2. And user2's EntityId and ReturnUrl for tenant_b is ending up at user1. But the context stays at the original tenant. Which is why error is thrown.

Since we need some custom logic in the Saml2Controller we copied most of it from the SAML2 Github and extended it. So SignIn:

public ActionResult SignIn()
    {
        Options.SPOptions.ReturnUrl = this.GetReturnUrl();
        Options.SPOptions.Saml2PSecurityTokenHandler = null;
        Options.SPOptions.EntityId = new EntityId(GetClientId());

        var result = CommandFactory.GetCommand(CommandFactory.SignInCommandName).Run(
            Request.ToHttpRequestData(),
            Options);

        result.ApplyCookies(Response, true);
        return result.ToActionResult();
    }

And Acs:

public ActionResult Acs()
    {
        // This is where error occures
        result = CommandFactory.GetCommand(CommandFactory.AcsCommandName).Run(
            Request.ToHttpRequestData(),
            Options);

        // lots of custom code

        result.SignInOrOutSessionAuthenticationModule();
        result.ApplyCookies(Response, true);
        return result.ToActionResult();
    }

The web.config for all our tenants look identical since EntityId and ReturnUrl are assigned in the controller:

<sustainsys.saml2 authenticateRequestSigningBehavior="Always">
    <nameIdPolicy allowCreate="true" format="Persistent">
    </nameIdPolicy>
    <identityProviders>
        <add entityId="https://idp.de/auth/realms/zzz" 
            logoutUrl="https://idp.de/auth/realms/zzz/protocol/saml" 
            signOnUrl="https://idp.de/auth/realms/zzz/protocol/saml" 
            allowUnsolicitedAuthnResponse="true" 
            relayStateUsedAsReturnUrl="true" 
            binding="HttpPost">
            <signingCertificate fileName="~/App_Data/secret.cer" />
        </add>
    </identityProviders>
    <federations>
        <add metadataLocation="~/App_Data/" allowUnsolicitedAuthnResponse="true" />
    </federations>
    <serviceCertificates>
        <add storeName="My" storeLocation="LocalMachine" findValue="123456789" x509FindType="FindBySerialNumber" use="Signing" />
    </serviceCertificates>
</sustainsys.saml2>

Does anyone see where the mix up happens? Or is this an issue from the IDP which is Red Hat Keycloak.

UPDATE

In our Saml2Controller we use IOptions from the refrence implementation:

public static IOptions Options
    {
        get { return Sustainsys.Saml2.Mvc.Saml2Controller.Options; }
        set { Sustainsys.Saml2.Mvc.Saml2Controller.Options = value; }
    }

Solution

  • With a multi tenant setup and the MVC controller library, the best solution is to add one IdentityProvider object per tenant. If you do not know/want to create them all on startup, you can use get SelectIdentityProvider and GetIdentityProvider notifications to supply them from your own store.

    Messing with the options per request is not how the library was designed to work.