Implementing Free Two-Factor Authentication in .NET using Google Authenticator

Username/password combinations don’t cut it anymore, and Two-Factor authentication is a great way to help secure user accounts. If you have an account with a system that supports it, you should be using it. Likewise, if you develop systems that require users to log in with a username and password, you should be offering it. By using two-factor authentication, you dramatically reduce the attack footprint – no longer would a nefarious individual have to just guess your password, but they would have to guess your password AND a PIN number that changes every couple of minutes.

We are in the middle of adding two-factor auth to a few of our own systems. There are a few sites (like Authy) that will operate this as a cloud service for you at a monthly or usage cost, but there’s no need. Google offers a completely free solution via the Google Authenticator app for iOS and Android, with an equivalent app just called 'Authenticator' for Windows Phone. I was surprised there were no really good libraries to use this method of two-factor authentication in an implementation. So, I made one.

What is two-factor authentication?

Two-factor authentication, as the name implies, requires users to supply normal credentials (a username and password, for example), but adds a second, real-time token to the login to verify the user’s identity.

Old-School Tokens

image

You may have seen these RSA keys floating around in Enterprise IT departments. These have been around forever, but have normally been a pain to implement and support.

Text Tokens

Some forms of two-factor authentication will text you a one-time unique token when needed. And that is fairly easy to implement. However, text tokens have a few drawbacks:

  • Not everyone is within cell carrier service all the time
  • Text costs can still be prohibitive (when traveling internationally, for example)
  • Sending true text/SMS messages costs money, through services like Twilio

Two-Factor Apps

Fortunately, this problem is easily solved by apps, and Google Authenticator and its similar alternatives are my pick.

There are others, but that should cover most users pretty well.

The workflow on this is straightforward, and can be used offline – tokens are algorithm-generated, and do not require a live internet connection on the user’s device.

  1. Your system/web site/app generates a two-factor token for the user. Perhaps a GUID, or any unique identifier string specific to that user.
  2. You give the user a code to add to the Google Authenticator app, or show them a QR code to scan for the easy way.
  3. Google Authenticator then generates a 6-digit PIN code every 30 seconds. Prompt the user for this code during their login, and validate it!

It looks like this on iPhone:

GoogleAuthScreenshot

Try it out!

Go get the app and give it a try to see how it works – I set up a sample workflow here: http://GAuthTwoFactorSample.azurewebsites.net

Scan the QR code into Google Authenticator, and then try validating your PIN code.

Now, on to implementation…

This is supposed to be easy, so start by grabbing the NuGet package GoogleAuthenticator (here’s a link).

image

Present User Setup QR Code / Manual Entry Code with GenerateSetupCode

Users have two options when setting up a new Google Authenticator account. If using a mobile device, they can scan a QR code (easiest), or they can enter or copy/paste a manual code into the app.

Generating this information takes a couple lines of code:

TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
var setupInfo = tfa.GenerateSetupCode("MyApp", "user@example.com", "SuperSecretKeyGoesHere", 300, 300);

string qrCodeImageUrl = setupInfo.QrCodeSetupImageUrl;
string manualEntrySetupCode = setupInfo.ManualEntryKey;

GenerateSetupCode requires a couple arguments:

  1. Issuer ID - this is the issuer ID that will appear on the user's Google Authenticator app, right above the code. It should be the name of your app/system so the user can easily identify it.
  2. Account Title – this will be displayed to the user in the Google Authenticator app. It cannot have any spaces (if it does, the library will filter them). The user’s e-mail address is appropriate to use here, if that works for your system.
  3. Account Secret Key – this is the unique user key that only your system knows about. A good length for this is 10-12 characters. Don’t show this to the user! Your users should never see it. I exposed it on the demo site just to show what’s going on.
  4. QR Code Width – width (in pixels) of generated QR code image
  5. QR Code Height – height (in pixels) of generated QR code image

It returns an object with a few notable properties:

  1. QrCodeSetupImageUrl – the URL to the QR code image that the user can scan (powered by Google Charts)
  2. ManualEntryKey – if the user can’t scan the QR code, this is the string they will need to enter into Google Authenticator in order to set up the two-factor account.

Validate a user’s PIN with ValidateTwoFactorPIN

Prompt the user for their current PIN displayed in Google Authenticator, and validate it:

TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
bool isCorrectPIN = tfa.ValidateTwoFactorPIN("SuperSecretKeyGoesHere", "123456");

That’s it!

About Clock Drift

