Scott Hanselman

Adding Two-Factor authentication to an ASP.NET application

April 8, '14 Comments [30] Posted in ASP.NET | ASP.NET MVC
Sponsored By
German Lorenz cipher machine by Timitrius used under CC Attributin

ASP.NET Identity 2.0 was released last month and it's got a number of significant updates and new features that are worth checking out. For historical context, read the "Introduction to ASP.NET Identity" article that includes a lot of background and information on why certain decisions were made, as well as an  overview of some of the goals of ASP.NET Identity 2.0 like:

  • One Identity system for ASP.NET Web Forms, MVC, Web API, and Web Pages
  • Total control over user profile schema.
  • Pluggable storage mechanisms from Windows Azure Storage Table Service to NoSQL databases
  • Unit Testable
  • Claims-based Auth adds more choice over simple role membership
  • Social Logins (MSFT, FB, Google, Twitter, etc)
  • Based on OWIN middleware, ASP.NET Identity has no System.Web dependency

You can watch a video of Pranav Rastogi and I upgrading the ASP.NET Membership systems on an older ASP.NET application to the latest bits. There's also migration docs in detail:

ASP.NET Identity is on CodePlex today (and soon to be open sourced...paperwork) at https://aspnetidentity.codeplex.com/ or access the NuGet feed for nightly builds.

Adding Two-Factor authentication to an ASP.NET application

I recently changed all my accounts online to two-factor auth, and I really recommend you do as well. Here's how to add Two-Factor Auth to an ASP.NET application using Identity 2.0.

You'll have a class that is a UserManager that handles access to users and how they are stored. Inside this manager there's an IIdentityMessageService that you can implement to validate a user with whatever you want, like email, SMS, or a time-based token.

Send Verification Code

Here's an example SmsService where I'm using Twilio to send text messages. Again, you can do whatever you want in your implementation.

public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your sms service here to send a text message.
message.Destination = Keys.ToPhone; //your number here
var twilio = new TwilioRestClient(Keys.TwilioSid, Keys.TwilioToken);
var result = twilio.SendMessage(Keys.FromPhone, message.Destination, message.Body);

return Task.FromResult(0);
}
}

If I were sending an EmailMessage, I'd do something like this. Note it's just another implementation of the same simple interface:

public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
string text = message.Body;
string html = message.Body;
//do whatever you want to the message
MailMessage msg = new MailMessage();
msg.From = new MailAddress("scott@hanselman.com");
msg.To.Add(new MailAddress(message.Destination));
msg.Subject = message.Subject;
msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));

SmtpClient smtpClient = new SmtpClient("smtp.whatever.net", Convert.ToInt32(587));
System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(Keys.EmailUser, Keys.EMailKey);
smtpClient.Credentials = credentials;
smtpClient.Send(msg);

return Task.FromResult(0);
}
}

In your IdentityConfig.cs you can register as many TwoFactorProviders as you'd like. I'm adding both Email and Sms here. They include token providers but again, everything is pluggable.

manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> {
MessageFormat = "Your security code is: {0}"
});
manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser> {
Subject = "SecurityCode",
BodyFormat = "Your security code is {0}"
});

manager.EmailService = new EmailService();
manager.SmsService = new SmsService();

If a user tries to login you need to make sure they are a VerifiedUser. If not, get a valid two factor provider and send them a code to validate. In this case, since there are two providers to choice from, I let them pick from a dropdown. Here's the POST to /Account/SendCode:

public async Task<ActionResult> SendCode(SendCodeViewModel model)
{
// Generate the token and send it
if (!ModelState.IsValid)
{
return View();
}

if (!await SignInHelper.SendTwoFactorCode(model.SelectedProvider))
{
return View("Error");
}
return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl });
}

The sender of the two factor code depends on your implementation, of course.

public async Task<bool> SendTwoFactorCode(string provider)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == null)
{
return false;
}

var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);
// See IdentityConfig.cs to plug in Email/SMS services to actually send the code
await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);
return true;
}

When it's time to get the code from them, they need to have logged in with name and password already, and we're now checking the code:

[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, string returnUrl)
{
// Require that the user has already logged in via username/password or external login
if (!await SignInHelper.HasBeenVerified())
{
return View("Error");
}
var user = await UserManager.FindByIdAsync(await SignInHelper.GetVerifiedUserIdAsync());
return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl });
}

We can sign users potentially a number of ways, like with External Sign Ins (Twitter, etc) but here's the TwoFactorSignIn

