Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sitecore 9 Federated Authentication with IdentityServer3, Endless Loop

I've been struggling to get Federated Authentication working with Sitecore 9 using IdentityServer 3 as the IDP. I've followed the example seen in http://blog.baslijten.com/enable-federated-authentication-and-configure-auth0-as-an-identity-provider-in-sitecore-9-0/ for Auth0, and have converted that to IDS3. But what I experienced was an endless loop between the IDP and Sitecore.

It seems that upon authentication, IdentityServer 3 redirects back to Sitecore, which fails to convert the authentication into a cookie. I'm left with a .nonce cookie instead. Sitecore, not seeing an authenticated user, redirects over to the IDP and this continues until I stop the process.

My IdentityProviderProcessor (with dummy values):

using System.Threading.Tasks;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Configuration;
using Sitecore.Owin.Authentication.Pipelines.IdentityProviders;
using Sitecore.Owin.Authentication.Services;

namespace xx.xxxx.SC.Foundation.Authentication
{
    public class IdentityProviderProcessor : IdentityProvidersProcessor
    {
        public IdentityProviderProcessor(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration) : base(federatedAuthenticationConfiguration)
        {

        }

        /// <summary>
        /// Identityprovidr name. Has to match the configuration
        /// </summary>
        protected override string IdentityProviderName
        {
            get { return "ids3"; }
        }

        protected override void ProcessCore(IdentityProvidersArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            IdentityProvider identityProvider = this.GetIdentityProvider();
            string authenticationType = this.GetAuthenticationType();

            args.App.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies"
            });

            args.App.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                Authority = "xxxx",
                ClientId = "xxxx",
                Scope = "openid profile xxxx",
                RedirectUri = "xxxx",
                ResponseType = "id_token token",
                SignInAsAuthenticationType = "Cookies",
                UseTokenLifetime = false,
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    SecurityTokenValidated = (context) =>
                    {
                        var identity = context.AuthenticationTicket.Identity;

                        foreach (Transformation current in identityProvider.Transformations)
                        {
                            current.Transform(identity, new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
                        }

                        var virtualUser = Sitecore.Security.Authentication.AuthenticationManager.BuildVirtualUser("xxxx\\[email protected]", true);

                        // You can add roles to the Virtual user
                       virtualUser.Roles.Add(Sitecore.Security.Accounts.Role.FromName("extranet\\MyRole"));

                        // You can even work with the profile if you wish
                        virtualUser.Profile.SetCustomProperty("CustomProperty", "12345");
                        virtualUser.Profile.Email = "[email protected]";
                        virtualUser.Profile.Name = "My User";

                        // Login the virtual user
                        Sitecore.Security.Authentication.AuthenticationManager.LoginVirtualUser(virtualUser);

                        return Task.FromResult(0);
                    },
                },
            });
        }
    }
}

And my config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore role:require="Standalone or ContentDelivery or ContentManagement">

    <pipelines>
      <owin.identityProviders>
        <!-- Processors for coniguring providers. Each provider must have its own processor-->
        <processor type="xx.xxxx.SC.Foundation.Authentication.IdentityProviderProcessor, xx.xxxx.SC.Foundation.Authentication" resolve="true" />
      </owin.identityProviders>
    </pipelines>

    <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
      <!--Provider mappings to sites-->
      <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
        <!--The list of providers assigned to all sites-->
        <mapEntry name="all sites" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
          <sites hint="list">
            <sites hint="list">
              <site>modules_website</site>
              <site>website</site>
            </sites>
          </sites>
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='ids3']" />
          </identityProviders>
          <externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">

            <param desc="isPersistentUser">false</param>

          </externalUserBuilder>
        </mapEntry>

      </identityProvidersPerSites>

      <!--Definitions of providers-->
      <identityProviders hint="list:AddIdentityProvider">
        <!--Auth0 provider-->
        <identityProvider id="ids3" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">$(id)</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <!--This text will be showed for button-->
          <caption></caption>
          <icon></icon>
          <!--Domain name which will be added when create a user-->
          <domain>sitecore</domain>
          <!--list of identity transfromations which are applied to the provider when a user signin-->
          <transformations hint="list:AddTransformation">
            <!--SetIdpClaim transformation-->
            <transformation name="set idp claim" ref="federatedAuthentication/sharedTransformations/setIdpClaim" />
          </transformations>
        </identityProvider>
      </identityProviders>
      <sharedTransformations hint="list:AddTransformation">
      </sharedTransformations>
    </federatedAuthentication>
  </sitecore>
</configuration>

Note that the only way I could accomplish this was to create a VirtualUser on validation. Given the near-total lack of documentation on this topic, I'm not sure if this is a needed step, or if something is wrong with the way I've set this up.