Given that this two-factor authentication method is time-based, it is highly likely that there is some time difference between your servers and the user’s device. With these PIN codes changing every 30 seconds, you must decide what an acceptable ‘clock drift’ might be. Using the above code samples, the library will default to a clock drift tolerance of +/- 5 minutes from the current time. This means that if your user’s device is perfectly in sync with the server time, their PIN code will be ‘correct’ for a 10-minute window of time. However, if their device time is more than +/- 5 minutes off from your server’s time, the PIN code displayed on their device will never match up.

If you want to change this default clock drift tolerance, you can use the overloaded version of ValidateTwoFactorPIN, and provide a TimeSpan.

That’s all – hope this library is useful and makes two-factor authentication a no-brainer.

  • Is the source of your nuget package available? Is it open source?

    • Brandon,
      I also would love to see the source. Any chance you will make it available?
      tnx,
      JamBo

  • Thomas Bjerke

    Hi, awsome blog.
    How ever the library for .net 4.0 has a minor bug. the assembly reference is pointing to framework 4.5 instead of 4.0

    • Brandon

      Just saw this - was corrected today - see new package update.

  • Mrinal Kumar

    Hi Brandon, How I can capture anything related to device information. Please help.

    • Brandon

      Like what? (Example?)

      • Mrinal Kumar

        Thanks for the quick reply, Example like if I scan QR Code from my Nexus Mobile, a method which can return this information like Nexus with Device ID for log purpose.
        So if I again scan same QR Code from my IPad I will get IPad return with some unique deviceID for again logging purpose.

  • Brandon

    You can't do this on scan of the QR code (as its an offline process at that point), however in ASP.NET you can capture the User-Agent header from the request where you actually present the code to them (and store it along with the account's secret key you generated or whatever). The downside is that you won't know whether or not they scanned the code.

    On the other hand, you could capture the same user-agent value when they use the two-factor login for the first time, and then you would know what device they are using for two-factor login.

    • Mrinal Kumar

      Thank you Brandon I will check this in ASP.NET as per your suggestions.

  • David Mahaffy

    Great library, thanks!

  • Jijin George

    Hi Brandon,

    This a great article. I had already done these things as you specified here. What I want to implement next is the generation of backup codes (using these set of One time backup codes the user can login if the phone is not accessible). Could you please help me out by saying how to implement it..??

    Thanks,
    Jijin

  • Karteeka

    Hi Brandon- This is a very helpful blog. I have used this in our application.
    However the library generates the QR image URL as
    "http://chart.apis.google.com" instead of "https://chart.googleapis.com".

    The first URL is not compatible with IE while the later one is compatible with IE.
    Some of our users use IE and I am trying to make it work for them as well

    Do you have a solution for this?

    • Paul

      I used the following and it solved my issues:

      imgQRCode.ImageUrl = qrCodeImageUrl.Replace("http://chart.apis.google.com", "http://chart.googleapis.com");

  • Joshua

    I am developing in Mono, no Visual Studio, so I don't have package manager. Is the .dll available anywhere?
    Great idea to finally code this up. Great work!

  • Brandon
  • Atul Gandhale

    Really nice article it saved my time ..
    thank you very much.

  • Les

    what is the license for the GoogleAuthenticator nuget package?

    • yeurch

      Hey Les, according to the license file in the GitHub repo, it's licensed as Apache 2.0.

  • Rener Lemes

    Simple and functional.

    • Sanjib Mukherjee

      Its not working everytime its returning false

      • Syed Haider

        Have you put your public key to your App & get the generated pin from your device to the website ,it is working

    • Sanjib Mukherjee

      I am showing my code, its always returning false for ValidateTwoFactorPin

      public string base64Encode(string data)
      {
      try
      {
      byte[] encData_byte = new byte[data.Length];
      encData_byte = System.Text.Encoding.UTF8.GetBytes(data);
      string encodedData = Convert.ToBase64String(encData_byte);
      return encodedData;
      }
      catch (Exception e)
      {
      throw new Exception("Error in base64Encode" + e.Message);
      }
      }
      protected void GetLogin()
      {
      bool status = false;
      if (txtuser.Text == "developer.sanjib@gmail.com" && txtpass.Text == "123456")
      {
      status = true;
      Session["Username"] = txtuser.Text;
      TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
      string UserUniqueKey = base64Encode(txtuser.Text.Trim()+"A123$@**");
      Session["UserUniqueKey"] = UserUniqueKey;
      var setupInfo = tfa.GenerateSetupCode("Dotnet Awesome", txtuser.Text, UserUniqueKey, 300, 300);
      Session["BarcodeImageUrl"] = setupInfo.QrCodeSetupImageUrl;
      Session["SetupCode"] = setupInfo.ManualEntryKey;
      lblcode.Text = setupInfo.ManualEntryKey;
      Image1.ImageUrl = setupInfo.QrCodeSetupImageUrl;
      Panel1.Visible = false;
      Panel2.Visible = true;
      }
      else
      {
      lblerr.Text = "Invalid credential";
      }
      Session["Status"] = status;
      }
      protected void TwoFA()
      {
      TimeSpan span = new TimeSpan(0, 0, 0, 30, 0);
      TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
      string UserUniqueKey = Session["UserUniqueKey"].ToString();
      bool isValid = tfa.ValidateTwoFactorPIN(UserUniqueKey, txtcode.Text,span);
      if (isValid)
      {
      Session["IsValid2FA"] = true;
      lblshow.Text = "Hi Sanjib";
      }
      else
      {
      Session["IsValid2FA"] = false;
      lblshow.Text = "Sorry";
      }
      }
      protected void Button1_Click(object sender, EventArgs e)
      {
      GetLogin();
      }
      protected void Button2_Click(object sender, EventArgs e)
      {
      TwoFA();
      }

  • 2S

    Can you tell me how to use the function, GeneratePINAtInterval, and explain that what to do?

  • Pau

    I've tried to run this code in console and the isCorrectPin return false. why is it ? do I have to have a real emai or application?

    • Brandon Potter

      Console app should work fine. Post some code?

      • Pau

        It already works. I thought the manual entry code should be put directly.

  • Pau

    I have just one question though. The issuer ID and the account title, what is it for? If it doesn't help in creating the manualcode or the QR. What is it for then?

    • Brandon Potter

      It's text that shows up in Google Authenticator to help the user identify which account it is.

      • Pau

        Yes but when I use this code I didn't bother to change it. I've use manual entry into google authenticator. I've try to change the account name the same as what is written in my code and the key that is generated. the issuer id is actually nothing. Does it mean that I can use is without paying attention in this detail? I could make it "" but still it will verify. In short when will it be useful?

        • Brandon Potter

          You can still provide a setup code to your users without an issuer ID or account title; this just helps the user especially when they have multiple accounts in Google Authenticator.

          • Pau

            Is there a nuget package for sending text message using google two way other than just a nuget for the google authenticator app..

          • Brandon Potter

            There are plenty of services and packages to do that; however it's not free, which is the scope of this library.

            Twilio is popular.

      • Pau

        Sorry for having lots of questions. I just wanted all of the codes essential. 🙂

  • NoX1De

    Is there a way to set this up and make it work to protect IIS hosted websites? I suppose I'm not fully grasping what you are setting this up to protect or is that configurable? I don't use .NET so that may be part of my confusion...Any chance you could elaborate on the scope of use for this? Thank you!

    • Brandon Potter

      No, this is a library for use in ASP.NET code. If you're looking for something at the IIS-module level that adds two-factor authentication kind of like basic auth or NTLM, I'm not aware of a drop-in module that will do that. Generally two-factor auth needs to be built into the IIS application code that you're running.

  • Shyvoex

    How can I add this to my ASP.Net MVC App? o-o

  • Daniel

    This may sound stupid but, where do I have to place that code?

    • Brandon Potter

      Which part of the code?

      • Daniel

        Maybe I didn't understand correctly.

        Do I have to just write this:

        TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();

        var setupInfo = tfa.GenerateSetupCode("MyApp", "user@example.com", "SuperSecretKeyGoesHere", 300, 300);

        string qrCodeImageUrl = setupInfo.QrCodeSetupImageUrl;

        string manualEntrySetupCode = setupInfo.ManualEntryKey;

        and this:

        TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();

        bool isCorrectPIN = tfa.ValidateTwoFactorPIN("SuperSecretKeyGoesHere", "123456");

        somewhere to work, or are there other methods and classes that are required?

        Sorry if I didn't understand correctly but I'm trying to do this for a school assignment and I'm trying to "implement first, understand later"

        • Brandon Potter

          Ah, it's up to you where you put it in your app - but typically the "setup" part would be its own page somewhere in your app where you present the user with the QR code to scan with their device, and then record their secret key along with their account.

          Then, the ValidateTwoFactorPIN method is used wherever you decide to prompt the user for that two-factor PIN code and need to make sure its correct. Typically that's just after login, but you could do it anywhere.

  • srjnj

    Functionality given above doesn't give any backup codes.Is there any solution to get backup code?

  • Colin Goss

    In your example the validation code is always valid.

    The below images have been taken several images after the first validation.

    Edit: This looks like it's the result of not refreshing the information on the page. Disregard

  • You can add to that "Text Tokens" section another point: SMS tokens are insecure thanks to the prevalence of recent porting attacks and the new revelation that NIST has deprecated it as well https://techcrunch.com/2016/07/25/nist-declares-the-age-of-sms-based-2-factor-authentication-over/

    • PattyPatty

      How unsafe are the SMS tokens?

      Guess at someone's username.
      Guess at someone's password.
      Guess at their carrier name and phone number.
      Call their carrier and open a new phone line and port the number over.
      Get the SMS and PIN.

      Pretty hard to steal 100,000 accounts that way.

  • Oren Deri

    Great article! Is there a way to generate the QR code locally without accessing google site, meaning not using http://chart.googleapis.com ?

  • sufficey

    Very interesting. Thanks 😀

  • DORIT

    I working with integration to google authenticator and I have question about the concept, the idea in two factor auth is the extra layer of security that the piece of information that not exist in user , but if we supply the secret key to client he can save it and create extra google auth acount with that ??

  • Sanjib Mukherjee

    I am showing my code, its always returning false for ValidateTwoFactorPin

    public string base64Encode(string data)
    {
    try
    {
    byte[] encData_byte = new byte[data.Length];
    encData_byte = System.Text.Encoding.UTF8.GetBytes(data);
    string encodedData = Convert.ToBase64String(encData_byte);
    return encodedData;
    }
    catch (Exception e)
    {
    throw new Exception("Error in base64Encode" + e.Message);
    }
    }
    protected void GetLogin()
    {
    bool status = false;
    if (txtuser.Text == "developer.sanjib@gmail.com" && txtpass.Text == "123456")
    {
    status = true;
    Session["Username"] = txtuser.Text;
    TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
    string UserUniqueKey = base64Encode(txtuser.Text.Trim()+"A123$@**");
    Session["UserUniqueKey"] = UserUniqueKey;
    var setupInfo = tfa.GenerateSetupCode("Dotnet Awesome", txtuser.Text, UserUniqueKey, 300, 300);
    Session["BarcodeImageUrl"] = setupInfo.QrCodeSetupImageUrl;
    Session["SetupCode"] = setupInfo.ManualEntryKey;
    lblcode.Text = setupInfo.ManualEntryKey;
    Image1.ImageUrl = setupInfo.QrCodeSetupImageUrl;
    Panel1.Visible = false;
    Panel2.Visible = true;
    }
    else
    {
    lblerr.Text = "Invalid credential";
    }
    Session["Status"] = status;
    }
    protected void TwoFA()
    {
    TimeSpan span = new TimeSpan(0, 0, 0, 30, 0);
    TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
    string UserUniqueKey = Session["UserUniqueKey"].ToString();
    bool isValid = tfa.ValidateTwoFactorPIN(UserUniqueKey, txtcode.Text,span);
    if (isValid)
    {
    Session["IsValid2FA"] = true;
    lblshow.Text = "Hi Sanjib";
    }
    else
    {
    Session["IsValid2FA"] = false;
    lblshow.Text = "Sorry";
    }
    }
    protected void Button1_Click(object sender, EventArgs e)
    {
    GetLogin();
    }
    protected void Button2_Click(object sender, EventArgs e)
    {
    TwoFA();
    }

  • cuong dau

    Nice post. Thanks!

  • gjackson

    Hi there,
    Many thanks for providing this library. In addition to clock drift, are there any considerations or issues when users are authenticating from devices in different time zones?
    Thanks

    • Brandon Potter

      Excellent question. The epoch and current time are UTC based so time zone doesn't matter (so long as the user's device is configured correctly).

      • gjackson

        Cool, thanks a lot!

  • Shijo Thomas

    Thank you Brandon, it was a cakewalk. all worked well.

    • Brandon Potter

      Awesome!

  • gjackson

    Hi there,

    I am having a problem getting pin code validation to work properly when I use the manual entry key method vs. the barcode image scanning method to create the same user account in Google Authenticator on my phone.

    When I set up my user account on my phone by scanning the generated QR barcode image (as generated by the GenerateSetupCode() method), the ValidateTwoFactorPIN() method is working perfectly and as expected for every generated pin code.

    However, when I remove and set up the same user account again on my phone by manually entering the QR code, the ValidateTwoFactorPIN() method is always returning false for every generated pin code.

    I am 100% certain I am entering the correct manual entry key (as generated by the GenerateSetupCode() method), and the secret key passed each time in is identical. The only difference is how I set up the account on my phone.

    Any help or suggestions would be massively appreciated!?

    Many thanks,

    Graeme

    • gjackson

      Okay, in case anyone else has encountered this.., it turns out the issue is when the 32-character generated manual setup code is entered into Google Authenticator software in it's raw form, without any spaces, the pin code validation always fails.

      However, if I add a space character after every fourth character then pin code validations work as expected. For example:

      Entering 'IRIU2RKOKQZFUSZWJMZEQU2SIFJE4TCB' does not work

      Entering 'IRIU 2RKO KQZF USZW JMZE QU2S IFJE 4TCB' works

      This means I need to parse the manual setup code and add the spaces before displaying to the user. Phew..

  • Ranjith Kumar Kakkarayil

    Hi There. the below code is always returning false for me. Can you please help.

    bool isCorrectPIN = tfa.ValidateTwoFactorPIN("SuperSecretKeyGoesHere", "123456");

    • Brandon Potter

      You will need to replace "supersecretkeygoeshere" with your secret key. Then replace 123456 with the code generated by your authenticator app (changes every 30 sec).

      • Ranjith Kumar Kakkarayil

        That's changed Brandon. I was just wondering whether there will be any issue with the time / clock drift.

        • Brandon Potter

          Possibly, try passing a huge TimeSpan (like 24 hours) into the overload of the validate method to adjust the time drift tolerance. If that works, then narrow down from there...

          • Ranjith Kumar Kakkarayil

            ok. will try that.. thanks.

          • Ranjith Kumar Kakkarayil

            Thanks Brandon.. issue is fixed. Thanks for this awesome plugin .. cheers

          • Brandon Potter

            Cool what was it?

          • Ranjith Kumar Kakkarayil

            I was using special characters in the secret key. Removed the special character and it solved the problem.
            However i am not sure if that'st the exact issue, but removing the special character solved it.

  • Prashanth

    Woah. That is an incredibly useful piece of code. Thanks a ton Brandon!

  • Syed Haider

    Great Article ! i have a question how about sharing the same QR code to different users on different devices ,is that even possible ? i have some how increased the time span for validation Two factor Pin , it is now validating the old Pin code as well , kindly help me figure out this .

    • Brandon Potter

      Architecturally speaking, sharing the account should be the responsibility of the user's authenticator app - you should only show the setup code to the user once.

      Authy is a great iOS app example of this. When a user sets up a two-factor code in Authy, it can automatically sync that two-factor account across the user's iCloud devices.

      • Syed Haider

        Thanks brandon ! i have manually adjust the time setting of devices specifically for IOS since there is no option for time syncing in IOS google authenticator app . i am gonna try Authy as you advised ! again thanks a a lot

  • PattyPatty

    It's amazing that this can work even if your cell phone is in airplane-mode and has NO access to the outside world at all.

    But what if I pick the "counter" method instead? Without a connection.. how will the counter correctly increment on the server.... exactly the same number of times as my offline phone does?

    • Brandon Potter

      These types of two-factor authentication mechanisms are essentially a counter method with a hashing function. The server is calculating the code from a UTC epoch and your phone is calculating from a UTC epoch as well, so the current time is your counter.

  • PattyPatty

    So if some hacker steals our entire password database, they might be able to figure out some heavily salted/hashed passwords. (But I doubt it.)

    If they steal our other table (where the google authenticator codes are stored) won't they just plug them into any copy of google authenticator and instantly have everyone's PINs????

    How does this "solution" do anything more than just "move/copy:" the authentication problem to another place? (Steal Table #2, instead of just table #1)

    • Brandon Potter

      Two-factor auth is generally not designed to be a safeguard against a hacker stealing your own database. There are bigger issues there...

  • studies stuff

    Hi Brandon ,
    I am trying to create an ios application using the Xamarin framework included as part of .net 4.5 framework in visual studio and i had a problem in adding the nuget package as a reference to my project, Will that also work with the Xamarin Platform?

    • Brandon Potter

      I don't think the nuget package is targeted for Xamarin, however, you might be able to just copy the code from GitHub into a class in your project. It's very small.