Introduction
Learn and easy way to stored passwords in a SQL-Server database table using an NuGet package BCrypt.Net-Next and Microsoft EF Core using a value converter.
Many new developers will store passwords in a text file or a database table which leaves these passwords open to prying eyes. Passwords should never be stored as plain text, instead they should be hashed.
Note
What is presented will work with other databases such as SQLite, Oracle, PostgreSQL and others. Also, data operations are not tied to EF Core, the base operations can be done for instance with Dapper.
Important
There are more secure methods to secure passwords. What is presented here an attacker that gains access to password hashes can still try to use brute force on them. With that known what is presented will not satisfy security of passwords for an enterprise like Amazon on a bank for instance.
BCrypt.Net-Next is a great start over no security such as plain text passwords in a database table.
For ASP.NET Core an option is PasswordHasher<TUser> Class
and a great article Exploring the ASP.NET Core Identity PasswordHasher on PasswordHasher that even Microsoft references.
Projects
Project name | Description |
---|---|
AdminApplication | Used to create mocked users |
EF_Core_ValueConversionsEncryptProperty | Creates database |
HashingNoDatabaseApp | Password hashing no database |
RazorPagesSample | ASP.NET Core example |
Requires
Microsoft Visual Studio 2022 or higher with .NET Core 8 available. Other editors/IDE like Rider and Microsoft VS-Code will work except for the Windows Forms project
Table structure
The base structure has enough columns to show password hashing, a primary key, username and password. More columns may be appropriate for business requirements.
EF Power tools
EF Power Tools Visual Studio extension was used to reverse engineer the database, yes, no migrations were used.
Sample project
The project is an ASP.NET Core project which has two pages performing a login with a hashed password. The only difference between the two pages is one provide a toggle reveal of password and the other does not.
Setup
- Install NuGet package BCrypt.Net-Next
- Install NuGet package Microsoft.EntityFrameworkCore.SqlServer
- Add the following package alias to the project file.
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<Using Include="BCrypt.Net.BCrypt" Alias="BC" />
</ItemGroup>
To keep the code simple, this project works against a single user in the database which is created and populated in a console project named EF_Core_ValueConversionsEncryptProperty
.
Running this project will use EF Core to:
- Create the database, if the database already exists it will be recreated.
- AuthenticateUser validate the new entry works with the correct plain text password
- NotAuthenticateUser validate the new entry works with the incorrect plain text password
Code
internal partial class Program
{
private static async Task Main(string[] args)
{
await Examples.CreateDatabase();
Console.WriteLine();
await Examples.AuthenticateUser();
Console.WriteLine();
await Examples.NotAuthenticateUser();
ExitPrompt();
}
}
The important aspect is using a value converter in OnModelCreating as shown below.
- First part of HasConversion hashes the given plain text password into the database table.
- The second part of HasConversion retrieves the hashed password from the table
public class Context : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => BC.HashPassword(v),
v => v);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(new DbContextToFileLogger().Log,
[DbLoggerCategory.Database.Command.Name],
LogLevel.Information)
.UseSqlServer(ConnectionString())
.EnableSensitiveDataLogging();
}
Note
If not using ASP.NET Core, the code provided above should be enough to hash passwords. And if not using EF Core, perhaps Dapper simply BC.HashPassword(plain text password).
Back to the ASP.NET Core project.
For dependency injection, add the following interface.
/// <summary>
/// Defines methods for authenticating users within the application.
/// </summary>
public interface IAuthentication
{
/// <summary>
/// Validates the specified user against the provided context.
/// </summary>
/// <param name="user">The user to validate, containing user credentials.</param>
/// <param name="context">The database context used to retrieve user information.</param>
/// <returns>
/// A tuple where the first element indicates whether the user is valid,
/// and the second element is the user's ID if validation is successful, or -1 if not.
/// </returns>
(bool, int) ValidateUser(User user, Context context);
}
Followed by the class to validate a password.
public class Authentication : IAuthentication
{
/// <summary>
/// Validates the specified user by comparing the provided password with the stored password in the database.
/// </summary>
/// <param name="user">The user object containing the credentials to validate.</param>
/// <param name="context">The database context used to access the stored user data.</param>
/// <returns>
/// A tuple containing a boolean and an integer:
/// <list type="bullet">
/// <item>
/// <description><c>true</c> if the provided password matches the stored password for the user; otherwise, <c>false</c>.</description>
/// </item>
/// <item>
/// <description>The user's ID if the password matches; otherwise, -1.</description>
/// </item>
/// </list>
/// </returns>
/// <remarks>
/// This method utilizes the BCrypt library to verify the password and logs the result of the authentication attempt.
/// </remarks>
public (bool, int) ValidateUser(User user, Context context)
{
var current = context.Users.FirstOrDefault(x => x.Name == user.Name);
return current is null ?
(false, -1) :
(BC.Verify(user.Password, current.Password), current.Id);
}
}
Add the following to Program.cs
builder.Services.AddScoped<IAuthentication, Authentication>();
For the login page
A primary constructor is used for the DbContext and authentication work.
public class IndexModel(Context context, IAuthentication authentication) : PageModel
A property for the mocked user.
[BindProperty]
public User CurrentUser { get; set; }
A property which is displayed to indicate success or failure. Of course a dialog may be used.
public string Message { get; set; } = "";
Frontend, there is a button with an event handler.
<button type="submit" class="btn btn-primary mb-3" asp-page-handler="ValidateUser">
Login
</button>
OnPost event for the above button which performs validation and uses SeriLog to show results along with setting text for a Bootstrap 5.3 alert @Html.Raw(Model.Message)
.
public void OnPostValidateUser()
{
var (authenticated, id) = authentication.ValidateUser(CurrentUser!, context);
Log.Information(authenticated ?
"{Id,-3} User {Name} authenticated" :
"User {Name} not authenticated", id,CurrentUser.Name);
Message = authenticated ? "Authenticated" : "Not authenticated";
}
Note that is the table configuration, for this sample reading one mocked user the HasConversion is not needed but would be needed for a real application accepting new users.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RazorPagesSample.Models;
namespace RazorPagesSample.Data.Configurations;
public partial class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> entity)
{
entity.ToTable("User");
entity.Property(e => e.Name).IsRequired();
entity.Property(e => e.Password).IsRequired();
entity.Property(e => e.Password).HasConversion(
v => BC.HashPassword(v),
v => v);
OnConfigurePartial(entity);
}
partial void OnConfigurePartial(EntityTypeBuilder<User> entity);
}
Adding more users
In the project AdminApplication (Windows forms) done cheaply using NuGet package Bogus to generate users.
- Keeps the first record
- If the database does not exists the app stops with a notice and ends gracefully.
To get at the plain text passwords used so they can be used UsersExposed.json
is created in the application folder.
[
{
"Id": 2,
"Name": "Arthur_Anderson37",
"Password": "wee98uD_Yj"
},
{
"Id": 3,
"Name": "Allen51",
"Password": "CwjTmtAVwz"
},
{
"Id": 4,
"Name": "Inez_Skiles26",
"Password": "3p0jAJh8IV"
},
{
"Id": 5,
"Name": "Marlon.Kreiger1",
"Password": "LMa_iXHiMW"
},
{
"Id": 6,
"Name": "Cody_Davis",
"Password": "z4_qWZWs7p"
}
]
Summary
With the code samples provided there is zero reasons to store plain text passwords in a database.
Top comments (25)
Why did you choose a 3rd party nuget package (albeit a well known one) over the built-in crypto libraries like Sha256, etc? I always opt for a built-in library over a 3rd party nuget that you have to maintain updates for among other things. Among many other classes, there's even ASP.NET Core's built-in PasswordHasher class
Many novice developers use nothing and can be bothered with figuring out the built-in libraries so this is one step up. This is perfect for novice developers and as they gain knowledge they can upgrade to the next level.
Certainly something is better than nothing. But unless you need something super custom (which is never recommended for security or encryption) it is far less code that you have to write and implement to use a built-in API. For example, you missed adding the salt. So right off the bat, this code is vulnerable and incorrect. A novice programmer wouldn't even know what a salt is. With the built-in API, salting is done for you and it's really hard to get it wrong. I'd suggest that your example is proof to use built-in API's and avoid a 3rd party nuget where you are required to do a lot more to make it secure is proof to use built-in API's.
It appears Karen doesn't know what salting is (or a rainbow attack either).
It should as a minimum have a disclaimer that the code as presented is insecure (with the why) at the top.
I know full well what salting and rainbow attack are.
Really? and yet you made a comment that this is good code for a novice (which is what the above code is, extremely amateurish when it comes to the most basic of security practices).
The responsible thing to do would be to edit this article and include a header that details why this code is insecure.
It is irresponsible to leave this article in it's current state with the stated comme t that there are no plans to make changes to this article.
I'm of the opinion (and have been for many years) that security code should come with jail time if a site is breached and shown to be vulnerable to the most basic of attacks, then hard time should be done by the designer of the security code.
I know this is a pointy view. This sort of code leads to detrimental effects down the line for people and negatively effects their lives. Over 35 years, I've seen it dozens of times.
Please do the right thing and either edit this article to include the use of salting, or just take it down.
The entire point of the article is to get novice developers to not use plain text passwords in a database and nothing more. As a developer advances in knowledge they hopefully will use other methods. Telling a novice developer this is not good enough for learning the likelihood of them using plain text is high, same as me giving them advance (to them) code now.
No, a novice dev will start with the built in security template. This is more secure than the code above (by a long margin).
A novice dev will do a Web search for c# and BCrypt (since it's popular and might be a good choic and ends up on this page.
If the novice dev went with the default template, then it would be a better choice.
If you wanted to do something to help novice devs, then it should be.
"Don't store passwords in the clear, don't garble your passwords in a reversible encoding where you can recover the password as clear text (ever), don't think that it's secure because you found out you need to hash. Don't write your own code that securely stores the passwords...it is 99.99% chance of being insecure. Get someone who knows why sites always get breached to do the code.and lastly... a li k to some correctly implemented code that also describes what the issues are and why to not attempt this as a jnr dev."
Hey...I know of snr devs who do not better than the novice.
All you have to do do...is fix your article. Instead, you are choosing to propagate bad practices for uneducated devs (of all skill levels).
I suspect that your refusal to update this article is because you don't have the skills to. Prove me wrong and gain back some security points at the same time!
As I said originally the article stands as is. All of the work I do is with on-prem Active Directory with enterprise policies to protect and provide rules for passwords.
Unfortunately, articles like this that omit critical pieces, and in this case knowingly do so, are the reason why this site and others like it are such low quality and bad for the industry. They contribute to ignorance of what is acceptable and complete. I understand your point about trying to write an intro article but what is the point of writing anything if you don't provide critical pieces about the content you are writing about.
So why then are you writing an article about Web based security?
If you are using AD policy, then you sit atop of ADs security hashes and don't actually get remotely close to the actual implementation of how Windows secures things and how that all ties into Kerberos, SASL.
Windows AD security and integrated authentication isn't even part of your article. If a novice developer used the default template in VS, then their solution could be abstracted from the underlying security storage and be swapped between integrated auth, or stored salted hashes via Web.co fig changes.
If you work in managing AD via group policy/intone, then write articles in that space.
Q: As an AD network admin, points for being able to explain why, on a domain member computer, when you change your password that doesn't meet the policy password requirements, that the modal window that opens that says your password doesn't meet the requirements, why that modal window doesn't tell you the policy rules!
The AD security my clients use are Yubico Authenticator or a client authenticator. Do not know what happens when a password does not match rules as I always follow the rules. My main client demands a password length of 30 with the usual rules.
I use a Yubikey myself.
It's still very far away from Web based password storage. That uses an assertion based auth mechanism with the secret stored under hardware protection that is used to do a key exchange for a shared key (e.g. Diffie-Helman).
I thinkn) it best to either fix this post, put a disclaimer, or take it down.
If this isn't your space, and you feel that the outdated default of having a jnr dev code their own vs the "lazy" approach of using the default generated code (with the latter being the most common path for at least the last 5 years, probably more like last 10), then you are out of you area of expertise.
Be security responsible and not propagate bad practices.
Do the right thing. Take it down.
** INSECURE CODE **
See my original comment.
The code above is novice code and is just as insecure as no hashes (via a rainbow attack)
Novice developers will take this code verbatim and add it to their side, never to be though off again, leaking peoples passwords to hackers who can reveal an equivalent password via a dictionary attack.
Karen: please stop professing the security merits of this code, it is ever slo slightly better than passwords given in the clear.
Also, Novice developers would more likely be using the starter template in VS (thankfully)
I offered to explain why the code is wrong, but are happy to make other comments about it being better than nothing, of which is isn't if it doesn't salt the password hashes. Instead, I got a "thanks for the feedback" and nothing else.
** DO NOT USE THIS CODE - IT'S VULNERABLE **
This code suffers from a Rainbox attack. Using it without modification opens your users to having their password "revealed" because there is no "salting" of the password hashes.
Yes, I know what a hash is. Google what a rainbow attach is before commenting. BCrypt is fine, not salting the password hashes is not fine.
I just wish people who don't have security expertise would strop writing code that pretends to be secure.
@karenpayneoregon - i'm happy for you to reach out what is wrong with this post and the code presented, and how it can be fixed.
Not doing so could lead to long term detrimental effects to anyone who uses this code and the passwords that get revealed as a result and down the line reuse of common passwords (which everyone does...still)
In the interim, this article should have an edit that says it has a vulnerability and to check back once it's fixed.
Thanks for your feedback.
I'd like to know if you plan to address the issues presented. A thanks for the feedback response is not sufficient given I have experience where posts like this lead (being used in someone else's templates, etc, etc.)
As someone who is experienced in security, this sort of code is the reason why jnr devs think they know how to build a secure site.
Saying that passwords are hashed is no where near enough. The code presented gives the passwords to the person who has the hashed passwords. There is no need to find a collision, you just download the BCrypt rainbow dictionary and do a lookup for the matching password.
Please respond with how you plan to fix this article.
Good security is about transparency and disclosure. Please demonstrate good security practices by letting everyone know what is planned for this article in terms of edits.
No plans to change anything in this article.
OK, I'll reach out to MS regarding your MVP status then.
You do know there are 23M using this library, if it was all that bad there would not be that many downloads.
BCrypt is the algorithm, it is not a library that implements a secure login.
As I mentioned in one of my comments, BCrypt is fine. It's how you have shown to use it above that is the problem.
Saying you are now secure because you hash passwords (without salt) is a false sense of security (others, please Google a Rainbox attack, you don't need to even break the hash when you have every BCrypt permuation available in a dictionary. You just lookup up the hash and viola..you have the password)
I offered to explain to you why it's insecure, but you have put up a wall.
This is a poor response and not something that bodes well for anyone in the computer security space.
Sorry.... but WOW, you clearly don't seem to know what's wrong with your implementation.
If you did, you wouldn't be implying your code is secure because BCrypt is secure (enough).
Such an ammitureish response.
Amazing.
Have a nice day.
I should add too, that this code is far less secure than the template code generated by VS projects that sit atop of EF.
Those templates do salting. Different algorithms. I always swap to SHA512 to because you cannot construct a rainbow dictionary for SHA512, ever! - A big thumbs up for anyone who comments below with the correct answer!
resemble with Google password manager web browser menu