Loading...
Area: Episerver Commerce
Applies to versions: 11.5.1 and higher

Support for OpenID Connect in Episerver Commerce

This topic explains how to configure mixed mode authentication with OpenID Connect and Membership provider. That is, how an Episerver Commerce application can authenticate admins/editors and shoppers/users through different authentication components. To accomplish these goals, you need to

  • use OpenID Connect to sign-in admins from a single/multi-tenant environment. To accomplish this, use the ASP.Net OpenID Connect OWIN middleware.
  • use a built-in Membership provider to authenticate users/shoppers.

You need to follow these steps in both your front-end site and in Commerce Manager.

About Azure Active Directory and OpenID

Azure Active Directory (Azure AD) is Microsoft's multi-tenant cloud-based directory and identity management service. Azure AD provides single sign-on (SSO) access to many cloud-based SaaS applications, and includes a full suite of identity management capabilities.

OAuth is an open standard for authorization used by Azure AD. OpenID Connect is built on top of OAuth and extends it. You can use OpenID Connect as an authentication protocol rather than just an authorization protocol.

For more information about the protocols, see Authentication Scenarios for Azure ADIntegrate Azure AD into a web application using OpenID Connect and Authorize access to web applications using OpenID Connect and Azure Active Directory.

Prerequisites

Create and configure an Azure Active Directory Application

Configure both sites to support HTTPS

Configuring mixed mode authentication in the front-end site 

1. Disable the Role Provider

Iweb.config, disable the built-in Role provider. But leave the profile system enabled, since edit and admin views use it for language settings. You can use another profile system on the website.

<authentication mode="None" />
...
<roleManager enabled="false">
  <providers>
    <clear />
  </providers>
</roleManager>
...
<membership>
  <providers>
    <clear />
  </providers>
</membership>

2. Configure Episerver to support claims

Enable claims on virtual roles by setting the addClaims property. Also, add the provider SynchronizingRolesSecurityEntityProvider for security entities, which is used by the set access rights dialog box, content approval, and impersonating users among other things.

Users and groups are synchronized to custom Episerver tables in the database when a user is authenticated (see ISynchronizingUserService in the code example below). There is no background synchronization, so for a role change to take effect, the user must login to the site again.

<episerver.framework>
<securityEntity>
  <providers>
    <add name="SynchronizingProvider" 
         type="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer"/>
  </providers>
</securityEntity>
<virtualRoles addClaims="true">
  //existing virtual roles
</virtualRoles>

You can also replace virtual roles with roles defined in the manifest to delegate this control from the application to Azure. See Adding application roles in Azure Active Directory.

3. Install NuGet packages

In Visual Studio, open Package Manager and install the following packages.

Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Host.SystemWeb
Update-Package Microsoft.IdentityModel.Protocol.Extensions -Safe
 

Note: Always use Microsoft.IdentityModel.Protocol.Extensions package version 1.0.2 or later. Previous versions contain a critical bug that might cause threads to hang. The Katana team is working hard on updating performance and security, however sometimes bugs are logged. Visit the Katana teams Github page to stay on top of issues that could affect your implementation.

4. Configure the Startup.cs class to support mixed mode authentication

To configure the OpenID Connect and Membership provider, replace the code in the startup class for OWIN middleware with the following example. The SecurityTokenValidated-event is used to synchronize the user and group membership to Episerver. You can also use this event for custom logic (for example, to add custom data to the user profile).

using EPiServer.Cms.UI.AspNetIdentity;
using EPiServer.Core;
using EPiServer.Reference.Commerce.Shared.Identity;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using Mediachase.Data.Provider;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Helpers;

