7/5/2025 Admin

Customizing Blazor External Authentication Login (Blazor 9)


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. Simply follow the directions here: Facebook and Google authentication in ASP.NET Core | Microsoft Docs. However, you have to write custom code to associate an external login to an existing login.

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

Note: This is a Blazor 9 version of the previous blog post: Customizing The Blazor Login Process With External Authentication Providers (Blazor 8)

 

Application Flow

image

We start with a registration of a normal local account.

 

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.

Note: 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.

 

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 (updated to .Net 9) (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).

 

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 Code

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

Select a DbContext class and click the Add button.

 

image

The ExternalLogin page 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 for the UI:

 

@page "/Account/ExternalLogin"
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using Microsoft.AspNetCore.Identity
@using EndToEnd.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger
<h1>Register</h1>
<h2>Associate your @ProviderDisplayName account.</h2>
<hr />
<div class="alert alert-info">
    You've successfully authenticated with <strong>@ProviderDisplayName</strong>.
    Please click the Register button to finish logging in.
</div>
<!-- New‐user registration form -->
<EditForm 
    Model="Input"
    FormName="registerForm"
    OnValidSubmit="OnValidSubmitAsync">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="form-floating mb-3">
        <InputText @bind-Value="Input.Email"
                   class="form-control"
                   placeholder="Email"
                   autocomplete="email"
                   disabled="true" />
        <label>Email</label>
        <ValidationMessage For="() => Input.Email" />
    </div>
    <button type="submit" class="btn btn-primary w-100">
        Register
    </button>
</EditForm>

 

Add the following procedural code:

 

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;
    [SupplyParameterFromQuery] 
    string? ReturnUrl { get; set; }
    [SupplyParameterFromQuery]
    private string? RemoteError { get; set; }
    [SupplyParameterFromQuery]
    private string? Action { get; set; }
    public const string LoginCallbackAction = "LoginCallback";
    ExternalLoginInfo? externalInfo;
    InputModel Input = new();
    private string? ProviderDisplayName => externalInfo?.ProviderDisplayName;
    protected override async Task OnInitializedAsync()
    {
        if (RemoteError is not null)
        {
            RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
        }
        externalInfo = await SignInManager.GetExternalLoginInfoAsync();
        if (externalInfo is null)
        {
            RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
        }
        if (HttpMethods.IsGet(HttpContext.Request.Method))
        {
            if (Action == LoginCallbackAction)
            {
                await OnLoginCallbackAsync();
                return;
            }
            // We should only reach this page via the login callback, so redirect back to
            // the login page if we get here some other way.
            RedirectManager.RedirectTo("Account/Login");
        }
        externalInfo = await SignInManager.GetExternalLoginInfoAsync()
                       ?? throw new InvalidOperationException();
        Input.Email = externalInfo.Principal.FindFirstValue(ClaimTypes.Email)!;
    }
    private async Task OnLoginCallbackAsync()
    {
        if (externalInfo is null)
        {
            RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
        }
        // Sign in the user with this external login provider if the user already has a login.
        var result = await SignInManager.ExternalLoginSignInAsync(
            externalInfo.LoginProvider,
            externalInfo.ProviderKey,
            isPersistent: false,
            bypassTwoFactor: true);
        if (result.Succeeded)
        {
            Logger.LogInformation(
                "{Name} logged in with {LoginProvider} provider.",
                externalInfo.Principal.Identity?.Name,
                externalInfo.LoginProvider);
            RedirectManager.RedirectTo(ReturnUrl);
        }
        else if (result.IsLockedOut)
        {
            RedirectManager.RedirectTo("Account/Lockout");
        }
        // If the user does not have an account, then ask the user to create an account.
        if (externalInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input.Email = externalInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
        }
    }
    async Task OnValidSubmitAsync()
    {
        // try to create new user...
        var EmailStore = GetEmailStore();
        var user = Activator.CreateInstance<ApplicationUser>()!;
        await UserStore.SetUserNameAsync(user, Input.Email, default);
        await EmailStore.SetEmailAsync(user, Input.Email, default);
        var result = await UserManager.CreateAsync(user);
        if (result.Succeeded)
        {
            // link & sign in
            await UserManager.AddLoginAsync(user, externalInfo!);
            // Automatically confirm the email
            var token = await UserManager.GenerateEmailConfirmationTokenAsync(user);
            var confirmResult = await UserManager.ConfirmEmailAsync(user, token);
            // Log the user in
            await SignInManager.SignInAsync(user, false, externalInfo!.LoginProvider);
            RedirectManager.RedirectTo(ReturnUrl);
            return;
        }
        // if email already exists, navigate to associate page
        if (result.Errors.Any(e => e.Code == "DuplicateUserName"))
        {
            NavigationManager.NavigateTo($"/Account/ExternalLoginAssociate?returnUrl={Uri.EscapeDataString(ReturnUrl ?? "/")}");
            return;
        }
        // otherwise show errors in ValidationSummary...
    }
    private IUserEmailStore<ApplicationUser> GetEmailStore()
    {
        if (!UserManager.SupportsUserEmail)
        {
            throw new NotSupportedException("The default UI requires a user store with email support.");
        }
        return (IUserEmailStore<ApplicationUser>)UserStore;
    }
    class InputModel
    {
        [Required, EmailAddress]
        public string Email { get; set; } = "";
    }
}

 

