ASP.NET Core Identity or Azure AD B2C: choosing the front door
Own the password table or rent the login page. Both are fine choices; what is not fine is owning it by accident.
The first login page I was responsible for, in 2012, ran on ASP.NET Membership: aspnet_Users, aspnet_Membership, a stored procedure for every operation, and a salted SHA-1 hash that was already an embarrassment. MVC 4 had just shipped with SimpleMembership next to it, lighter and incompatible with the old schema, so we spent a fortnight choosing between two of Microsoft's password tables and told ourselves it was architecture. Three years later that corner of the codebase had a backlog of its own: reset mails landing in spam, a lockout rule that one person with a stuck keyboard could trigger for a whole office, a second factor bolted on by hand, and a support runbook titled "customer lost their phone". None of that was the product we sold. All of it was on our release calendar.
In 2012 there was no second door to draw. There is now, and it changes the question. Not "which is more secure", because both can be made secure, but "which product do you want to be in". If you keep the identity tables, you are, in a small way, an identity vendor. If you hand the door to an external provider, you are a tenant of theirs, with everything that implies about pricing, roadmaps and the occasional rename.
The drawing below is the one I still sketch first when a new team asks the question. What sits behind each door is what you will be maintaining.
Door A: the tables are yours
ASP.NET Core Identity gives you AspNetUsers, AspNetRoles, AspNetUserClaims and their friends, a UserManager<TUser> and a SignInManager<TUser>, and a password hasher (PBKDF2, with an iteration count that went from ten thousand to a hundred thousand in .NET 7). The framework does the cryptographic part well and I have never had a reason to distrust it, which is more than I could say for what I was handed in 2012. What it still does not do is run your operations.
Concretely, you own the transactional mail. Password reset, email confirmation, "your account was locked", "a new device signed in". You own the deliverability of those mails, which in practice means an SPF and DKIM conversation with whoever hosts the customer's domain. You own lockout tuning: five failures in five minutes is the default, and it is wrong for a shared reception desk and wrong again for a warehouse scanner that retries. You own MFA. The framework supports authenticator apps and recovery codes out of the box; SMS is yours to integrate and yours to pay for. And you own the audit story: when a customer's security officer asks how passwords are stored, you answer, not a vendor page.
The 2012 version of this configuration was a <membership> element in web.config naming a provider type in a string. Today it is a few lines in Program.cs:
builder.Services.AddIdentity<AppUser, IdentityRole<Guid>>(o =>
{
o.Password.RequiredLength = 12;
o.Password.RequireNonAlphanumeric = false;
o.Lockout.MaxFailedAccessAttempts = 8;
o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
o.SignIn.RequireConfirmedEmail = true;
o.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
No composition rule is what NIST has recommended since 2017; twelve characters is my own floor, and the template default of six characters with a symbol is a relic of the decade this sheet was first drawn in. Eight attempts and a ten-minute lockout is my compromise between brute force and the reception desk.
Door B: you rent the login page
With an external identity provider the browser leaves your site, signs in on a hosted page, and comes back with an ID token. Your application never sees a password. On Azure the first credible version of this was Azure AD B2C: a separate directory with user flows for sign-up and reset, custom HTML for the pages, and a free tier of fifty thousand monthly active users. It worked. A product I was responsible for moved to it in 2020, when a customer asked to sign in with their Microsoft 365 accounts and we did not want to write federation ourselves.
Then the ground shifted. Since 1 May 2025, Azure AD B2C is no longer sold to new customers, and Microsoft's successor is Microsoft Entra External ID: the same idea, an external tenant that holds customer accounts, but built on the Entra platform, so Conditional Access and the standard app registration model come with it. Existing B2C tenants keep running, with support promised until at least 2030; new products should start on External ID. If you read an earlier revision of this sheet and built on B2C, you have a migration on your roadmap, and I would rather you heard it here than from a deprecation banner.
What you rent: the hosted pages, MFA, password reset mails sent from a Microsoft domain (which do get delivered), federation to Google and to other Entra tenants, brute-force and password-spray protection at a scale you will never test yourself. What you give up: the login page is never quite your brand, custom attributes are capped (a hundred per user, which is plenty until it is not), and every clever requirement becomes a custom authentication extension calling back into your API.
The bill
Identity costs developer time, continuously. A conservative figure from three products: one or two days a month of somebody's attention, plus a week or two a year when something breaks in a way that reaches customers. The Azure line item is zero.
The external door costs money per monthly active user above the free tier, and time in a different shape: less code, more configuration, and a dependency on a vendor's release notes. On a product with four thousand active users the invoice was small enough that nobody looked at it. On a consumer-facing product with two hundred thousand it was a real number, and it was still cheaper than the engineer.
When you should never store passwords
There are three situations where I will not draw Door A at all. The first is when your customers are companies with their own directory. They want to sign in with the accounts they already administer, they want to revoke access by disabling the employee in that directory, and they will ask about it in the security questionnaire. Storing a second password for those people is a liability you are inviting.
The second is when you are small. A two-person team cannot run an identity system safely next to a product; the on-call load alone will decide it for you. The third is when your data is sensitive enough that a breach of the password table would end the company. Salary records qualify. Health data qualifies. Owning a table of credentials is a way of owning a risk that an external provider carries on your behalf, and their incident response team is bigger than yours.
The door I would draw today
For a business product I draw Door B, with Entra External ID for the tenant's own users and federation to the customer's Entra tenant when they have one (that is a sheet of its own, I-05). For a consumer product with a strong brand I still draw Door B, and I fight for the branding budget. I draw Door A only when the product is internal, the team is large enough to staff it, or the customer refuses to let user data leave their infrastructure, which happens with public bodies more often than you would think.
If you already have Door A and it works, I would not rip it out. I would stop adding to it, put the MFA and reset flows behind interfaces, and make the next customer who asks for "sign in with Microsoft" the moment you open the second door beside the first.
Drawn from
- Introduction to Identity on ASP.NET Corelearn.microsoft.com
- What is Azure Active Directory B2C?learn.microsoft.com
- Microsoft Entra External ID overviewlearn.microsoft.com
- Multi-factor authentication in ASP.NET Corelearn.microsoft.com