Adding external authentication using ASP.NET Core Identity

External authentication services (identity providers) allow website visitors to sign in with existing user accounts from other sites or services. When developing ASP.NET Core sites, you can set up external authentication using the Kentico.Membership API (provided as part of the Xperience.AspNetCore.WebApp integration package), in combination with ASP.NET Core Identity.

When a user successfully authenticates through an external service, you can use the provided API to create a corresponding user in the shared Xperience database.

Xperience supports all third-party authentication providers officially supported by Microsoft

On this page

Registering external authentication services

This scenario assumes you already have the Xperience ASP.NET Core Identity implementation integrated into your project. Use the following steps to install and register external authentication services into your project:

  1. Open your project in Visual Studio.
  2. Install NuGet packages with the authentication middleware components of the services you wish to use. This page covers integration with Microsoft, Facebook, Google, and Twitter:
    • Microsoft.AspNetCore.Authentication.MicrosoftAccount
    • Microsoft.AspNetCore.Authentication.Facebook
    • Microsoft.AspNetCore.Authentication.Google
    • Microsoft.AspNetCore.Authentication.Twitter
  3. Modify the startup file of your project (Startup.cs by default).

  4. Register any required authentication services using the IServiceCollection.AddAuthentication AuthenticationBuilder within the Configure method:

                services.AddAuthentication()
                    .AddFacebook(facebookOptions =>
                    {
                        facebookOptions.AppId = "placeholder";
                        facebookOptions.AppSecret = "placeholder";
                    })
                    .AddMicrosoftAccount(microsoftAccountOptions =>
                    {
                        microsoftAccountOptions.ClientSecret = "placeholder";
                        microsoftAccountOptions.ClientId = "placeholder";
                    })
                    .AddGoogle(googleOptions =>
                    {
                        googleOptions.ClientId = "placeholder";
                        googleOptions.ClientSecret = "placeholder";
                    })
                    .AddTwitter(twitterOptions =>
                    {
                        twitterOptions.ConsumerKey = "placeholder";
                        twitterOptions.ConsumerSecret = "placeholder";
                        twitterOptions.RetrieveUserDetails = true;
                    });
    

The Xperience Identity implementation and the specified external authentication services are now registered for your application. Continue by setting up the components required for using external authentication on your website (controllers, views, etc.).

Setting up external authentication

After you register the required authentication services, you need to implement actions that redirect visitors to the external authentication service, process the response, and handle the corresponding user accounts in Xperience.

The following is a basic example of external authentication handling (this example uses the MVC approach):

  1. Create a new controller class in your project or edit an existing one.
  2. Provide instance of the following classes using dependency injection:

    using System;
    using System.Threading.Tasks;
    using System.Security.Claims;
     
    using CMS.Helpers;
    using CMS.Core;
    using CMS.Base;
     
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authorization;
    using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
     
    using Kentico.Membership;
    
            private readonly SignInManager<ApplicationUser> signInManager;
            private readonly ApplicationUserManager<ApplicationUser> userManager;
            private readonly IEventLogService eventLogService;
            private readonly ISiteService siteService;
     
            public ExternalAuthenticationController(SignInManager<ApplicationUser> signInManager,
                                                    ApplicationUserManager<ApplicationUser> userManager,
                                                    IEventLogService eventLogService,
                                                    ISiteService siteService)
            {
                this.signInManager = signInManager;
                this.userManager = userManager;
                this.eventLogService = eventLogService;
                this.siteService = siteService;
            }
    

  3. Add a POST action that challenges external authentication providers. Use a parameter to specify the name of the requested authentication middleware instance.
  4. Add an action that handles the responses from external services.
    • Call the SignInManager.GetExternalLoginInfoAsync method to get login information from the external identity provided by the service.
    • Use the SignInManager.ExternalLoginSignInAsync method to sign in to Xperience based on external data.
  5. Add logic that creates new users in the Xperience database if the first sign-in using the external data fails:
    1. Prepare a new Kentico.Membership.ApplicationUser object based on the external login data. You must enable the user object's IsExternal property.
    2. Call the ApplicationUserManager.CreateAsync method to create the user in the Xperience database.
    3. Call the ApplicationUserManager.AddLoginAsync method to create a mapping between the new user and the given authentication provider.

    External authentication example
            /// <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
                {
                    // Attempts to create a new user in Xperience if the authentication failed
                    IdentityResult userCreation = await CreateExternalUser(loginInfo);
     
                    // Attempts to sign in again with the new user created based on the external authentication data
                    result = await signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, isPersistent: false);
     
                    // Verifies that the user was created successfully and was able to sign in
                    if (userCreation.Succeeded && result == SignInResult.Success)
                    {
                        // Redirects to the original return URL
                        return RedirectToLocal(returnUrl);
                    }
     
                    // If the user creation was not successful, displays corresponding error messages
                    foreach (IdentityError error in userCreation.Errors)
                    {
                        ModelState.AddModelError(String.Empty, error.Description);
                    }
     
                    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.
                }           
            }
     
     
            [HttpGet]
            [AllowAnonymous]
            public IActionResult Lockout()
            {
                return View();
            }
     
     
            [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.Name) ?? 
                                                                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 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");
            }
    

  6. Design the user interface required for external authentication on your website

    • Add buttons onto your website's sign-in page that target the RequestSignIn action. Supply parameters that specify the provider type for each authentication service and optionally a return URL.

      Example of external authentication buttons (Razor view)
              @using Microsoft.AspNetCore.Authentication
              @using Microsoft.AspNetCore.Identity
              @using Kentico.Membership
       
              @inject SignInManager<ApplicationUser> SignInManager
       
              @{
                  @* 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>
                          <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
                  {
                      @* Generates a form with buttons targeting the RequestSignIn action. Optionally pass a redirect URL as a parameter. *@
                      <form asp-controller="ExternalAuthentication" asp-action="RequestExternalSignIn" asp-route-returnurl="http://localhost/LearningKitCore/Account/SignIn" method="post" class="form-horizontal">
                          <div>
                              <p>
                                  @foreach (AuthenticationScheme provider in signInProviders)
                                  {
                                      <button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
                                  }
                              </p>
                          </div>
                      </form>
                  }
              }
      

    • Create a view for displaying information in situations where the external authentication fails (ExternalAuthenticationFailure in the example).

    • Create a view for displaying information in situations where the external authentication succeeds, but the matching Xperience user account is disabled (Lockout in the example).
    • Create a view for the ExternalSignInCallback action.

      In the most basic version of the ExternalSignInCallback view, you only need to display error messages in situations where the external authentication succeeds, but Xperience is unable to create a matching user account.

      You can implement an extended version with an editing form that allows visitors to adjust and resubmit the data of the new user account (using a strongly typed view that receives the external login data from the action).

Visitors can now sign in on the website through the external authentication services that you registered in the Startup class. Upon successful authentication, the system creates a corresponding user account in the shared Xperience database, along with a mapping between the user and the given authentication provider.


Was this page helpful?