public class Startup
  {
    private readonly IConnectionStringHandler _connectionStringHandler;

    public Startup() : this(ServiceLocator.Current.GetInstance<IConnectionStringHandler>())
      {
        // Parameterless constructor required by OWIN.
      }

    public Startup(IConnectionStringHandler connectionStringHandler)
      {
        _connectionStringHandler = connectionStringHandler;
      }

    public void Configuration(IAppBuilder app)
      {
        app.AddCmsAspNetIdentity<SiteUser>(new ApplicationOptions
          {
            ConnectionStringName = _connectionStringHandler.Commerce.Name
          });
            
        // Enables the application to temporarily store user information when they are
        // verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

        // Enables the application to remember the second login verification factor such as phone or email.
        // Once you check this option, your second step of verification during the login process
        // will be remembered on the device where you logged in from.
        // This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
        app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ExternalCookie);
        app.UseCookieAuthentication(new CookieAuthenticationOptions
          {
            AuthenticationMode = AuthenticationMode.Active,
            AuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
            CookieName = ".AspNet." + DefaultAuthenticationTypes.ExternalCookie,
            ExpireTimeSpan = TimeSpan.FromMinutes(5)
          });

        // <add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
        string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];

        // <add key="ida:ClientId" value="Client ID from Azure AD application" />
        string clientId = ConfigurationManager.AppSettings["ida:ClientId"];

        // <add key="ida:Tenant" value="Azure Active Directory, Directory Eg. Contoso.onmicrosoft.com/" />
        string tenant = ConfigurationManager.AppSettings["ida:Tenant"];

        //<add key="ida:PostLogoutRedirectUri" value="https://the logout post uri/" />
        string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];

        string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

        const string LogoutPath = "/Login/SignOut";

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
          {
            ClientId = clientId,
            Authority = authority,
            RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"],
            PostLogoutRedirectUri = postLogoutRedirectUri,
            Scope = OpenIdConnectScopes.OpenIdProfile,
            ResponseType = OpenIdConnectResponseTypes.IdToken,
            TokenValidationParameters = new TokenValidationParameters
              {
                ValidateIssuer = false,
                RoleClaimType = ClaimTypes.Role
              },
            Notifications = new OpenIdConnectAuthenticationNotifications
              {
                AuthenticationFailed = context =>
                  {
                    context.HandleResponse();
                    context.Response.Write(context.Exception.Message);
                    return Task.FromResult(0);
                  },
                RedirectToIdentityProvider = context =>
                  {
                    // Here you can change the return uri based on multisite
                    HandleMultiSiteReturnUrl(context);

                    // To avoid a redirect loop to the federation server send 403 
                    // when user is authenticated but does not have access
                    if (context.OwinContext.Response.StatusCode == 401 &&
                            context.OwinContext.Authentication.User.Identity.IsAuthenticated)
                      {
                        context.OwinContext.Response.StatusCode = 403;
                        context.HandleResponse();
                      }
                    return Task.FromResult(0);
                  },
                SecurityTokenValidated = context =>
                  {
                    var redirectUri = new Uri(context.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
                    if (redirectUri.IsAbsoluteUri)
                      {
                        context.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
                      }
                    //Sync user and the roles to EPiServer in the background
                    ServiceLocator.Current.GetInstance<ISynchronizingUserService>().
                    SynchronizeAsync(context.AuthenticationTicket.Identity);
                    return Task.FromResult(0);
                  }
              }
          });

        app.UseCookieAuthentication(new CookieAuthenticationOptions
          {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Login"),
            Provider = new CookieAuthenticationProvider
              {
                // Enables the application to validate the security stamp when the user logs in.
                // This is a security feature which is used when you change a password
                // or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager<SiteUser>, SiteUser>(
                  validateInterval: TimeSpan.FromMinutes(30),
                  regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user)),
                OnApplyRedirect = (context => context.Response.Redirect(context.RedirectUri)),
                OnResponseSignOut = (context => context.Response.Redirect(UrlResolver.Current.GetUrl(ContentReference.StartPage)))
              }
          });

        app.UseStageMarker(PipelineStage.Authenticate);
        app.Map(LogoutPath, map =>
          {
            map.Run(ctx =>
              {
                if (OpenIdConnectUser(ctx.Authentication.User))
                  {
                    ctx.Authentication.SignOut();
                  }
                else
                  {
                    ctx.Get<ApplicationSignInManager<SiteUser>>().SignOut();
                  }
                return Task.FromResult(0);
              });
        });

        // If the application throws an antiforgerytoken exception like
        // “AntiForgeryToken: A Claim of Type NameIdentifier or 
        // IdentityProvider Was Not Present on Provided ClaimsIdentity”, 
        // set AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier.
        AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
      }

    private bool OpenIdConnectUser(ClaimsPrincipal user)
      {
        const string openIdConnectProviderClaimType = "http://schemas.microsoft.com/identity/claims/identityprovider";
        const string aspNetIdentityProviderClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
        if (user.Claims.Any(c => c.Type == aspNetIdentityProviderClaimType))
          {
            return false;
          }
        if (user.Claims.Any(c => c.Type == openIdConnectProviderClaimType))
          {
            return true;
          }
        return false;
      }

    private void HandleMultiSiteReturnUrl(
      RedirectToIdentityProviderNotification<OpenIdConnectMessage,
      OpenIdConnectAuthenticationOptions> context)
      {
        // here you change the context.ProtocolMessage.RedirectUri to corresponding siteurl
        // this is a sample of how to change redirecturi in the multi-tenant environment
        if (context.ProtocolMessage.RedirectUri == null)
          {
            var currentUrl = EPiServer.Web.SiteDefinition.Current.SiteUrl;
            context.ProtocolMessage.RedirectUri = new UriBuilder(
              currentUrl.Scheme,
              currentUrl.Host,
              currentUrl.Port,
              HttpContext.Current.Request.Url.AbsolutePath).ToString();
          }
      }
  }