For now, the VirtualUser works like a champ and we'll likely keep this in place. I would like to know, however, is the creation of a VirtualUser here required or did I do something wrong?

Thanks for any input.

like image 373
William Cooper Avatar asked Nov 21 '17 04:11

William Cooper


1 Answers

I see several issues in your overall configuration, but the most important is the first one (and the workaround must be removed of course):

  1. The implementation of the IdentityProvidersProcessor must contain only a middleware to configure authentication to external provider, like UseOpenIdConnectAuthentication or UseAuth0Authentication or UseFacebookAuthentication. It must not configure the cookie authentication, because it is already done for you in the Sitecore.Owin.Authentication.config:

    <pipelines>
        ...
        <owin.initialize>
            ...
            <processor type="Sitecore.Owin.Authentication.Pipelines.Initialize.CookieAuthentication, Sitecore.Owin.Authentication"
                       resolve="true" patch:before="processor[@method='Authenticate']" />
            ...
        </owin.initialize>
    </pipelines>
    

    Note: if you need to handle any of OWIN cookie authentication events, just use corresponding pipeline owin.cookieAuthentication.*

    Actions:

    1. Remove your UseCookieAuthentication middleware.
    2. Use string authenticationType = this.GetAuthenticationType(); to set the SignInAsAuthenticationType property of the OpenIdConnectAuthenticationOptions object (the authenticationType variable is unused in your code).
  2. Not an issue, but there is an extension method exists in the Sitecore.Owin.Authentication.Extensions namespace that could replace the whole foreach statement:

    notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, identityProvider));
    
  3. You tried to manually build virtual users and authenticate them, but Sitecore takes care of it, when you fix the first issue.

    Action: replace SecurityTokenValidated handler with:

    SecurityTokenValidated = notification =>
    {
        notification.AuthenticationTicket.Identity
          .ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, identityProvider));
        return Task.CompletedTask;
    }
    
  4. Another "not an issue, but..": it was the case for the non-RTM Sitecore 9.0 builds, but now you don't need to manually specify the setIdpClaim transformation for every identity provider. All transformation, that are specified in the federatedAuthentication/sharedTransformations node, are automatically executed for all identity providers.

    Action: remove extra transformation

    <!--SetIdpClaim transformation-->
    <transformation name="set idp claim" ref="federatedAuthentication/sharedTransformations/setIdpClaim" />
    
  5. Make sure you have a proper value in the RedirectUri property. It must be presented in the RedirectUris property of the appropriate IdentityServer3.Core.Models.Client object.


When everything is done, your external user will be authenticated, but it will not have any assigned roles yet. User has assigned roles when any of these is true:

  1. User exists in the DB and it has assigned roles there.
  2. User's ClaimsIdentity object has claims with "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" type.

The best way to assign role claims is to use the claim transformations.

Example: assume that you want to assign a sitecore\Developer role to all Azure AD users that are included in the group with an object id 3e12be6e-58af-479a-a4dc-7a3d5ef61c71. The claim transformation for the AzureAD identity provider will look like this:

    <transformation name="developer role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
        <sources hint="raw:AddSource">
            <claim name="groups" value="3e12be6e-58af-479a-a4dc-7a3d5ef61c70" />
        </sources>
        <targets hint="raw:AddTarget">
            <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="sitecore\Developer " />
         </targets>
    </transformation>

Important note: AzureAD does not send back the group claims by default. You need to set the value of groupMembershipClaims to the SecurityGroup in the app manifest.


Now your user has roles, but it's profile is not filled up. Unlike the claim transformations, the property mappings configuration is shared between all identity providers. The general idea behind that is to apply personalized claim transformations for different identity providers and receive the "normalized" ClaimsIdentity with claim types that you expect to see.

For instance, first provider gives you "second name" claim, second one gives "surname" and the third one "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname". Then you write two claim transformations for corresponding providers that maps "second name" to "surname" and "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" to "surname" as well:

in the fist provider config node:

    <transformation name="surname" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
        <sources hint="raw:AddSource">
            <claim name="second name" />
        </sources>
        <targets hint="raw:AddTarget">
            <claim name="surname" />
        </targets>
    </transformation>

in the second provider config node:

    <transformation name="surname" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
        <sources hint="raw:AddSource">
            <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
        </sources>
        <targets hint="raw:AddTarget">
            <claim name="surname" />
        </targets>
    </transformation>

Since now, you have a normalized ClaimsIdentity, you can write one property mapping:

    <map name="surname" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
      <data hint="raw:AddData">
        <source name="surname" />
        <target name="Surname" />
      </data>
    </map>

the user profile data could be read with user.Profile["Surname"].

Note: it's possible and easy to implement custom claim transformations and property mappings if you need them.

like image 167
Vyacheslav Pritykin Avatar answered Nov 10 '22 06:11

Vyacheslav Pritykin