Add Password to Local Account Linked to External Login

Add Password to Local Account Linked to External Login in ASP.NET Core Identity

In this article, I will discuss how to add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity. Please read our previous article discussing how to implement the Change Password in ASP.NET Core Identity. In ASP.NET Core Identity, users who register via external login providers (like Google or Facebook) typically don’t have a local password set. The Set Password functionality allows such users to create a local password for their account securely. This ensures they can log in using either their external provider or their newly created password for greater flexibility and control.

How to Add a Password to a Local Account Linked to an External Login:

Now, we will see how to add a Password to a Local Account Linked to an External Login so that the user can log in using both external authentication and their local username and password. Let us first understand the flow.

The following is our login page, and from this page, the user can log in using their local account or their external account. Let’s assume the user logs in using his Google External account by clicking on the Google button, as shown in the image below.

How to Add a Password to a Local Account Linked to an External Login

After logging in, verifying the UserLogins table will show that the external provider information is stored as shown in the image below. The UserLogins table in ASP.NET Core Identity is used to store the external user authentication details.

How to Add a Password to a Local Account Linked to an External Login

Now, if you check the Users table, you will see that the PasswordHash column value is Null for the above External Login user. Our requirement now is to enable external users to log in to our system using both External Login and Local Username and Password. Let us first understand the flow.

How to Add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity?

Now, we need to display either the Set Password or Change Password link based on whether the logged-in user has set a local password or is using external authentication without a local password. If the user sets the password, we need to display the Change Password link; else, we need to display the Set Password link. Let us assume the user logged in using external authentication and has not set a local password. In that case, we are providing the option to add a Local Password.

How to Add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity?

Once the user clicks on the Set Password button, it will open the following Set Password page. Here, you can see that we don’t have a Current or Old Password text box, which makes sense since this is an external user who hasn’t set a password earlier.

How to Add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity?

After the user provides and confirms their password, clicking the Set Password button, the system validates the password. If valid, it sets the password in the Users table and redirects the user to the confirmation page.

How to Add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity?

Now, if you verify the above user details in the Users table, you will see that the PasswordHash column should be set.

Now, the user can log in using external authentication as well as using a local user account. Once the password is set for the external user, you will see the Change Password button instead of Set Password, as shown in the image below, as we have set a local password for the external user:

Add Password to Local Account Linked to External Login

Let us proceed and see how we can add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity.

Steps to Follow:

To add a password to a local account that’s linked to an external login in ASP.NET Core Identity, we need to follow the steps below.

  • User Authentication: Ensure the user is authenticated through their external login.
  • Retrieve User Information: Once authenticated, retrieve the user’s information from the ASP.NET Core Identity database.
  • Check for Existing Password: Check if the user already has a password set. This can be done using the UserManager.HasPasswordAsync method.
  • Set Password UI: If the user doesn’t have a password, provide a UI for them to input a new password. This typically involves creating a view with a form for password input.
  • Password Validation: Validate the password input according to the password policy configuration (e.g., length, complexity).
  • Update User Account: Use UserManager.AddPasswordAsync method to add the password to the user’s account. This method will hash the password and store it in the database.
  • Handle Success or Failure: Depending on the result of AddPasswordAsync, we will redirect the user to a confirmation page or show error messages.

Note: Once the password is set, the user should now be able to sign in using both the local account (with the new password) and the external login.

Creating Set Password View Model

First, create a ViewModel to represent the data needed to add a password. Add a class file named SetPasswordViewModel.cs within the ViewModels folder and copy and paste the following code. As the external user don’t have password in the database, so, in the Set Password View, we don’t need the Current Password. So, the View Model has two properties: Password and ConfirmPassword.

using System.ComponentModel.DataAnnotations;