Configuring OpenID Connect in Commerce Manager

Note: OpenID Connect with Azure Active Directory delegates authentication of users with the right roles defined in the Azure AD Application Manifest. As Commerce Manager is business-critical software, we recommend not using it in mixed-mode authentication.

1. Configure the web.config file and install the same packages as above 

See 2. Configure Episerver to support claims and 3. Install NuGet packages.

2. Configure the Commerce Manager Startup.cs class to use OpenID Connect

To configure OpenID Connect, replace the code in the startup class for OWIN middleware with the following example. 

using EPiServer.Security;
using EPiServer.ServiceLocation;
using Microsoft.AspNet.Identity;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;

public class Startup
  {
    public Startup()
      {
        // Parameterless constructor required by OWIN.
      }
   
    public void Configuration(IAppBuilder app)
      {
        app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ExternalCookie);

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider.
        // Configure the sign in cookie.
        app.UseCookieAuthentication(new CookieAuthenticationOptions
          {
            AuthenticationMode = AuthenticationMode.Active,
            AuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
            CookieName = ".AspNet." + DefaultAuthenticationTypes.ExternalCookie,
            ExpireTimeSpan = TimeSpan.FromMinutes(5)
          });

        // <add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
        string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];

        // <add key="ida:ClientId" value="Client ID from Azure AD application" />
        string clientId = ConfigurationManager.AppSettings["ida:ClientId"];

        // <add key="ida:Tenant" value="Azure Active Directory, Directory Eg. Contoso.onmicrosoft.com/" />
        string tenant = ConfigurationManager.AppSettings["ida:Tenant"];

        //<add key="ida:PostLogoutRedirectUri" value="https://the logout post uri/" />
        string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];

        string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

        const string LogoutPath = "/Apps/Shell/Pages/Logout.aspx";

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
          {
            ClientId = clientId,
            Authority = authority,
            RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"],
            PostLogoutRedirectUri = postLogoutRedirectUri,
            Scope = OpenIdConnectScopes.OpenIdProfile,
            ResponseType = OpenIdConnectResponseTypes.IdToken,
            TokenValidationParameters = new TokenValidationParameters
              {
                ValidateIssuer = false,
                RoleClaimType = ClaimTypes.Role
              },
            Notifications = new OpenIdConnectAuthenticationNotifications
              {
                AuthenticationFailed = context =>
                  {
                    context.HandleResponse();
                    context.Response.Write(context.Exception.Message);
                    return Task.FromResult(0);
                  },
                RedirectToIdentityProvider = context =>
                  {
                    // Here you can change the return uri based on multisite
                    HandleMultiSiteReturnUrl(context);

                    // To avoid a redirect loop to the federation server send 403 
                    // when user is authenticated but does not have access
                    if (context.OwinContext.Response.StatusCode == 401 &&
                      context.OwinContext.Authentication.User.Identity.IsAuthenticated)
                      {
                        context.OwinContext.Response.StatusCode = 403;
                        context.HandleResponse();
                      }
                    return Task.FromResult(0);
                  },
                SecurityTokenValidated = (ctx) =>
                  {
                    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);
                  }
              }
          });
        app.UseStageMarker(PipelineStage.Authenticate);
        app.Map(LogoutPath, map =>
          {
            map.Run(ctx =>
              {
                ctx.Authentication.SignOut();
                return Task.FromResult(0);
              });
          });
      }

    private void HandleMultiSiteReturnUrl(
      RedirectToIdentityProviderNotification<OpenIdConnectMessage,
      OpenIdConnectAuthenticationOptions> context)
      {
        // here you change the context.ProtocolMessage.RedirectUri to corresponding siteurl
        // this is a sample of how to change redirecturi in the multi-tenant environment
        if (context.ProtocolMessage.RedirectUri == null)
          {
            var currentUrl = EPiServer.Web.SiteDefinition.Current.SiteUrl;
            context.ProtocolMessage.RedirectUri = new UriBuilder(
              currentUrl.Scheme,
              currentUrl.Host,
              currentUrl.Port,
              HttpContext.Current.Request.Url.AbsolutePath).ToString();
          }
      }
  }