image

Next, add a new page called ExternalLoginAssociate.razor.

This is the page that will be used to make the login association (if it is needed).

Replace all the code with the following code for the UI:

 

@page "/Account/ExternalLoginAssociate"
@using EndToEnd.Data
@using Microsoft.AspNetCore.Identity
@using System.Security.Claims
@using System.ComponentModel.DataAnnotations
@inject NavigationManager NavigationManager
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
<h3>Link your existing account</h3>
<div class="alert alert-info">
    There is an existing account with that email.
    To associate this external login with the existing account, please enter the password for that account.
</div>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
    <div class="alert alert-danger">@ErrorMessage</div>
}
<EditForm Model="Input"
          FormName="associateForm"
          OnValidSubmit="OnAssociateAsync">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="mb-3">
        <label>Password</label>
        <InputText type="password" 
            @bind-Value="Input.Password" 
            id="Input.Password" 
            class="form-control" 
            autocomplete="current-password" 
            aria-required="true" 
            placeholder="password" />
        <ValidationMessage For="() => Input.Password" />
    </div>
    <button type="submit" class="btn btn-primary">
        Associate Account
    </button>
</EditForm>

 

Add the following procedural code:

 

@code {
    [SupplyParameterFromQuery] public string? ReturnUrl { get; set; }
    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();
    private ExternalLoginInfo? externalInfo;
    private string? ErrorMessage;
    protected override async Task OnInitializedAsync()
    {
        externalInfo = await SignInManager.GetExternalLoginInfoAsync()
            ?? throw new InvalidOperationException("No external login info.");
    }
    private async Task OnAssociateAsync()
    {
        ErrorMessage = null;
        // 1) Ensure we have an email from the external provider
        var email = externalInfo?.Principal.FindFirstValue(ClaimTypes.Email);
        if (string.IsNullOrEmpty(email))
        {
            ErrorMessage = "External login did not provide an email.";
            return;
        }
        // 2) Find the local user
        var user = await UserManager.FindByEmailAsync(email);
        if (user == null)
        {
            ErrorMessage = "No local account exists for that email.";
            return;
        }
        // 3) Verify their password
        if (!await UserManager.CheckPasswordAsync(user, Input.Password!))
        {
            ErrorMessage = "Incorrect password.";
            return;
        }
        // 4) Link the external login
        var result = await UserManager.AddLoginAsync(user, externalInfo);
        if (!result.Succeeded)
        {
            ErrorMessage = string.Join(" ", result.Errors.Select(e => e.Description));
            return;
        }
        // 5) Sign in and redirect
        await SignInManager.SignInAsync(user, isPersistent: false);
        RedirectManager.RedirectTo(ReturnUrl);
    }
    private class InputModel
    {
        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; } = "";
    }
}

 

image

Now the code will allow us to associate an external login to the existing account.

 

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.

 

Links

An unhandled error has occurred. Reload 🗙