namespace ASPNETCoreIdentityDemo.ViewModels
{
    public class SetPasswordViewModel
    {
        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password, ErrorMessage = "Invalid password format.")]
        [Display(Name = "New Password")]
        public string Password { get; set; } = null!;

        [Required(ErrorMessage = "Please confirm your password.")]
        [DataType(DataType.Password, ErrorMessage = "Invalid password format.")]
        [Display(Name = "Confirm New Password")]
        [Compare("Password", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; } = null!;
    }
}
Update the Service Interface

Add service methods in your IAccountService:

using ASPNETCoreIdentityDemo.ViewModels;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;

namespace ASPNETCoreIdentityDemo.Services
{
    public interface IAccountService
    {
        //Existing Methods
        Task<IdentityResult> RegisterUserAsync(RegisterViewModel model);
        Task<IdentityResult> ConfirmEmailAsync(Guid userId, string token);
        Task<SignInResult> LoginUserAsync(LoginViewModel model);
        Task LogoutUserAsync();
        Task SendEmailConfirmationAsync(string email);
        Task<ProfileViewModel> GetUserProfileByEmailAsync(string email);

        //New Methods for External Login
        AuthenticationProperties ConfigureExternalLogin(string provider, string? redirectUrl);
        Task<ExternalLoginInfo?> GetExternalLoginInfoAsync();
        Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent);
        Task<IdentityResult> CreateExternalUserAsync(ExternalLoginInfo info);

        //New Methods for Forgot Password
        Task<bool> SendPasswordResetLinkAsync(string email);
        Task<IdentityResult> ResetPasswordAsync(ResetPasswordViewModel model);

        //New Method for Changes Password
        Task<IdentityResult> ChangePasswordAsync(ChangePasswordViewModel model, HttpContext httpContext);

        //New Method for Adding Local password to Exernal provider
        Task<bool> HasPasswordAsync(ClaimsPrincipal principal);
        Task<IdentityResult> SetPasswordAsync(SetPasswordViewModel model, ClaimsPrincipal principal);
    }
}
Service Implementation

Now, we need to implement the newly added methods in the AccountService. So, please add the following two methods to the AccountService class.

// Check if user has an existing password
public async Task<bool> HasPasswordAsync(ClaimsPrincipal principal)
{
    var user = await _userManager.GetUserAsync(principal);
    if (user == null) return false;

    return await _userManager.HasPasswordAsync(user);
}

// Add a new password to an external login account
public async Task<IdentityResult> SetPasswordAsync(SetPasswordViewModel model, ClaimsPrincipal principal)
{
    var user = await _userManager.GetUserAsync(principal);
    if (user == null)
    {
        return IdentityResult.Failed(new IdentityError
        {
            Code = "UserNotFound",
            Description = "User not found or session expired."
        });
    }

    return await _userManager.AddPasswordAsync(user, model.Password);
}
Update AccountController

Now, please add the following methods to the Account Controller.

SetPassword (GET)

This method displays the Set Password page for users who do not yet have a local password (e.g., users registered with external logins like Google or Facebook). If the logged-in user already has a password, they are redirected to the Change Password page instead, since setting a new one is unnecessary.

[HttpGet]
public async Task<IActionResult> SetPassword()
{
    if (await _accountService.HasPasswordAsync(User))
        return RedirectToAction("ChangePassword");

    return View();
}
SetPassword (POST)

This method processes the submitted Set Password form. It first validates the form input and then delegates the business logic to the service layer (_accountService.SetPasswordAsync). If the operation succeeds, the user is redirected to the Set Password Confirmation page with a success message. If it fails (e.g., password doesn’t meet complexity rules), the errors are added to ModelState and the form is redisplayed with feedback.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    var result = await _accountService.SetPasswordAsync(model, User);

    if (result.Succeeded)
    {
        TempData["SuccessMessage"] = "Your password has been set successfully.";
        return RedirectToAction("SetPasswordConfirmation");
    }

    foreach (var error in result.Errors)
        ModelState.AddModelError(string.Empty, error.Description);

    return View(model);
}
SetPasswordConfirmation (GET)

This method renders a confirmation page after a password has been successfully set. It provides a friendly acknowledgment to the user that their account is now secured with a local password.