Adding application roles in Azure Active Directory

By default, you need to declare application roles, such as WebEditors and WebAdmins, in the active directory application. Either the application owner (developer of the app) or the global administrator of the developer’s directory can declare roles for an application.

  1. In the Azure portal, choose your Azure AD tenant by selecting it from the top right corner of the page (Click your name. In the dropdown, you will see Directory. Select the Azure AD directory where you created the AD application).
  2. Select Azure Active Directory extension from the left navigation panel and click App Registrations.
  3. Click to open the application for which you want to declare application roles.
  4. From the application page, click Manifest to open the inline manifest editor. From here, you can choose to download the manifest or upload a modified copy if you would rather use another editor.
  5. Locate the appRoles setting and insert the appRole definitions in the array.  
    Below is an example of approles that declares WebAdmins and WebEditors. Modify it according to your application roles.

Note: You need to generate a new Guid for each role declaration.

 "appRoles": [
    {
      "allowedMemberTypes": [
        "User"
      ],
      "description": "Editor can edit the site.",
      "displayName": "WebEditors",
      "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
      "isEnabled": true,
      "value": "WebEditors"
    },
    {
      "allowedMemberTypes": [
        "User"
      ],
      "description": "Admins can manage roles and perform all task actions.",
      "displayName": "WebAdmins",
      "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX",
      "isEnabled": true,
      "value": "WebAdmins"
    },
     {
      "allowedMemberTypes": [
        "User"
      ],
      "description": "Admin the site.",
      "displayName": "Administrators",
      "id": "XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX",
      "isEnabled": true,
      "value": "Administrators"
    }
  ],
  1. Upload the edited manifest using the inline manifest editor or save your changes if you used the inline manifest editor. For more information, see Understanding the Azure Active Directory application manifest.

Assigning users and groups to roles in the AD application

In your AD application, in order to assign users and groups to specific roles, you need to:

  1. Navigate to the registered Azure Active Directory application which you have configured to use with OpenId Connect.
  2. On the AD application main page, click Managed application in local directory
  3. In the left pane, click User and Groups.
  4. Click Add User.
  5. Select or invite a user from Users.
  6. From Select Role, select a role to assign.
  7. Click Assign.

Note: If a user is assigned multiple roles, repeat this process for each role.

Known issue

  • If the application throws an antiforgerytoken exception like AntiForgeryToken: A Claim of Type NameIdentifier or IdentityProvider Was Not Present on Provided ClaimsIdentity, set AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier.

Last updated: Nov 22, 2017