External Identity Providers in ASP.NET Core

How External Identity Provider Works (Goolge)

If a user wants to use users Google account to sign in to our application, then the user click of the "Sign In with Google" button, the user provides his/her google login credentials to the google login page. Upon successful login, google will then send the user back to our application and a pre-configured callback function is executed. 

The code in this callback function checks the identity received from the external identity provider and sign-in that the user into our application. 



To use an external identity provider like Google, we have to first register our application with Google. Upon successful registration, we will be provided with Client ID & Client Secret which we need to use Google authentication.


Create google oauth credentials - Client Id and Client Secret


Navigate to the following website and login with your Google credentials

Step 1: Create a project if you do not have one already

Click on Select a project dropdownlist and then, click New Project link in the popup window that appears. 



Step 2 : Enable Google+ API



Click on the Library tab on the left and search for Googleplus API and enable it.

enable google plus api

Step 3 : Configure OAuth consent screen

Click on the OAuth consent screen tab on the left. If you do not see OAuth consent screen tab, click on Google APIs banner image on the top left hand corner.

configure consent screen oauth google

On the OAuth consent screen, the only required field is the Application name. This is the name that will be shown to end users asking for their consent. 

If this is not entirely clear at the moment, please do not worry. In our upcoming videos, when we actually integrate google authentication and see the consent screen in action it will be much clear at that point.

Step 4 : Create OAuth client credentials

Click on the Credentials tab on the left navigation menu.

create google oauth client

On the subsequent page, click Create credentials button. From the dropdownlist, select OAuth client ID.

google developer console create oauth client

On the next screen (i.e Create OAuth client ID)
  • Select Web application as the Application type
  • Provide a meaningful name for the OAuth client.
  • Authorized JavaScript origins - This is the URL of where our application is running. To get this URL, on your localhost, right click on the project name in Solution Explorer in Visual Studio and select Properties. On the Debug tab, you will find the App URL.
  • Authorized redirect URIs - This is the path in our application that users are redirected to after they are authenticated by Google. The default path in asp.net core is signin-google. So the complete redirect URI is Application Root URI/signin-google. If we do not like this default path signin-google we can change it. We will discuss how to do this in our next video, when we discuss integrating google authentication into our asp.net core application.
create google oauth client id

Enable Google Authentication in ASP.NET Core

Include the following configuration in ConfigureServices() method of the Startup class. We discussed registering our application with Google and obtaining Client Id and Secret in our previous video.

services.AddAuthentication().AddGoogle(options =>
    {
        options.ClientId = "XXXXX";
        options.ClientSecret = "YYYYY";
    });

The code required for Google authentication including this AddGoogle() method is present in Microsoft.AspNetCore.Authentication.Google nuget package. Since I am using ASP.NET Core 2.2, this package is automatically included in the project as part of the meta package. If you are using older versions of ASP.NET Core you have to manually install this nuget package.

LoginViewModel

The model for login view is LoginViewModel class. Include ReturnUrl and ExternalLogins properties.

public class LoginViewModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember me")]
    public bool RememberMe { get; set; }

    public string ReturnUrl { get; set; }

    // AuthenticationScheme is in Microsoft.AspNetCore.Authentication namespace
    public IList<AuthenticationScheme> ExternalLogins { get; set; }
}

ReturnUrl is the URL the user was trying to access before authentication. We preserve and pass it between requests using ReturnUrl property, so the user can be redirected to that URL upon successful authentication.

ExternalLogins property stores the list of external logins (like Facebook, Google etc) that are enabled in our application. You will better understand what this property does in just a bit, when we actually use it.