[HttpGet]
public IActionResult SetPasswordConfirmation()
{
    return View();
}
Creating SetPassword View:

Add a view named SetPassword.cshtml within the Views/Account folder and then copy and paste the following code. This is the view where the user will input the new password and confirm the password.

@model ASPNETCoreIdentityDemo.ViewModels.SetPasswordViewModel

@{
    ViewData["Title"] = "Set Password";
}

<div class="container mt-2">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow">
                <div class="card-header text-center bg-primary text-white">
                    <h4>Set Your Password</h4>
                </div>
                <div class="card-body">
                    <p class="text-info text-center mb-4">
                        You are currently signed in using an external account. To enhance your account security,
                        you can set a local password that allows you to log in directly with your email.
                    </p>
                    <form method="post" asp-action="SetPassword" asp-controller="Account">
                        <div asp-validation-summary="All" class="text-danger"></div>

                        <div class="form-group mb-3">
                            <label asp-for="Password" class="form-label">New Password</label>
                            <input asp-for="Password" class="form-control" placeholder="Enter a strong password" />
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>

                        <div class="form-group mb-4">
                            <label asp-for="ConfirmPassword" class="form-label">Confirm Password</label>
                            <input asp-for="ConfirmPassword" class="form-control" placeholder="Re-enter your password" />
                            <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
                        </div>

                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">Set Password</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
Creating SetPasswordConfirmation View:

Add a view named SetPasswordConfirmation.cshtml within the Views/Account folder and then copy and paste the following code. This is the view where the user will be redirected after successfully setting the password.

@{
    ViewData["Title"] = "Set Password Confirmation";
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow">
                <div class="card-header bg-success text-white text-center">
                    <h4>Password Successfully Set</h4>
                </div>
                <div class="card-body text-center">
                    <p class="text-success fs-5">
                        Congratulations! You have successfully set a local password for your account.
                    </p>
                    <p class="text-muted">
                        You can now log in using either your local account credentials or continue using your external authentication provider.
                    </p>
                    <div class="mt-4">
                        <a href="/" class="btn btn-primary btn-lg me-2">Go to Home</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
Modifying the Layout Page:

To conditionally display either the “Change Password” or “Set Password” button based on the user’s authentication method and whether they have a local password set, we can use the UserManager and SignInManager services provided by ASP.NET Core Identity.

  • First, we need to inject the UserManager<ApplicationUser> service into our Layout view. This service enables us to interact with user-related operations, such as verifying whether a user has set a password.
  • Then, we need to use a Razor code block to asynchronously retrieve the current user and determine whether they have a password set. Based on this, modify the My Account dropdown menu to conditionally display the “Set Password” or “Change Password” link.

So, please modify the _Layout.cshtml view as follows:

@using Microsoft.AspNetCore.Identity
@inject UserManager<ApplicationUser> UserManager
@{
ViewData["Title"] = ViewData["Title"] ?? "Dot Net Tutorials";
bool isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
bool isAdmin = User?.IsInRole("Admin") ?? false;
bool isManager = User?.IsInRole("Manager") ?? false;
bool isStaff = isAdmin || isManager; // Admin OR Manager
// Default
bool hasPassword = false;
if (isAuthenticated)
{
var currentUser = await UserManager.GetUserAsync(User!);
hasPassword = currentUser != null && await UserManager.HasPasswordAsync(currentUser);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@ViewData["Title"] - Dot Net Tutorials</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
</head>
<body class="d-flex flex-column min-vh-100">
<!-- Dark Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand fw-bold" href="@Url.Action("Index", "Home")">Dot Net Tutorials</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<!-- Left: Primary navigation -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="@Url.Action("Index", "Home")">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="@Url.Action("About", "Home")">About</a>
</li>
<!-- Any authenticated user -->
@if (isAuthenticated)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="SecureMethod">
Secure
</a>
</li>
}
<!-- Public -->
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="NonSecureMethod">
Non Secure
</a>
</li>
<!-- Admin-only -->
@if (isAdmin)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Roles" asp-action="Index">
Roles
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-controller="Claims" asp-action="Index">
Claims
</a>
</li>
}
<!-- Admin OR Manager -->
@if (isStaff)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Users" asp-action="Index">
Users
</a>
</li>
}
</ul>
<!-- Right: Auth links / user menu -->
<ul class="navbar-nav mb-2 mb-lg-0">
@if (isAuthenticated)
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>
Hello, @User!.Identity!.Name
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item" href="@Url.Action("Profile", "Account")">
<i class="bi bi-person-lines-fill me-2"></i> Profile
</a>
</li>
@if (hasPassword)
{
<li>
<a class="dropdown-item" asp-controller="Account" asp-action="ChangePassword">
<i class="bi bi-key me-2"></i> Change Password
</a>
</li>
}
else
{
<li>
<a class="dropdown-item" asp-controller="Account" asp-action="SetPassword">
<i class="bi bi-key me-2"></i> Set Password
</a>
</li>
}
<li><hr class="dropdown-divider" /></li>
<li>
<form method="post" asp-controller="Account" asp-action="Logout" class="px-3 py-1">
<button type="submit" class="btn btn-link text-decoration-none w-100 text-start">
<i class="bi bi-box-arrow-right me-2"></i> Logout
</button>
</form>
</li>
</ul>
</li>
}
else
{
<li class="nav-item"><a class="nav-link" href="@Url.Action("Login", "Account")">Login</a></li>
<li class="nav-item"><a class="nav-link" href="@Url.Action("Register", "Account")">Register</a></li>
}
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="container flex-grow-1 py-3">
@RenderBody()
</main>
<!-- Footer -->
<footer class="bg-dark text-light py-4 mt-auto">
<div class="container d-flex flex-column flex-md-row justify-content-between align-items-center">
<div>
&copy; @DateTime.Now.Year Dot Net Tutorials. All rights reserved.
</div>
<div>
<a href="@Url.Action("Contact", "Home")" class="text-light me-3 text-decoration-none">Contact</a>
<a href="@Url.Action("Privacy", "Home")" class="text-light me-3 text-decoration-none">Privacy Policy</a>
<a href="@Url.Action("Terms", "Home")" class="text-light text-decoration-none">Terms of Service</a>
</div>
</div>
</footer>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
When to Add a Password to a Local Account Linked to an External Login?

