How to integrate Kentico 13 Xperience Core MVC with Single Sign-On (SSO) using Azure AD B2C?

Introduction

Integrate Azure AD B2C SSO with Kentico Xperience Core MVC for seamless user experiences and enhanced security. This comprehensive guide provides step-by-step instructions, best practices, and considerations. Learn to streamline authentication processes and achieve centralized control. Empower your applications with SSO using Azure AD B2C!


 

Prerequisites:

  • Azure Subscription
  • Azure Active Directory
  • Azure B2C
  • nuGet Package
  • Microsoft.Identity.Web(2.5.0)
  • Microsoft.Identity.Web.UI(2.5.0)

 
To establish a connection between Azure AD B2C and your Kentico 13 Xperience Core project, follow these steps:
 
Step 1: Install the necessary NuGet packages in your project:
 
Microsoft.Identity.Web (version 2.5.0)
Microsoft.Identity.Web.UI (version 2.5.0)
 
Step 2: Add the following code snippet to your Startup.cs file:
 
// Code snippet for establishing connection to Azure AD B2C
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();
 
By implementing these steps, you can successfully establish the connection between Azure AD B2C and your Kentico 13 Xperience Core project.
 

private void ConfigureApplicationIdentity(IServiceCollection services)
{
            services.AddScoped<IPasswordHasher<ApplicationUser>, Kentico.Membership.PasswordHasher<ApplicationUser>>();
            services.AddScoped<IMessageService, MessageService>();
            services.AddSingleton<ExternalRoles>();
            services.AddApplicationIdentity<ApplicationUser, ApplicationRole>(options =>
            {
                // A place to configure Identity options, such as password strength requirements
                options.SignIn.RequireConfirmedEmail = true;
            })
                    // Adds default token provider used to generate tokens for
                    // email confirmations, password resets, etc.
                    .AddApplicationDefaultTokenProviders()
                    // Adds an implementation of the UserStore for working
                    // with Xperience user objects
                    .AddUserStore<ApplicationUserStore<ApplicationUser>>()
                    // Adds an implementation of the RoleStore used for
                    // working with Xperience roles
                    .AddRoleStore<ApplicationRoleStore<ApplicationRole>>()
                    // Adds an implementation of the UserManager for Xperience membership
                    .AddUserManager<ApplicationUserManager<ApplicationUser>>()
                    // Adds the default implementation of the SignInManger
                    .AddSignInManager<SignInManager<ApplicationUser>>();
 
            //DocSection:ExternalAuth
            services.AddAuthentication()
               .AddOpenIdConnect(options =>
               {
                   options.ClientId = SettingsKeyInfoProvider.GetValue("ClientId", SiteContext.CurrentSiteName);
                   options.ClientSecret = SettingsKeyInfoProvider.GetValue("ClientSecret", SiteContext.CurrentSiteName);
                   options.CallbackPath = SettingsKeyInfoProvider.GetValue("CallbackPath", SiteContext.CurrentSiteName);
                   options.MetadataAddress = SettingsKeyInfoProvider.GetValue("B2CMetadataAddress", SiteContext.CurrentSiteName);
                   options.SignedOutCallbackPath = SettingsKeyInfoProvider.GetValue("SignedOutCallbackPath", SiteContext.CurrentSiteName);
               });
            //EndDocSection:ExternalAuth
 
            services.AddAuthorization();
            services.AddScoped<ApplicationRoleStore<ApplicationRole>>();
            services.ConfigureApplicationCookie(c =>
            {
                c.LoginPath = new PathString("/");
                c.ExpireTimeSpan = TimeSpan.FromDays(14);
                c.SlidingExpiration = true;
                c.Cookie.Name = AUTHENTICATION_COOKIE_NAME;
            });
 
            // Registers the authentication cookie with the 'Essential' cookie level
            // Ensures that the cookie is preserved when changing a visitor's allowed cookie level below 'Visitor'
            CookieHelper.RegisterCookie(AUTHENTICATION_COOKIE_NAME, CookieLevel.Essential);
}