Login Action in AccountController
  • Populate ReturnUrl and ExternalLogins properties of LoginViewModel and then pass the instance to the view.
  • GetExternalAuthenticationSchemesAsync() method of SignInManager service, returns the list of all configured external identity providers like (Google, Facebook etc).
  • At the moment we only have one external identity provider configured and that is Google.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl)
{
    LoginViewModel model = new LoginViewModel
    {
        ReturnUrl = returnUrl,
        ExternalLogins = 
        (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList()
    };

    return View(model);
}

Login Page (Login.cshtml)

The following is the code specific to external login.

<form method="post" asp-action="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl">
    <div>
        @foreach (var provider in Model.ExternalLogins)
        {
            <button type="submit" class="btn btn-primary"
                    name="provider" value="@provider.Name"
                    title="Log in using your @provider.DisplayName account">
                @provider.DisplayName
            </button>
        }
    </div>
</form>
  • We are looping through each external login provider we have in Model.ExternalLogins
  • For each external login provider a submit button is dynamically generated
  • At the moment we only have one external identity provider configured and that is Google, so we get one Submit button.
  • This submit button is inside a form. The form method attribute value is post and asp-action attribute value is ExternalLogin
  • So when the submit button is clicked the form is posted to ExternalLogin action in AccountController
  • The login provider is Google, so in the foreach loop, provider.Name returns Google.
  • Since the button name is set to provider, asp.net core model binding maps the provider name which is Google to provider parameter on the ExternalLogin action.
Complete Code of Login View

@model LoginViewModel

@{
    ViewBag.Title = "User Login";
}

<div class="row">
    <div class="col-md-6">
        <h1>Local Account Login</h1>
        <hr />
        <form method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <div class="checkbox">
                    <label asp-for="RememberMe">
                        <input asp-for="RememberMe" />
                        @Html.DisplayNameFor(m => m.RememberMe)
                    </label>
                </div>
            </div>
            <button type="submit" class="btn btn-primary">Login</button>
        </form>
    </div>
    <div class="col-md-6">
        <h1>External Login</h1>
        <hr />
        @{
            if (Model.ExternalLogins.Count == 0)
            {
                <div>No external logins configured</div>
            }
            else
            {
                <form method="post" asp-action="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl">
                    <div>
                        @foreach (var provider in Model.ExternalLogins)
                        {
                            <button type="submit" class="btn btn-primary"
                                    name="provider" value="@provider.Name"
                                    title="Log in using your @provider.DisplayName account">
                                @provider.DisplayName
                            </button>
                        }
                    </div>
                </form>
            }
        }
    </div>
</div>

ExternalLogin action in AccountController

[AllowAnonymous]
[HttpPost]
public IActionResult ExternalLogin(string provider, string returnUrl)
{
    var redirectUrl = Url.Action("ExternalLoginCallback""Account",
                        new { ReturnUrl = returnUrl });
    var properties = signInManager
        .ConfigureExternalAuthenticationProperties(provider, redirectUrl);
    return new ChallengeResult(provider, properties);
}

ExternalLoginCallback action in asp.net core


Upon successful authentication, Google redirects the user back to our application and the following ExternalLoginCallback action is executed.

[AllowAnonymous]
public async Task<IActionResult>
            ExternalLoginCallback(string returnUrl = nullstring remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    LoginViewModel loginViewModel = new LoginViewModel
    {
        ReturnUrl = returnUrl,
        ExternalLogins =
                (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList()
    };

    if (remoteError != null)
    {
        ModelState
            .AddModelError(string.Empty, $"Error from external provider: {remoteError}");

        return View("Login", loginViewModel);
    }

    // Get the login information about the user from the external login provider
    var info = await signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ModelState
            .AddModelError(string.Empty, "Error loading external login information.");

        return View("Login", loginViewModel);
    }

    // If the user already has a login (i.e if there is a record in AspNetUserLogins
    // table) then sign-in the user with this external login provider
    var signInResult = await signInManager.ExternalLoginSignInAsync(info.LoginProvider,
        info.ProviderKey, isPersistent: false, bypassTwoFactor: true);

    if (signInResult.Succeeded)
    {
        return LocalRedirect(returnUrl);
    }
    // If there is no record in AspNetUserLogins table, the user may not have
    // a local account
    else
    {
        // Get the email claim value
        var email = info.Principal.FindFirstValue(ClaimTypes.Email);

        if (email != null)
        {
            // Create a new user without password if we do not have a user already
            var user = await userManager.FindByEmailAsync(email);

            if (user == null)
            {
                user = new ApplicationUser
                {
                    UserName = info.Principal.FindFirstValue(ClaimTypes.Email),
                    Email = info.Principal.FindFirstValue(ClaimTypes.Email)
                };

                await userManager.CreateAsync(user);
            }

            // Add a login (i.e insert a row for the user in AspNetUserLogins table)
            await userManager.AddLoginAsync(user, info);
            await signInManager.SignInAsync(user, isPersistent: false);

            return LocalRedirect(returnUrl);
        }

        // If we cannot find the user email we cannot continue
        ViewBag.ErrorTitle = $"Email claim not received from: {info.LoginProvider}";
        ViewBag.ErrorMessage = "Please contact support on Pragim@PragimTech.com";

        return View("Error");
    }
}

Why email confirmation is important

Letting users login and use your application, without a confirmed email is a security risk. Email confirmation is important both for your security and the security of the application.


Comments