public async Task<SignInStatus> TwoFactorSignIn(string provider, string code, bool isPersistent, bool rememberBrowser)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == null)
{
return SignInStatus.Failure;
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
{
// When token is verified correctly, clear the access failed count used for lockout
await UserManager.ResetAccessFailedCountAsync(user.Id);
await SignInAsync(user, isPersistent, rememberBrowser);
return SignInStatus.Success;
}
// If the token is incorrect, record the failure which also may cause the user to be locked out
await UserManager.AccessFailedAsync(user.Id);
return SignInStatus.Failure;
}

If you want this blog post's sample code, make an EMPTY ASP.NET Web Application and run this NuGet command from the Package Manager Console

Install-Package Microsoft.AspNet.Identity.Samples -Pre

Have fun!

Related Links

* Photo of German Lorenz cipher machine by Timitrius used under CC Attribution 


Sponsor: Big thanks to Novalys for sponsoring the blog feed this week! Check out their security solution that combines authentication and user permissions. Secure access to features and data in most applications & architectures (.NET, Java, C++, SaaS, Web SSO, Cloud...). Try Visual Guard for FREE.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Tuesday, April 08, 2014 7:36:08 AM UTC
Nice post! Thanks! Two things that I would like to point out:

ASP.NET Identity is Open Source and you get the source here https://aspnetidentity.codeplex.com/ or access the NuGet feed for nightly builds.


Tuesday, April 08, 2014 7:41:06 AM UTC
Tugberk - Thanks! I'll fix that!
Tuesday, April 08, 2014 10:27:59 AM UTC
Pretty slick. This year i'm replacing asp.net R&M system that uses AzMan provider. Its a single app that we use to authenticate all of our intranet web applications via cross-app redirect. I'm hoping i can do the same using this asp.net identity.
Rich
Tuesday, April 08, 2014 10:44:58 AM UTC
Do you know what, Scott? I love the fact that for the last year the web dev team seem to be bringing out just what we (my team anyway) want, just when we need it.
I'm not sure if that means we're behind the curve, but it's very handy :)
Andy
Tuesday, April 08, 2014 1:49:26 PM UTC
Great article, on a subject that was mentioned multiple times at the recent DevWeek2014 conference in London. At some point I will have to sit down and digest it properly.

P.S. 'watch a video of Pranav Rastogi and I' - surely not... http://www.oxforddictionaries.com/words/i-or-me
Bob Armour
Tuesday, April 08, 2014 3:01:28 PM UTC
Hi Scott,

I came across an error while installing the nuget package and just thought I'd share. The following error is displayed when the install attempts to add EntityFramework 6.1:

Install failed. Rolling back...
Install-Package : Input string was not in a correct format.
At line:1 char:16
+ Install-Package <<<< Microsoft.AspNet.Identity.Samples -Pre
+ CategoryInfo : NotSpecified: (:) [Install-Package], FormatException
+ FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PowerShell.Commands.InstallPackageCommand


This post on Codeplex helped resolve the issue. It seems as though it may be caused by having multiple versions of Visual Studio installed.

Hope this helps.
Tuesday, April 08, 2014 3:56:42 PM UTC
And here's the (only existing current?) implementation of the Azure Table storage provider. It's in current development, and offers implementations for all the different stores used by Identity.

https://github.com/WillSullivan/AzureTableIdentityStorageProvider

I'm the only one currently using this, afaict, so I'm beating down bugs as they crop up.
Tuesday, April 08, 2014 4:06:06 PM UTC
I upgraded the NuGet package two nights ago containing ASP.NET Identity v2. I am not using Code First migrations, since I don't want it automatically updating my Azure staging database and I use custom stored procedures instead of EF. I prefer to use SQL scripts for schema changes. So it was a little bit of a headache. One of the columns in AspNetUserClaims is renamed from User_Id to UserId like it should have been originally. Additional columns are added to AspNetUsers and one dropped which wasn't a big deal; however, my identity model had already been extended to include Email and PhoneNumber which were new columns added to Identity, so I ended up having to drop them off of my model.

All in all I'm happy, I look forward to lockout and two-factor auth support now.
Ryan
Tuesday, April 08, 2014 6:06:03 PM UTC
Cool but this sample app nuget package is just tons of cruft. Would probably only snag necessary bits from it.
Wednesday, April 09, 2014 9:01:15 AM UTC
This is what I was thinking to implement in my startup.. Thanks!
Satya
Wednesday, April 09, 2014 6:14:35 PM UTC
Perfect... This is what I am looking for
Thursday, April 10, 2014 11:39:24 AM UTC
Scott, Enjoyed reading this article. Excellent!!!

I am planning to change my existing application to use ASPNetOneIdentity. Is it possible to call my exisitng repository class methods like UserRepository.Add from UserManager?

I would like to call like below, with out using EF, Async, await or Task


var user = new ApplicationUser() { UserName = model.UserName };
var result = UserManager.Create(user, model.Password);


