8/21/2022 Admin

Customizing The Blazor Login Process With External Authentication Providers


You can easily implement external authentication providers to allow the users of your Blazor application to log in with Facebook, Google, Microsoft, Twitter, and many others. However, you have to write code to associate an external login to an existing login. You can implement several methods such as sending a code in an email.

The method, covered here, will ask the user to enter the password of the existing account to make the association.

image

After clicking the Log In link to access the login screen, the user is presented with the option to log in using one or more external authentication providers.

Clicking the button for the external authentication provider, will direct the user to the authentication provider, to be authenticated.

image

After authenticating with the external authentication provider, the user is returned to the application.

If the user already has an account in the application they will be logged in. If the user does not have an account in the application, they will be presented with the Register page.

The email of the external account is displayed (they will not be allowed to change it) and they will be presented with the Register button that will create the account.

image

If the email address is already being used by an existing account, the user is informed of this, and they are prompted to enter the password for the existing account to associate this external login with that account.

image

If they do not enter the correct password, they are prompted again.

image

After entering the correct password, they are logged in, and the external login is associated with the existing account.

The user can now log in directly using a username and password, or log into the same account using the external authentication provider.

image

Note: When an account is created only using an external authentication provider, it will not have a password in the database. If a user attempts to associate an external provider to that account, (for example to associate a Facebook login to an account that was created using a Google login), they will see an error message.

image

To resolve this, the user simply needs to log in, using the working external authentication provider, and click on their username.

image

On the manage account settings page, they can select the Password menu option, to be presented with the form that will allow them to set a password for the account.

Creating The Application

image

We start with the application from the tutorial: Creating A Step-By-Step End-To-End Database Server-Side Blazor Application (Note: you can download the completed application from the Downloads page on this site).