Note: Create above setting keys on CMS
 
Step 3: Create the following controller to handle the External Authentication


public class ExternalAuthenticationController : Microsoft.AspNetCore.Mvc.Controller
    {
        //DocSection:DependencyInjection
        private readonly SignInManager<ApplicationUser> signInManager;
        private readonly ApplicationUserManager<ApplicationUser> userManager;
        private readonly ApplicationRoleStore<ApplicationRole> roleStore;
        private readonly IEventLogService eventLogService;
        private readonly ISiteService siteService;
 
        /// <summary>
        /// ExternalAuthenticationController Constructor
        /// </summary>
        /// <param name="signInManager"></param>
        /// <param name="userManager"></param>
        /// <param name="roleStore"></param>
        /// <param name="eventLogService"></param>
        /// <param name="siteService"></param>
        public ExternalAuthenticationController(SignInManager<ApplicationUser> signInManager,
                                                ApplicationUserManager<ApplicationUser> userManager,
                                                ApplicationRoleStore<ApplicationRole> roleStore,
                                                IEventLogService eventLogService,
                                                ISiteService siteService)
        {
            this.signInManager = signInManager;
            this.userManager = userManager;
            this.roleStore = roleStore;
            this.eventLogService = eventLogService;
            this.siteService = siteService;
        }
        //EndDocSection:DependencyInjection
 
 
        //DocSection:ExternalAuth
        /// <summary>
        /// Redirects authentication requests to an external service.
        /// Posted parameters include the name of the requested authentication middleware instance and a return URL.
        /// </summary>
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public IActionResult RequestExternalSignIn(string provider, string returnUrl)
        {
            // Sets a return URL targeting an action that handles the response
            string redirectUrl = Url.Action(nameof(ExternalSignInCallback), new { ReturnUrl = returnUrl });
 
            // Configures the redirect URL and user identifier for the specified external authentication provider
            AuthenticationProperties authenticationProperties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
 
            // Challenges the specified authentiction provider, redirects to the specified 'redirectURL' when done
            return Challenge(authenticationProperties, provider);
        }
 
 
        /// <summary>
        /// Handles responses from external authentication services.
        /// </summary>
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> ExternalSignInCallback(string returnUrl, string remoteError = null)
        {
            // If an error occurred on the side of the external provider, displays a view with the forwarded error message
            if (remoteError != null)
            {
                return RedirectToAction(nameof(ExternalAuthenticationFailure));
            }
 
            // Extracts login info out of the external identity provided by the service
            ExternalLoginInfo loginInfo = await signInManager.GetExternalLoginInfoAsync();
 
            // If the external authentication fails, displays a view with appropriate information
            if (loginInfo == null)
            {
                return RedirectToAction(nameof(ExternalAuthenticationFailure));
            }
 
            // Attempts to sign in the user using the external login info
            SignInResult result = await signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, isPersistent: false);
 
            // Success occurs if the user already exists in the connected database and has signed in using the given external service
            if (result.Succeeded)
            {
                eventLogService.LogInformation("External authentication", "EXTERNALAUTH", $"User logged in with {loginInfo.LoginProvider} provider.");
                return RedirectToLocal(returnUrl);
            }
 
            // The 'NotAllowed' status occurs if the user exists in the system, but is not enabled
            if (result.IsNotAllowed)
            {
                // Returns a view informing the user about the locked account
                return RedirectToAction(nameof(Lockout));
            }
            else
            {
                return View();
 
                // Optional extension:
                // Send the loginInfo data as a view model and create a form that allows adjustments of the user data.
                // Allows visitors to resolve errors such as conflicts with existing usernames in Xperience.
                // Then post the data to another action and attempt to create the user account again.
                // The action can access the original loginInfo using the AuthenticationManager.GetExternalLoginInfoAsync() method.
            }
        }
 
        /// <summary>
        /// Lockout
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Lockout()
        {
            return View();
        }
 
        /// <summary>
        /// ExternalAuthenticationFailure
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [AllowAnonymous]
        public IActionResult ExternalAuthenticationFailure()
        {
            return View();
        }
 
 
        /// <summary>
        /// Creates users in Xperience based on external login data.
        /// </summary>
        private async Task<IdentityResult> CreateExternalUser(ExternalLoginInfo loginInfo)
        {
 
           
            // Prepares a new user entity based on the external login data
            ApplicationUser user = new ApplicationUser
            {
                UserName = ValidationHelper.GetSafeUserName(loginInfo.Principal.FindFirstValue(ClaimTypes.Email) ??
                                                        loginInfo.Principal.FindFirstValue(ClaimTypes.Email),
                                                        siteService.CurrentSite.SiteName),
                Email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email),
                Enabled = true, // The user is enabled by default
                IsExternal = true // IsExternal must always be true for users created via external authentication
                // Set any other required user properties using the data available in loginInfo
            };
 
            // Attempts to create the user in the Xperience database
            IdentityResult result = await userManager.CreateAsync(user);
            if (result.Succeeded)
            {
                if (externalRoles.Length > 0) {
                    IdentityResult roleResult = await userManager.AddToRolesAsync(user, externalRoles);
                    if (roleResult.Succeeded)
                    {
                        eventLogService.LogEvent(EventTypeEnum.Information, "External Login", "UserRole Assign", "Role(s) Assigned to User");
                    }
                }
                    // If the user was created successfully, creates a mapping between the user and the given authentication provider
                    result = await userManager.AddLoginAsync(user, loginInfo);
            }
 
            return result;
        }
 
 
        /// <summary>
        /// Redirects to a specified return URL if it belongs to the MVC website or to the site's home page.
        /// </summary>
        private IActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
 
            return RedirectToAction(nameof(Index), "Home");
        }
        //EndDocSection:ExternalAuth
    }