Is it possible to customize Asp.Net one identity?
Murali
Saturday, April 12, 2014 1:32:54 AM UTC
@Murali
yes it is possible to do that with ASP.NET Identity
Pranav
Saturday, April 12, 2014 8:48:51 PM UTC
Kudos! Great information!
Sunday, April 13, 2014 2:35:18 PM UTC
Thanks, I was waiting for this kind for article
Abu Ali Muhammad Sharjeel
Thursday, April 17, 2014 9:36:32 AM UTC
How to add a Foreign key in Customer table (CreatedBy column) for AspNetUser table (Id column)?

So that when we add the CustomerController using default "MVC 5 Controller with views, using Entity Framework" scaffolding template should create a drop-down of users from AspNetUsers table.

(Refer stackoverflow.com/.../3542245 for more details related to question)
Lisa
Friday, May 09, 2014 2:16:40 PM UTC
It it possible to use ASP.NET Identity 2.0 to implement TWO-FACTOR AUTHENTICATION within my single page application?
Fex
Monday, May 26, 2014 6:59:02 PM UTC


Please you can make 2 factor authentication via google a asp.net script for the public they can download and modified & explain how they can change his google id and password .

its very help full for public like me.

Tuesday, May 27, 2014 9:35:47 AM UTC
@Fex
I recently upgraded my SPA to use ASP.NET Identity 2.0 with Two-factor authentication and had no problems at all.
Ofcourse it depends on the implementation of your SPA client when it comes to clientside routing, but it's pretty easy to get everything working.
Marcel
Tuesday, May 27, 2014 11:53:03 PM UTC
Identity is still useless overall for us since no support for Model First...
Dave
Wednesday, May 28, 2014 4:08:51 PM UTC
@Dave you can look at the following project which has a model first approach to Identity.
https://github.com/KriaSoft/AspNet.Identity
pranav
Saturday, June 07, 2014 12:43:14 PM UTC
so when the tables are built in sql there is an email col and a username col in the AspNetUsers table - both fields contain the same user's email address - and the user is logged in with the display of an email on the page header - welcome email@emailaddress.com the manage page in the account folder seems to manage passwords only.

In the login process whether I use a 3rd party provider or register within the site I don't see where a user has the option to choose a username like they did with the simple membership.

Can you point me in the direction of some out of the box username info?

thank you.
jeff
Wednesday, June 11, 2014 9:49:07 PM UTC
any suggestions on how to implement IoC with Identity 2 and MVC (using StructureMap). I have managed to get the IUserStore and IRoleStore registered but get an error generating tokens for Register and ForgotPassword. I am having a devil of time trying to work out how to register the IUserTokenProvider in the IoC container or elsewhere.
Friday, June 13, 2014 10:51:15 AM UTC
Dave here is another article on database first approach. This worked for me.

https://danieleagle.com/blog/2014/05/setting-up-asp-net-identity-framework-2-0-with-database-first-vs2013-update-2-spa-template/
Sean Miller
Friday, June 13, 2014 2:50:19 PM UTC
How do I automatically take a user that has just confirmed their email address to the AddPhoneNumber view to complete their validation process. Obviously the Manage views for adding a phone number, enabling TFA, and choosing a method of sending the pin is for development testing. By default TwoFactorEnabled is false.

My guess is tweeking IdentityConfig.cs?
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
Sean Miller
Friday, June 27, 2014 4:54:07 AM UTC
Yet another splendid Hanselman post :)

Your sample source code really helped me out today.

It also helped me determine that I needed to get the prerelease version of Microsoft.AspNet.Identity.Owin to use the SignInManager and enable the logic for determining if the 2 factor authentication code had been verified.

Cheers Mr H

Thursday, July 03, 2014 10:34:17 PM UTC
@jeff
To change your app to use username instead of email, you can change the ExternalLoginConfirmation and change the viewmodel to use username and pull in the username field in the controller action.
pranav
Thursday, July 03, 2014 10:37:14 PM UTC
@jon
you can look at the following question on StackOverflow which explains how to do this http://stackoverflow.com/questions/20372594/dependency-injection-structuremap-asp-net-identity-mvc-5
pranav
Thursday, July 03, 2014 10:37:15 PM UTC
@jon
you can look at the following question on StackOverflow which explains how to do this http://stackoverflow.com/questions/20372594/dependency-injection-structuremap-asp-net-identity-mvc-5
pranav
Thursday, July 03, 2014 10:38:55 PM UTC
@sean once the user confirm's their email then in the confirm email view you can add the view for adding a phonenumber. you can reuse the addphonenumber login from the manage page.
This is totally an application flow on how do you add a phone number.
pranav
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.