Note: For added security, in the Program.cs file, it is suggested you set: options.SignIn.RequireConfirmedAccount = true and enable email confirmation (see: https://docs.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm?view=aspnetcore-6.0&tabs=visual-studio#register-confirm-email-and-reset-password).

image

We can run the application and click the Register link.

image

We can create an account.

image

We will then be logged in.

image

If we look in the database, we see that an account has been created with a password.

Set Up An External Authentication Provider

image

We will now set up an external authentication provider by following the directions here: Google external login setup in ASP.NET Core.

image

This requires us to install the Microsoft.AspNetCore.Authentication.Google NuGet package.

image

It also requires us to add code like this to the appsettings.json file:

								
									"Authentication": {
								
									"Google": {
								
									"ClientId": "{{ Google Client Id }}",
								
									"ClientSecret": "{{ Google Client Secret }}"
								
									}
								
									},

image

Finally, it requires us to add the following code to the Program.cs file:

								
builder.Services.AddAuthentication().AddGoogle(googleOptions =>
								
{
								
									googleOptions.ClientId =
								
									builder.Configuration.GetValue<string>("Authentication:Google:ClientId");
								
								
									googleOptions.ClientSecret =
								
									builder.Configuration.GetValue<string>("Authentication:Google:ClientSecret");
								
});

image

Now when we run the application and click the Log in link we have the option to log in using an external authentication provider.

image

After logging into the external authentication provider, we are redirected back to the application and presented with the page to create an account.

image

If we use an email address that is already used by another account, we are unable to link this external login to the existing account.

Customize The Process

image

The first step to customize the process is to right-click on the project node and select Add then New Scaffolded Item…

Note: If you don’t see this option, Install the Microsoft.VisualStudio.Web.CodeGeneration.Design NuGet package.

image

Select Identity then Add.

image

Wait for the initializations…

image

Select Account\ExternalLogin, the Data context class, and press the Add button.

image

The ExternalLogin pages will be created.

Currently they will provide the exact same functionality as before, however, now we can customize them.

Replace all the code with the following code:

ExternalLogin.cshtml

								
@page
								
@model ExternalLoginModel
								
@{
								
									ViewData["Title"] = "Register";
								
}
								

								
									<
									h1
									>@ViewData["Title"]</
									h1
									>
								
									<
									h2
									id="external-login-title"
									>Associate your @Model.ProviderDisplayName account.</
									h2
									>
								
									<
									hr
									/>
								

								
@if (Convert.ToString(ViewData["AssociateExistingAccount"]) == "false")
								
{
								
									<
									p
									id="external-login-description"
									class="text-info"
									>
								
									You've successfully authenticated with
									<
									strong
									>@Model.ProviderDisplayName</
									strong
									>.
								
									Click the Register button to finish logging in.
								
									</
									p
									>
								

								
									<
									div
									class="row"
									>
								
									<
									div
									class="col-md-4"
									>
								
									<
									form
									asp-page-handler="Confirmation"
									asp-route-returnUrl="@Model.ReturnUrl"
								
									method="post"
									>
								
									<
									div
									asp-validation-summary="ModelOnly"
									class="text-danger"
									>
									</
									div
									>
								
									<
									div
									class="form-floating"
									>
								
									<
									input
									asp-for="Input.Email"
									class="form-control"
									readonly="true"
								
									autocomplete="email"
									/>
								
									<
									label
									asp-for="Input.Email"
									class="form-label"
									>
									</
									label
									>
								
									<
									span
									asp-validation-for="Input.Email"
									class="text-danger"
									>
									</
									span
									>
								
									</
									div
									>
								
									<
									button
									type="submit"
									class="w-100 btn btn-lg btn-primary"
									>Register</
									button
									>
								
									</
									form
									>
								
									</
									div
									>
								
									</
									div
									>
								
}
								

								
@section Scripts {
								
									<
									partial
									name="_ValidationScriptsPartial"
									/>
								
}

ExternalLogin.cshtml.cs

								
									// Licensed to the .NET Foundation under one or more agreements.
								
									// The .NET Foundation licenses this file to you under the MIT license.
								
#nullable disable
								

								
									using
									System;
								
									using
									System.ComponentModel.DataAnnotations;
								
									using
									System.Security.Claims;
								
									using
									System.Text;
								
									using
									System.Text.Encodings.Web;
								
									using
									System.Threading;
								
									using
									System.Threading.Tasks;
								
									using
									Microsoft.AspNetCore.Authorization;
								
									using
									Microsoft.Extensions.Options;
								
									using
									Microsoft.AspNetCore.Identity;
								
									using
									Microsoft.AspNetCore.Identity.UI.Services;
								
									using
									Microsoft.AspNetCore.Mvc;
								
									using
									Microsoft.AspNetCore.Mvc.RazorPages;
								
									using
									Microsoft.AspNetCore.WebUtilities;
								
									using
									Microsoft.Extensions.Logging;
								

								
									namespace
									EndToEnd.Areas.Identity.Pages.Account
								
{
								
									[AllowAnonymous]
								
									public
									class
									ExternalLoginModel : PageModel
								
									{
								
									private
									readonly
									SignInManager<IdentityUser> _signInManager;
								
									private
									readonly
									UserManager<IdentityUser> _userManager;
								
									private
									readonly
									IUserStore<IdentityUser> _userStore;
								
									private
									readonly
									IUserEmailStore<IdentityUser> _emailStore;
								
									private
									readonly
									IEmailSender _emailSender;
								
									private
									readonly
									ILogger<ExternalLoginModel> _logger;
								

								
									public
									ExternalLoginModel(
								
									SignInManager<IdentityUser> signInManager,
								
									UserManager<IdentityUser> userManager,
								
									IUserStore<IdentityUser> userStore,
								
									ILogger<ExternalLoginModel> logger,
								
									IEmailSender emailSender)
								
									{
								
									_signInManager = signInManager;
								
									_userManager = userManager;
								
									_userStore = userStore;
								
									_emailStore = GetEmailStore();
								
									_logger = logger;
								
									_emailSender = emailSender;
								
									}
								

								
									[BindProperty]
								
									public
									InputModel Input {
									get;
									set; }
								
									public
									string
									ProviderDisplayName {
									get;
									set; }
								
									public
									string
									ReturnUrl {
									get;
									set; }
								

								
									[TempData]
								
									public
									string
									ErrorMessage {
									get;
									set; }
								

								
									[ViewData]
								
									public
									string
									AssociateExistingAccount {
									get;
									set; } = "false";
								

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

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

								
									public
									IActionResult OnGet() => RedirectToPage("./Login");
								

								
									public
									IActionResult OnPost(string
									provider,
									string
									returnUrl =
									null)
								
									{
								
									// Request a redirect to the external login provider.
								
									var redirectUrl =
								
									Url.Page("./ExternalLogin", pageHandler: "Callback", values:
									new
									{ returnUrl });
								

								
									var properties =
								
									_signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
								

								
									return
									new
									ChallengeResult(provider, properties);
								
									}
								

								
									public
									async Task<IActionResult> OnGetCallbackAsync(
								
									string
									returnUrl =
									null,
									string
									remoteError =
									null)
								
									{
								
									returnUrl = returnUrl ?? Url.Content("~/");
								
									if
									(remoteError !=
									null)
								
									{
								
									ErrorMessage = $"Error from external provider: {remoteError}";
								
									return
									RedirectToPage("./Login",
									new
									{ ReturnUrl = returnUrl });
								
									}
								
									var info = await _signInManager.GetExternalLoginInfoAsync();
								
									if
									(info ==
									null)
								
									{
								
									ErrorMessage = "Error loading external login information.";
								
									return
									RedirectToPage("./Login",
									new
									{ ReturnUrl = returnUrl });
								
									}
								

								
									// Sign in the user with this external login provider if the user already has a login.
								
									var result = await _signInManager.ExternalLoginSignInAsync(
								
									info.LoginProvider, info.ProviderKey, isPersistent:
									false, bypassTwoFactor:
									true);
								

								
									if
									(result.Succeeded)
								
									{
								
									return
									LocalRedirect(returnUrl);
								
									}
								
									if
									(result.IsLockedOut)
								
									{
								
									return
									RedirectToPage("./Lockout");
								
									}
								
									else
								
									{
								
									// If the user does not have an account, then ask the user to create an account.
								
									AssociateExistingAccount = "false";
								
									ReturnUrl = returnUrl;
								
									ProviderDisplayName = info.ProviderDisplayName;
								
									if
									(info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
								
									{
								
									Input =
									new
									InputModel
								
									{
								
									Email = info.Principal.FindFirstValue(ClaimTypes.Email)
								
									};
								
									}
								
									return
									Page();
								
									}
								
									}
								

								
									private
									IdentityUser CreateUser()
								
									{
								
									try
								
									{
								
									return
									Activator.CreateInstance<IdentityUser>();
								
									}
								
									catch
								
									{
								
									throw
									new
									InvalidOperationException($"Can't create an instance of
									" +
								
									$"'{nameof(IdentityUser)}'.
									" +
								
									$"Ensure that '{nameof(IdentityUser)}' is not an abstract class
									" +
								
									$"and has a parameterless constructor, or alternatively
									" +
								
									$"override the external login page in
									" +
								
									$"/Areas/Identity/Pages/Account/ExternalLogin.cshtml");
								
									}
								
									}
								

								
									private
									IUserEmailStore<IdentityUser> GetEmailStore()
								
									{
								
									if
									(!_userManager.SupportsUserEmail)
								
									{
								
									throw
									new
									NotSupportedException(
								
									"The default UI requires a user store with email support.");
								
									}
								
									return
									(IUserEmailStore<IdentityUser>)_userStore;
								
									}
								
									}
								
}

image

When we run the project and click the external login provider button…

image

…we are authenticated, but not logged in, and our account is not created yet.

Also, clicking the Register button wont do anything at this point.

To handle the Register button, add the following method to ExternalLogin.cshtml.cs:

								
									public
									async Task<IActionResult> OnPostConfirmationAsync(string
									returnUrl =
									null)
								
{
								
									returnUrl = returnUrl ?? Url.Content("~/");
								
									// Get the information about the user from the external login provider
								
									var info = await _signInManager.GetExternalLoginInfoAsync();
								
									if
									(info ==
									null)
								
									{
								
									ErrorMessage = "Error loading external login information during confirmation.";
								
									return
									RedirectToPage("./Login",
									new
									{ ReturnUrl = returnUrl });
								
									}
								

								
									if
									(ModelState.IsValid)
								
									{
								
									var user = CreateUser();
								

								
									await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
								
									await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
								

								
									var result = await _userManager.CreateAsync(user);
								

								
									if
									(result.Succeeded)
								
									{
								
									result = await _userManager.AddLoginAsync(user, info);
								
									if
									(result.Succeeded)
								
									{
								
									var userId = await _userManager.GetUserIdAsync(user);
								
									var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
								
									code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
								
									var callbackUrl = Url.Page(
								
									"/Account/ConfirmEmail",
								
									pageHandler:
									null,
								
									values:
									new
									{ area = "Identity", userId = userId, code = code },
								
									protocol: Request.Scheme);
								

								
									await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
								
									$"Please confirm your account by
									" +
								
									$"<a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>" +
								
									$"clicking here</a>.");
								

								
									// If account confirmation is required, we need to show the link if
								
									// we don't have a real email sender
								
									if
									(_userManager.Options.SignIn.RequireConfirmedAccount)
								
									{
								
									return
									RedirectToPage("./RegisterConfirmation",
									new
									{ Email = Input.Email });
								
									}
								

								
									await _signInManager.SignInAsync(user, isPersistent:
									false, info.LoginProvider);
								
									return
									LocalRedirect(returnUrl);
								
									}
								
									}
								
									else
								
									{
								
									// There is an existing account
									
								
									// Check if that account has a password
									
								
									var ExistingUserToCheck = await _userManager.FindByEmailAsync(user.Email);
								

								
									if
									(ExistingUserToCheck !=
									null)
								
									{
								
									if
									(ExistingUserToCheck.PasswordHash ==
									null)
								
									{
								
									StringBuilder PasswordNotSetError =
									new
									StringBuilder();
								
									PasswordNotSetError.Append("There is an existing account with that email address.
									");
								
									PasswordNotSetError.Append("However, that account has no password set.
									");
								
									PasswordNotSetError.Append("Please log in to that account, with the
									");
								
									PasswordNotSetError.Append("existing external login method, and set a password.
									");
								
									PasswordNotSetError.Append("Then you can associate it with additional external
									");
								
									PasswordNotSetError.Append("login methods.");
								

								
									AssociateExistingAccount = "blocked";
								
									ModelState.AddModelError(string.Empty, PasswordNotSetError.ToString());
								
									return
									Page();
								
									}
								
									}
								

								
									// We can associate this login to the existing account
								
									AssociateExistingAccount = "true";
								
									}
								

								
									// Display any errors that occurred
								
									// Usually says email is already used
								
									foreach
									(var error
									in
									result.Errors)
								
									{
								
									ModelState.AddModelError(string.Empty, error.Description);
								
									}
								
									}
								

								
									ProviderDisplayName = info.ProviderDisplayName;
								
									ReturnUrl = returnUrl;
								
									return
									Page();
								
}

Note: When a user logs in, using an external login, if they click Register button or the Log in button, they always end up hitting: public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)

Add the following code to ExternalLogin.cshtml:

								
@if (Convert.ToString(ViewData["AssociateExistingAccount"]) == "true")
								
{
								
									<
									div
									class="row"
									>
								
									<
									div
									class="col-md-4"
									>
								
									<
									form
									asp-page-handler="AssociateLogin"
									asp-route-returnUrl="@Model.ReturnUrl"
								
									method="post"
									>
								
									<
									div
									asp-validation-summary="ModelOnly"
									class="text-danger"
									>
									</
									div
									>
								
									<
									p
									>To associate this external login with the existing account,
								
									please enter the password for that account.</
									p
									>
								
									<
									div
									class="form-floating"
									>
								
									<
									input
									asp-for="Input.Password"
									class="form-control"
									autocomplete="off"
									/>
								
									<
									label
									asp-for="Input.Password"
									class="form-label"
									>
									</
									label
									>
								
									<
									span
									asp-validation-for="Input.Password"
									class="text-danger"
									>
									</
									span
									>
								
									</
									div
									>
								
									<
									button
									type="submit"
									class="w-100 btn btn-lg btn-primary"
									>Associate Account
								
								
									</
									button
									>
								
									</
									form
									>
								
									</
									div
									>
								
									</
									div
									>
								
}

image

Now, when we run the project and click the Register button, we are presented with an option to enter the password of the existing account to associate the login.

However, at this point, clicking the Associate Account button won’t do anything.

Now, add the following code to ExternalLogin.cshtml (to handle the situation where we cannot associate an account because it doesn’t have a password):

								
@if (Convert.ToString(ViewData["AssociateExistingAccount"]) == "blocked")
								
{
								
									<
									div
									class="row"
									>
								
									<
									div
									class="col-md-4"
									>
								
									<
									form
									asp-page-handler="#"
									asp-route-returnUrl="@Model.ReturnUrl"
								
									method="post"
									>
								
									<
									div
									asp-validation-summary="ModelOnly"
									class="text-danger"
									>
									</
									div
									>
								
									</
									form
									>
								
									</
									div
									>
								
									</
									div
									>
								
}

Finally, add the following method to ExternalLogin.cshtml.cs to handle the Associate Account button:

								
									public
									async Task<IActionResult> OnPostAssociateLoginAsync(string
									returnUrl =
									null)
								
{
								
									returnUrl = returnUrl ?? Url.Content("~/");
								

								
									// Set AssociateExistingAccount so we return to this method on postback
								
									AssociateExistingAccount = "true";
								

								
									// Get the information about the user from the external login provider
								
									var ExternalLoginUser = await _signInManager.GetExternalLoginInfoAsync();
								

								
									if
									(ExternalLoginUser ==
									null)
								
									{
								
									ErrorMessage = "Error loading external login information during confirmation.";
								
									return
									RedirectToPage("./Login",
									new
									{ ReturnUrl = returnUrl });
								
									}
								

								
									if
									(Input.Password !=
									null)
								
									{
								
									try
								
									{
								
									// Get email of the ExternalLoginUser
								
									string
									ExternalLoginUserEmail = "";
								

								
									if
									(ExternalLoginUser.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
								
									{
								
									ExternalLoginUserEmail =
								
									ExternalLoginUser.Principal.FindFirstValue(ClaimTypes.Email);
								
									}
								

								
									// Check password against user in database
								
									var user = await _userManager.FindByEmailAsync(ExternalLoginUserEmail);
								

								
									if
									(user !=
									null)
								
									{
								
									var CheckPasswordResult =
								
									await _userManager.CheckPasswordAsync(user, Input.Password);
								

								
									if
									(CheckPasswordResult)
								
									{
								
									// user found and password is correct
								
									// add external login to user and sign in
								
									var AddLoginResult =
								
									await _userManager.AddLoginAsync(user, ExternalLoginUser);
								

								
									if
									(AddLoginResult.Succeeded)
								
									{
								
									await _signInManager.SignInAsync(user, isPersistent:
									false);
								
									return
									LocalRedirect(returnUrl);
								
									}
								
									else
								
									{
								
									foreach
									(var error
									in
									AddLoginResult.Errors)
								
									{
								
									ModelState.AddModelError(string.Empty, error.Description);
								
									}
								
									}
								
									}
								
									else
									// password is incorrect
								
									{
								
									ModelState.AddModelError(string.Empty, "Password is incorrect");
								
									}
								
									}
								
									}
								
									catch
									(Exception ex)
								
									{
								
									ModelState.AddModelError(string.Empty, ex.Message);
								
									return
									Page();
								
									}
								
									}
								
									else
								
									{
								
									ModelState.AddModelError(string.Empty, "Password is required");
								
									}
								

								
									// If we got this far, something failed, redisplay form
								
									return
									Page();
								
}

image

Now the page will allow us to associate the login to the existing account.

Links

Download

The project is available on the Downloads page on this site.

You must have Visual Studio 2022 (or higher) installed to run the code.

An error has occurred. This application may no longer respond until reloaded. Reload 🗙