Step 4: Create the following partial view for displaying Login/Username links on page – and call this on Master Layout


@using System.Security.Principal
@using CMS.DataEngine;
@using CMS.SiteProvider;
@using Microsoft.Identity.Web;
@using Microsoft.Extensions.Options
@using Kentico.Membership
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
 
 
@*DocSection:ExternalAuthView*@
 
 
@inject SignInManager<ApplicationUser> SignInManager
 
@{
    var currentPageUrl =  Context.Request.Path + Context.Request.QueryString;
    @* Gets a collection of the authentication services registered in your application's startup class *@
    var signInProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    if (signInProviders.Count == 0)
    {
    <div style="display:none">
                <p>
                    There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                    for details on setting up this ASP.NET application to support logging in via external services.
                </p>
    </div>
    }
    else
    {
    <li>
                @* Generates a form with buttons targeting the RequestSignIn action. Optionally pass a redirect URL as a parameter. *@
 
                @foreach (AuthenticationScheme provider in signInProviders)
            {
                    @if (User.Identity?.IsAuthenticated == false)
                {
                    var displayName = (provider.DisplayName == "OpenIdConnect") ? "SIGNIN" : provider.DisplayName;
                        <form asp-controller="ExternalAuthentication" asp-action="RequestExternalSignIn" asp-route-returnurl="@currentPageUrl" method="post" class="">
 
                            <button type="submit" class="nav-link text-dark" id="ssoLoginBtn" name="provider" style="color:white;display:none;" value="@provider.Name">
                                @displayName
                            </button>
                        </form>
                }
            }
    </li>
    @if (User.Identity?.IsAuthenticated == true)
        {
            <li >
                <a class="navbar-text text-dark">@User.Identity?.Name</a>
            </li>
        }
    }
}
@*EndDocSection:ExternalAuthView*@
 
To include the necessary functionality in your website, add the following code to your Layout.cshtml file: <partial name="_LoginPartial" />.
 
After completing the integration steps, proceed to build and run your website to observe the desired output.
 

We use cookies to provide the best possible browsing experience to you. By continuing to use our website, you agree to our Cookie Policy