Two Factor Authentication with ASP.Net Core Identity
I have recently become interested in working with ASP.NET Core Identity in web applications, specifically in relation to two-factor authentication. This project was my effort to learn it better. I have also been interested in the use of Yubikeys to provide two-factor authentication. As it just so happens, I had a Yubikey 5 NFC available to me, so I could integrate that into this exercise as well.
This post will talk about:
- Implementing two-factor authentication with Identity, and
- Implementing custom two-factor authentication providers for Identity
Identity comes with a few support libraries to make using it a bit easier. For example, there are libraries to use Entity Framework Core storage
and libraries that implements the login and account management pages
for you. As this is a learning experience, I was not interested in using those and decided to implement it from the MVC template of the dotnet command.
I also decided to focus on the Idenity portion and not make an effort on making it nice or easy to use. For that purpose, I removed all styling and just depend on the default browser styles. Failures usually just result in the operation being cancelled and re-display the form without indication of what went wrong.
Finally, this post was also going to include setting this project as an Identity Provider with the Identity Server library. I decided a bit late in the process to push that to another project and post. That’s something to hopefully look forward to in the future!
The code for this post can be found here: https://gitlab.com/logandrews/mfa/-/tree/master
Implementing Basic Login
Adding Identity to Startup.cs
To begin, Identity needs to be added to the application services. The Microsoft.AspNetCore.Identity library provides methods to do so.
Because I wanted to implement as much as possible, I originally tried to use the AddIdentityCore<TUser>() method to add the minimal amount of classes. I experienced some issues with missing dependencies (primarily, AddIdentityCore does not add the SignInManager<TUser> class to services), so I decided to to use the AddIdentity<TUser, TRole>() method. At the moment, I don’t plan to implement roles, so that feature is mostly just ignored.
Implement IUserStore and IRoleStore
I created custom implementations of the User and Role class called ApplicationUser
and ApplicationRole
, respectively. These classes just consist of some public properties and most of them exist to implement features of IUserStore<TUser>.
To keep the implementation of IUserStore<TUser> simple, I just store the data on a static variable on the class. This also has the nice side effect that any changes you make are undone if you restart the application, so you can’t mess up the baseline data. The ApplicationUserStore
implements IUserPasswordStore<TUser>, IUserTwoFactorStore<TUser>, and IUserLockoutStore<TUser>, which are necessary for using the SignInManager<TUser> with two factor authentication.
The ApplicationRoleStore
follows the same idea, but for roles. Again, I’m not doing anything with roles at the moment.
Implement Authentication and Management
Before implementing two-factor authentication, I implemented basic login and allow the user to login and manage their password.
The login functionality exists in the AuthController
and the account management exists in the AccountController
.
Most of this functionality is using UserManager<TUser> and SignInManager<TUser> directly, so the details here are not all that interesting.
Implement Two-Factor Provider
I want to start out with a discussion of multi-factor authentication model used by ASP.Net Core. The model appears to be that an account just enables two-factor authentication and when they login, they choose what provider they want to use according to what they have enabled. For example, if they have confirmed their email and their phone, they can select between getting a code by phone or by email, but if they had only confirmed their email, they would only be able to receive the code by email.
I don’t like this. I feel this increases the attack surface and decreases security. The implementation I follow below is more focused on enabling a single form of two-factor authentication and excluding the others. I think provides the best security and generally makes the way that makes the most sense to me.
To implement a new way of creating and verifying a two-factor authentication token, you need to implement the interface IUserTwoFactorTokenProvider<ApplicationUser>, which has three methods to implement. The method CanGenerateTwoFactorTokenAsync determines if this method can be used by the account. This method isn’t used in my implementation because it has to do with the above model. For example, for the EmailTokenProvider
determines if the user has an email that has been confirmed. For my implementation, the account stores their selected two factor authentication provider in the TwoFactorProvider property. The other two methods are GenerateAsync and ValidateAsync, which generates an authentication token and validates an authentication token, respectively. The token provider implementation also needs to be registered with Identity at application startup.
To gain a better understanding of the interface, I decided to implement a simple token provider: the ConstantTwoFactorTokenProvider
. When you have selected this two-factor authentication method, you confirm your login with a constant value that is configured in the application. This method doesn’t really provide any additional security, but it was a useful learning process.
The implementation of CanGenerateTwoFactorTokenAsync always returns true. In the ASP.NET model for two-factor authentication, there are no requirements for using this provider. For my implementation, it’s irrelevant.
The implementation of GenerateAsync is a bit more interesting because this provider doesn’t actually generate a token. I was unsure what to put in this method initially, but, luckily, there is a provider that doesn’t generate tokens in the ASP.NET Identity library: the AuthenticatorTokenProvider
. The AuthenticationTokenProvider is used for authenticator applications (such as Google Authenticator
app), which is which generates the token and then the provider just validates it. That provider just returns an empty string, so I follow that same logic.
The ValidateAsync method implementation is pretty straight-forward as well. It just compares the provided token with the configured constant value. I use the Options pattern
to configure the constant. Otherwise, it just compares the string in a case-sensitive, culture-invariant manner for equality.
That’s it. I now had a provider that could be used. I just need to implement login and management functionality for two-factor authentication.
Implement Two-Factor Login
When you initially login with two-factor authentication enabled, the login result will fail, but the RequiresTwoFactor will be set to true. When that happens, you redirect to the two-factor page with an input to receive the token. The token is validated by providing the users TwoFactorProvider and the token to the SignInManager’s TwoFactorSignInAsync method.
As I was writing this, I realized I had a pretty big security vulnerability in the code. When the page was displayed, the provider was written out to a hidden field and used as the method to validate the token. This would have allowed a clever attacker to change the method and token before submitting the form. Thus, if you had a Yubikey for two-factor authentication, they could change the provider to ‘constant’ and provide the constant to do two-factor authentication into your account. This fix was simply using the two-factor provider saved to the account , rather than the provider submitted. Oops!
Overall, I thought implementing the login process was pretty straightforward.
Implement Two-Factor Management
Managing two-factor authentication was implemented in the AccountController.
The account management page shows your current provider or enables you to enable a provider.
The enable page has a list of two-factor authentication providers you can choose from. In this case, there is only the Constant provider.
Clicking on the provider will take you to the enable page. Rather than just immediately enabling it, there is some user interaction. In the case of the Constant provider, we just ask they type in the constant value, which is shown to them. This process will be more useful when we implement the Yubikey provider.
Disabling two-factor authentication involves providing your token to verify yourself.
With that, everything was implemented to allow two-factor authentication. The user can enable or disable two-factor authentication and the user can login with two-factor authentication. Yay!
Implementing Yubikey
Having implemented a simple token provider, it’s time to implement a provider that’s more practical: Yubikey!
Again, I wanted to implement as much of this as possible for learning experience. As a result, I didn’t use the officially created .Net client and implemented my own client for validating Yubikey OTPs. Another reason for implementing my own client was also that the official client targets .NET Framework and .NET Standard, so it doesn’t follow the conventions of ASP.NET Core.
Getting a Yubicloud Api Key
First, I needed to be able to use the Yubicloud to validate the tokens. In order to do that, I needed to signup for a Yubicloud API key. I was a bit surprised that it took me so long to find how to get a Yubicloud key. I would have figured they would make it obvious on the first page, but it took me a bit to find the registration page. I finally found it tucked away in some documentation: https://upgrade.yubico.com/getapikey/ . Providing an email address and an OTP got me a ClientId and a ClientSecret to begin using the service.
Creating the Client
The client is implemented in the YubikeyClient
folder. It doesn’t support the timeout or synclevel parameters, but it does support returning the timestamp information.
The protocol is pretty well documented on their OTP validation protocol page. That being said, there were a few things that tripped me up while implementing the client.
First, the nonce field in the request needs to be alphanumeric. This isn’t explicitly stated on the protocol page, but if you have other characters, the request will fail with MISSING_PARAMETER status. This caught me up because I originally tried to base64 encode random bytes for the nonce and that would randomly fail if you got some of the special characters.
Second, the character encoding with calculating the HmacSHA1 is not explicitly stated. I had to look up it up in the official client and it’s ASCII . I suppose UTF-8 would also work because this protocol doesn’t seem to use values that would encode to multiple bytes.
Next, the URL encoding done when calculating the hash is a bit odd. When you calculate the hash for a request, you construct the key-value pairs with URL encoding. When you calculate the hash for a response, you construct the key-value pairs without URL encoding. I found this to be strange, so I double checked with the official implementation and it URI escapes the data for the request hash and doesn’t for the data in the response hash . Again, this URI encoding doesn’t seem to be explicitly stated in the protocol and the difference in calculating the hashes seems odd to me.
Finally, there’s an empty line at the end of the response. This caused a small hiccup when initially parsing the response, but it caught me a bit off guard again.
The implementation is done in the YubicoService
. This uses the HttpClientFactory
pattern and the Options pattern. The ClientId and ClientSecret are stored in the user secrets to avoid them being included in source control. Aside from the logic of validating the token, the main method is just an HTTP request and response.
Creating the Yubikey Provider
To implement the Yubikey Provider, the ApplicationUser class needed another field to store the public Yubikey identifier and bind a specific key to the account. Implementing the provider was quite similar to the Constant provider and is found in the YubikeyTwoFactorTokenProvider
class. Its only dependency is on the YubicoService implemented above.
The CanGenerateTwoFactorTokenAsync checks that the two-factor provider the user has selected is this provider. Perhaps this should instead check that the YubikeyId field is not null or empty to better match the ASP.NET model. Again, this method is irrelevant to this implementation.
The GenerateAsync is the same as the Constant provider. Since the Yubikey creates the key, this just returns an empty string.
The ValidateAsync method is the main logic. First, the token is checked that it starts with the YubikeyId stored for the user. If it doesn’t, the user is using a different Yubikey than the one they used while enabling two-factor authentication and is rejected. After that, the YubicoService validates the token and ensure the response is “OK”.
That’s all there is in implementing the Yubikey provider, although it looks a bit easy because almost all the logic was implemented in the YubicoService.
Managing a Yubikey
To manage the yubikey, there are is a new page created to enable the Yubikey. Similar to the constant provider, the user must provide a valid Yubikey OTP when enabling it. While enabling the constant provider, the provider itself to validate the token, but that can’t be done with the Yubikey provider. The Yubikey provider uses the YubikeyId on the ApplicationUser as part of the validation. Since the user doesn’t have a YubikeyId yet, the provider can’t be used. Instead, the YubicoService is consumed directly by the controller, checks that the result is “OK”, and then updates the ApplicationUser with the YubicoId from the token the submitted.
There is nothing special to add to the disable two-factor authentication since that is handled the same way for all tokens.
Conclusion
It’s certainly not polished or production ready, but it was fun to write and learn about ASP.NET Core Identity and how to implement a two-factor authentication provider.
As I said at the start, I am interested in continuing this into an OpenId Connect Identity Provider using the IdentityServer library. I might create that as a new project instead of continuing with this particular project, but I’m not certain.
Another potential project to come out of this would be spinning the YubicoClient into it’s own library. I think there would be one project that could implement the service and another implementation that would implement helper methods that follow the conventions of ASP.NET Core, so it could be easily used in a variety of different applications.
Thanks for reading!