3

I'm working with a multi-site instance running Sitecore 9.2 where I have configured a custom identity provider based on the documentation here https://doc.sitecore.com/developers/92/sitecore-experience-manager/en/configure-federated-authentication.html

The identity provider is only intended to be used by one of the sites in this multi-site instance so I have configured the identityProvidersPerSites entry accordingly:

<identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
  <mapEntry name="my-site" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication" resolve="true">
    <sites hint="list">
      <site>my-site</site>
    </sites>

    <identityProviders hint="list:AddIdentityProvider">
      <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='Okta']" />
    </identityProviders>

  </mapEntry>
</identityProvidersPerSites>

The identity provider configuration:

<identityProviders hint="list:AddIdentityProvider">
    <identityProvider id="Okta" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication" >
        <param desc="name">$(id)</param>
        <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
        <caption>Log in with Okta</caption>
        <icon>/assets/okta.png</icon>
        <domain>mysite</domain>
        <transformations hint="list:AddTransformation">
        ...
        </transformations>
    </identityProvider>
</identityProviders>

And the implementation:

namespace Demo
{
    public class OktaIdentityProvider : IdentityProvidersProcessor
    {
        protected override string IdentityProviderName => "Okta";

        // OAuth provider setting
        private string ClientId => Settings.GetSetting(OktaSettings.ClientId, "");
        private string ClientSecret => Settings.GetSetting(OktaSettings.ClientSecret, "");
        private string Authority => Settings.GetSetting(OktaSettings.Authority, "");
        private string OAuthTokenEndpoint => Settings.GetSetting(OktaSettings.OAuthTokenEndpoint, "");
        private string OAuthUserInfoEndpoint => Settings.GetSetting(OktaSettings.OAuthUserInfoEndpoint, "");
        private string OAuthRedirectUri => Settings.GetSetting(OktaSettings.OAuthRedirectUri, "");

        private readonly string OpenIdScope = OpenIdConnectScope.OpenIdProfile + " email";
        private readonly string idToken = "id_token";
        private readonly string accessDeniedRelativePath = "/my-site/access-denied";

        protected IdentityProvider IdentityProvider { get; set; }

        public OktaIdentityProvider(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration, ICookieManager cookieManager, BaseSettings settings)
            : base(federatedAuthenticationConfiguration, cookieManager, settings)
        { }

        protected override void ProcessCore(IdentityProvidersArgs args)
        {
            Assert.ArgumentNotNull(args, "args");

            args.App.UseKentorOwinCookieSaver();
            args.App.UseOpenIdConnectAuthentication(CreateOpenIdConnectAuthenticationOptions(args));
        }

        private OpenIdConnectAuthenticationOptions CreateOpenIdConnectAuthenticationOptions(IdentityProvidersArgs args)
        {
            IdentityProvider = this.GetIdentityProvider();

            var options = new OpenIdConnectAuthenticationOptions
            {
                ClientId = ClientId,
                ClientSecret = ClientSecret,
                Authority = Authority,
                RedirectUri = GetCallbackUrl(args),
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                Scope = OpenIdScope,
                AuthenticationType = IdentityProvider.Name,
                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name"
                },
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = OnAuthenticationFailed,
                    AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                    RedirectToIdentityProvider = notification =>
                    {
                        if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                        {
                            // If signing out, add the id_token_hint
                            var idTokenClaim = notification.OwinContext.Authentication.User.FindFirst(idToken);

                            if (idTokenClaim != null)
                                notification.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
                        }

                        return Task.CompletedTask;
                    }
                }
            };

            return options;
        }

        ...
    }
}

The site (my-site) works as intended and I can log in using Okta. The issue is that adding the provider has the unintended consequence of redirecting 401 responses for all the other sites in this instance to Okta as well. My assumption was that the IdentityProvidersPerSites entry would ensure that this would not happen but I must be missing something about how this is indented to work.

To demonstrate the behavior I'm referring to I have added a separate site called foobar. Imagine I have just the to sites configured like this:

<site name="my-site" hostName="*" virtualFolder="/my-site" physicalFolder="/my-site" rootPath="/sitecore/content/My Site/" contentStartItem="/sitecore/content/My Site/Home" startItem="/home" language="en" contentLanguage="en" database="web" domain="mysite" ... />
<site name="foobar" hostName="*" virtualFolder="/foo" physicalFolder="/foo" rootPath="/sitecore/content/Foobar/" contentStartItem="/sitecore/content/Foobar/Home" startItem="/home" language="en" contentLanguage="en" database="web" domain="foo" />

I've then added a controller rendering with the sole purpose of returning a 401 response:

using Sitecore.Mvc.Controllers;
using System.Web.Mvc;

namespace Demo
{
    public class FooController : SitecoreController
    {
        [HttpGet]
        public ActionResult SansAccess()
        {
            return new HttpUnauthorizedResult("no foo for you");
        }
    }
}

After adding this rendering to the homepage of foobar, my expectation is that when I go to /foo I will get a default 401 page. Instead I see that the 401 is picked up by the Owin middleware, which then redirects (302) the request to Okta.

Has anyone come across this and can point to what I'm missing?


Update

The Owin middleware appears to intercept all 401 responses inside the ApplyResponseChallengeAsync method of Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationHandler. The reason it decides to use the Okta provider I've added is because, by default, the provider is configured with AuthenticationMode set to Active.

Changing the mode to Passive resolves the issue of 401 responses from other sites getting intercepted. I manually redirect login requests for my-site to /identity/login/my-site/Okta which sends the request off to Okta.

The issue now is that after I log in to Okta, an exception is generated and trapped by the AuthenticationFailed method of the provider. The exception is referring to nonce beeing null (IDX21323).

My guess is that there are additional steps that's required when the provider is set to passive but so far I haven't been able to identify what is required.

1
  • you could add custom redirect logic in the OnAuthenticationFailed() , but my guess it would require some hardcoded logic based on the exception Id
    – Abhay Dhar
    Commented May 11, 2020 at 13:45

1 Answer 1

1

I ended up adding a check inside RedirectToIdentityProvider to stop processing if the request context was invalid for my site. I had to check for the presence of the site name in the URL as well as the identity provider callback URL.

RedirectToIdentityProvider = notification =>
{
    if (!IsValidPath(notification.Request))
    {
        notification.HandleResponse();
        return Task.CompletedTask;
    }
    ...
}

private bool IsValidPath(IOwinRequest request)
{
    return request.Path.StartsWithSegments(new PathString("/identity/externallogincallback"))
        || request.Uri.AbsoluteUri.Contains("my-site");
}

It feels quite dirty as the OktaProvider is now essentially hard-coded to work with just this one site. I'm planning on at least making a small improvement to this to either get the site name from the config or perhaps test against something else. For now this does what I want in that 401 responses from other sites are left intact for those sites to handle how they see fit.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.