Adding a password to a local account that is linked to an external login is essential for maintaining both security and flexibility. The following are key scenarios where adding a password to your local account is beneficial:

  • Enhanced Security: If the external service (such as Google, Facebook, or another provider) experiences a security breach, having a separate password for your local account provides an additional layer of security. This way, even if your external login credentials are compromised, your local account remains secure.
  • Backup Access: If you lose access to your external account (due to reasons like forgotten credentials, account suspension, or provider issues), a local password ensures that you can still access your account. Without a local password, you may be entirely dependent on the external login service, which could lead to account lockout.
  • Separation of Concerns: If you prefer to keep your external login credentials separate from your local account for privacy or security reasons, adding a password to your local account can help. This gives you more control over your credentials and reduces the dependency on a third-party provider.
  • Transitioning Away from External Login: If you plan to stop using the external login service in the future, adding a password to your local account prepares your account for a smooth transition. This ensures you won’t be left without access to your account once you discontinue using the external service.
  • Different Privilege Levels: If your local account has higher privileges or accesses more sensitive information than your external login, a local password can act as an additional safeguard. This is especially important in cases where you manage sensitive data or require different levels of access within the application.

By implementing the Set Password feature, we enhance the overall account management experience for external login users. It gives them the option to transition to or supplement their login method with a secure local password, improving both convenience and security.

In the next article, I will discuss how to implement Account Lockout in ASP.NET Core Identity. In this article, I explain how to add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity. I hope you enjoy this article. Add Password to Local Account Linked to External Login in ASP.NET Core Identity.

Leave a Reply

Your email address will not be published. Required fields are marked *