EpiServer CMS integration with ADFS

ZZ
ZZ
Vote:
 

I am trying to integrate ADFS in our EpiServer CMS site using OWIN.

The requirement of ADFS is arised due to the front-end users login flow will be using OIDC. And when we changed authentication mode to "None", then we can't using existing log in flow for EPiServer admin site., which used AD.

I have followed this article : https://world.optimizely.com/documentation/developer-guides/archive/cms/security2/federated-security/

and the site it able to redirect to ADFS, when entering xxx.com/episerver.

I am getting following error

2022-02-03 14:59:52,650 [6] ERROR EPiServer.Async.Internal.DefaultTaskExecutor: Background task with resulttype 'no result type defined' faulted.
System.AggregateException: One or more errors occurred. ---> System.ArgumentNullException: Value cannot be null.
Parameter name: userName
   at EPiServer.Security.Internal.DefaultSynchronizedUsersRepository.SynchronizeUserAndClaims(String userName, String roleClaimType, IEnumerable`1 claims)
   at EPiServer.Security.Internal.DefaultSynchronizedUsersRepository.<>c__DisplayClass9_0.<SynchronizeAsync>b__0()
   at EPiServer.Async.TaskExecutor.<>c__DisplayClass9_0.<Start>b__0(CancellationToken t)
   at EPiServer.Async.TaskExecutor.<>c__DisplayClass15_0.<Start>b__0(CancellationToken t)
   at EPiServer.Async.Internal.DefaultTaskExecutor.<>c__DisplayClass11_0`1.<CreateWorkAction>b__0()
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---

The claims which I am getting from ADFS & Response.StatusCode is 401 after authenticated: 

Any help would be appreciated

#271092
Edited, Feb 03, 2022 15:25
Vote:
 

I would expect you not to be authorized to get in to the editor/admin unless you have the right claims/groups coming back

Out of the box there's 3 roles setup in a default configuration 

  • WebEditors - Editor can edit the site.
  • WebAdmins - Admins can manage roles and perform all task actions
  • Administrators - Admin the site. (can do a few extra things that WebAdmins can't)

So I'd expect unless you've modified the virtual roles configuration this you'd have the minimum role WebEditors as a role claim which would need to be setup in your directory for the login your using

#271139
Feb 04, 2022 9:44
ZZ
Vote:
 

Thanks a lot. After gettings role claims from ADFS, I can finally login to admin site:

We had allow roles in our web.config and after getting EPiServerxx1 claim from ADFS i was allowed to login

 <authorization>
        <allow roles="WebEditors, EPiServerxx1, EPiServerxx2, EPiServerxx3" />
      </authorization>

We are using ADFS for our internal employes while front-end users will be using OIDC implemented in startup.cs. 

The ODIC is set to passive AuthenticationMode.Passive, to be only kicked when user clicks login button. If we also set ADFS to passive then how can this be kicked in when internal employes try to access xx.com/episerver ? We don't want unauthenticated request kicks any of the login flows 

#271142
Feb 04, 2022 11:19
Vote:
 

From what I remember although it's been a while this is achieved by setting the Path as they are doing in https://world.optimizely.com/documentation/developer-guides/archive/cms/security2/configuring-mixed-mode-owin-authentication-10-11/  

I think setting the path or ADFS to the default authentication mechansim but I've not used ADFS for a long time so not 100% it has the same properties to set it all up.

#271144
Feb 04, 2022 13:14
ZZ
Vote:
 

Hi Scott

Thanks for your reply.

Below code is what I am trying to implement as we are using OIDC (as one of the flow others are MVC post) for front-end users and ADFS forn back-end users. I have created a get method on one of the page which calls challgenge for the ADFS. (I don't know if there is better way to do it)

 HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties()
            {
                RedirectUri = "/episerver",
                
            }, WsFederationAuthenticationDefaults.AuthenticationType);
            return new UnautorizedResult();

The problem I am facing right now is that when the front-end user is already logged and in the same browser back-end users try to go to episerver admin side (by invoking actionmethod). The CookieAuthentication takes over after ADFS validation and the user is prevented redirect to  EPIServer admin page. I think its trying to redirect to admin page in front-end user context.(which ofcourse doesn't have access to admin page).

How can I redirect to EPiServer admin page in backend user context (ADFS) ?

using Microsoft.Owin;
using Owin;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Web.Helpers;
using EPiServer;
using EPiServer.Security;
using EPiServer.ServiceLocation;
//using EPiServer.Cms.UI.AspNetIdentity;
using Microsoft.AspNet.Identity;
using Microsoft.IdentityModel.Logging;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.WsFederation;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.WsFederation;
using Newtonsoft.Json.Linq;

[assembly: OwinStartup(typeof(MyApplication.Startup))]

namespace MyApplication
{
    public class Startup
    {
        private readonly string _clientId = "xxx";
        private readonly string _redirectUri = "xxx";
        private readonly string _postLogoutRedirectUri = "xxxx";
        private readonly string _authority = "xxx";
        private readonly string _clientSecret = "xxx";
        private const string LogoutUrl = "xxx";


        public void Configuration(IAppBuilder app)
        {
            IdentityModelEventSource.ShowPII = true;
            ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Ssl3 | SecurityProtocolType.Ssl3;
            //// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
            ConfigureAuth(app);
        }

        public void ConfigureAuth(IAppBuilder app)
        {

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/LogMeIn/"),
                LogoutPath = new PathString("/"),
            });

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = _clientId,
                ClientSecret = _clientSecret,
                Authority = _authority,
                RedirectUri = _redirectUri,
                PostLogoutRedirectUri = _postLogoutRedirectUri,
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                Scope = "xxx",
                SaveTokens = true,
                AuthenticationMode = AuthenticationMode.Passive,

                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = notification =>
                    {
                       xxxxx
                        return Task.CompletedTask;
                    },

                    // Retrieve an access token from the remote token endpoint
                    // using the authorization code received during the current request.
                    AuthorizationCodeReceived = async notification =>
                    {
                       xxxxxx
                    },

                    // Attach the id_token stored in the authentication cookie to the logout request.
                    RedirectToIdentityProvider = notification =>
                    {
                        xxxxxx

                        return Task.CompletedTask;
                    },

                }
            });







            ////Enable cookie authentication, used to store the claims between requests
          //  app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
                CookieName = "xxx",
                AuthenticationMode = AuthenticationMode.Passive
            });

            //Enable federated authentication
            app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
            {
                //Trusted URL to federation server meta data
                MetadataAddress = "xxx",
                //Value of Wtreal must *exactly* match what is configured in the federation server
                Wtrealm = "xxx/",

                AuthenticationMode = AuthenticationMode.Passive,


                Notifications = new WsFederationAuthenticationNotifications()
                {
                    RedirectToIdentityProvider = (ctx) =>
                    {
                        //  To avoid a redirect loop to the federation server send 403 when user is authenticated but does not have access
                        if (ctx.OwinContext.Response.StatusCode == 401 && (ctx.OwinContext.Authentication.User.Identity.AuthenticationType == WsFederationAuthenticationDefaults.AuthenticationType && ctx.OwinContext.Authentication.User.Identity.IsAuthenticated))
                        {
                            ctx.OwinContext.Response.StatusCode = 403;
                            ctx.HandleResponse();
                        }
                        //XHR requests cannot handle redirects to a login screen, return 401
                        if (ctx.OwinContext.Response.StatusCode == 401 && IsXhrRequest(ctx.OwinContext.Request))
                        {
                            ctx.HandleResponse();
                        }
                        return Task.FromResult(0);
                    },
                    //AuthenticationFailed = (ctx) =>
                    //{
                    //    if (ctx.OwinContext.Response.StatusCode == 401)
                    //    {

                    //    }

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

                    //MessageReceived = (ctx) =>
                    //{
                    //    if (ctx.OwinContext.Response.StatusCode == 401)
                    //    {

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

                    SecurityTokenValidated = (ctx) =>
                    {

                        //Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
                        var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
                        if (redirectUri.IsAbsoluteUri)
                        {
                            ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
                        }
                        //Sync user and the roles to EPiServer in the background
                        ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
                        return Task.FromResult(0);
                    }
                }
            });
            //Add stage marker to make sure WsFederation runs on Authenticate (before URL Authorization and virtual roles)
            app.UseStageMarker(PipelineStage.Authenticate);

            //Remap logout to a federated logout
            app.Map(LogoutUrl, map =>
            {
                map.Run(ctx =>
                {
                    ctx.Authentication.SignOut();
                    return Task.FromResult(0);
                });
            });
          
}
#271289
Edited, Feb 07, 2022 11:07
ZZ
Vote:
 
#271290
Edited, Feb 07, 2022 11:13
ZZ
Vote:
 

I found out that I had to set AuthenticatioMode to Active in second cookie authentication , so now its working:

app.UseCookieAuthentication(new CookieAuthenticationOptions { 
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
CookieName = "xxx", AuthenticationMode = 
AuthenticationMode.Active });
       public void Configuration(IAppBuilder app)
    {
        //IdentityModelEventSource.ShowPII = true;
        ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;
        //// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888

        ConfigureMemberAuth(app);

        ConfigureAdminAuth(app);
    }

    private void ConfigureMemberAuth(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/logmein/"),
            LogoutPath = new PathString("/"),
            AuthenticationMode = AuthenticationMode.Active

        });

        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

       _ = app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = ApplicationSettings.OIDCClientId,
            ClientSecret = ApplicationSettings.OIDCClientSecret,
            Authority = ApplicationSettings.OIDCAuthority,
            RedirectUri = $"{ApplicationSettings.Domain}{_oidcRedirectMethod}",
            PostLogoutRedirectUri = ApplicationSettings.Domain,
            ResponseType = OpenIdConnectResponseType.CodeIdToken,
            Scope = "xxx",
            SaveTokens = true,
            AuthenticationMode = AuthenticationMode.Passive,
            AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
            
            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthenticationFailed = notification =>
                {
                    if (string.Equals(notification.ProtocolMessage.Error, "xxx", StringComparison.Ordinal))
                    {
                        notification.HandleResponse();

                        if (string.Equals(notification.ProtocolMessage.ErrorDescription, "xxx", StringComparison.Ordinal))
                            notification.Response.Redirect(ApplicationSettings.Domain);
                        else
                        {
                            var errorPage = UrlResolver.GetUrl(PageHelper.GetAdminPage().LoginBox.NemIDErrorPage.GetFriendlyUrl());
                            notification.Response.Redirect(errorPage);
                        }
                    }

                    return Task.CompletedTask;
                },

                // Retrieve an access token from the remote token endpoint
                // using the authorization code received during the current request.
                AuthorizationCodeReceived = async notification =>
                {
                    using (var client = new HttpClient())
                    {
                        var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
                        var tokenEndpointResult = await ExchangeCodeForTokens(notification, client, configuration);

                        // Add the identity token to the returned ClaimsIdentity to make it easier to retrieve.
                        notification.AuthenticationTicket.Identity.AddClaim(new Claim(
                            type: OpenIdConnectParameterNames.IdToken,
                            value: tokenEndpointResult.Value<string>(OpenIdConnectParameterNames.IdToken)));

                        // Retrieve the claims from UserInfo endpoint using the access token as bearer token.
                        var accesstoken = tokenEndpointResult.Value<string>(OpenIdConnectParameterNames.AccessToken);
                        var userInfoEndpointResult = await UserInfoEndpointClaims(notification, client, configuration, accesstoken);

                        //Security note: It is important to verify that the sub claim from ID token matches the sub claim in the UserInfo response
                        var userinfoSub = userInfoEndpointResult["xx"].Value<string>();
                        var idtokenSub = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
                        if (userinfoSub == idtokenSub)
                        {
                            //add claims from UserInfo endpoint to identity
                            foreach (var entry in userInfoEndpointResult)
                            {
                                if (!notification.AuthenticationTicket.Identity.HasClaim(c => c.Type == entry.Key))
                                {
                                    notification.AuthenticationTicket.Identity.AddClaim(new Claim(
                                        type: entry.Key,
                                        value: entry.Value.ToString()));
                                }
                            }
                            // Add access token to claims.
                            notification.AuthenticationTicket.Identity.AddClaim(new Claim(
                                OpenIdConnectParameterNames.AccessToken,
                                 accesstoken));
                        }
                    }
                },

                // Attach the id_token stored in the authentication cookie to the logout request.
                RedirectToIdentityProvider = notification =>
                {
                    if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                    {
                        var token = notification.OwinContext.Authentication.User?.FindFirst(OpenIdConnectParameterNames.IdToken);
                        if (token != null)
                        {
                            notification.ProtocolMessage.IdTokenHint = token.Value;
                        }

                        notification.Response.Redirect(ApplicationSettings.Domain);
                    }

                    return Task.CompletedTask;
                },

            }
        });

    }

    private void ConfigureAdminAuth(IAppBuilder app)
    {



        //Enable cookie authentication, used to store the claims between requests

        app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
            CookieName = WsFederationAuthenticationDefaults.CookieName,
            AuthenticationMode = AuthenticationMode.Active,
            CookieHttpOnly = true,
        });

        //Enable federated authentication
        _ = app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
        {
            //Trusted URL to federation server meta data
            MetadataAddress = ApplicationSettings.MetaDataAddress,
            //Value of Wtreal must *exactly* match what is configured in the federation server
            Wtrealm = ApplicationSettings.RelyPartyUri,
            AuthenticationMode = AuthenticationMode.Passive,

            Notifications = new WsFederationAuthenticationNotifications()
            {
                RedirectToIdentityProvider = (ctx) =>
                {
                    //  To avoid a redirect loop to the federation server send 403 when user is authenticated but does not have access
                    if (ctx.OwinContext.Response.StatusCode == 401 && (ctx.OwinContext.Authentication.User.Identity.AuthenticationType == WsFederationAuthenticationDefaults.AuthenticationType && ctx.OwinContext.Authentication.User.Identity.IsAuthenticated))
                    {
                        ctx.OwinContext.Response.StatusCode = 403;
                        ctx.HandleResponse();
                    }
                    //XHR requests cannot handle redirects to a login screen, return 401
                    if (ctx.OwinContext.Response.StatusCode == 401 && IsXhrRequest(ctx.OwinContext.Request))
                    {
                        ctx.HandleResponse();
                    }
                    return Task.FromResult(0);
                },
                SecurityTokenValidated = (ctx) =>
                {

                    //Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
                    var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
                    if (redirectUri.IsAbsoluteUri)
                    {
                        ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
                    }
                    //Sync user and the roles to EPiServer in the background
                    ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
                    return Task.FromResult(0);
                },

            }
        });
        //Add stage marker to make sure WsFederation runs on Authenticate (before URL Authorization and virtual roles)
        app.UseStageMarker(PipelineStage.Authenticate);

        // Remap logout to a federated logout
        app.Map(LogoutUrl, map =>
        {
            map.Run(ctx =>
            {
                ctx.Authentication.SignOut();
                ctx.Response.Redirect(ApplicationSettings.Domain);
                return Task.FromResult(0);
            });
        });
    }
#271298
Edited, Feb 07, 2022 14:14
Scott Reed - Feb 07, 2022 14:25
Excellent, yes I've seen on mixed mode needing to set it this way. Apologies for the delay, busy day at work
ZZ - Feb 10, 2022 10:24
Thanks a lot. I appreciate your help. After finalizing the code I can see that front-end user login (OIDC) is not working. Because when the calls return to client it thinks as this is also Authentication type = "Federation"
When I out comment this line: app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType); then client login works but EPiServer login is no longer working (even though back-end user is redirected to ADFS)
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.