ASP.NET Core 3.1 - 2FA Recovery Codes
This article will describe the implementation of 2FA recovery codes. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.
Users Without Identity Project and Article Series
Creation Series
- ASP.NET Core 3.1 - Users Without Identity
- ASP.NET Core 3.1 - User Entity
- ASP.NET Core 3.1 - Password Hasher
- ASP.NET Core 3.1 - User Management
- ASP.NET Core 3.1 - Admin Role
- ASP.NET Core 3.1 - Cookie Validator
- ASP.NET Core 3.1 - Concurrency Conflicts
- ASP.NET Core 3.1 - Must Change Password
- ASP.NET Core 3.1 - User Database Service
- ASP.NET Core 3.1 - Rename Related Entities
2FA Series
- ASP.NET Core 3.1 - 2FA Without Identity
- ASP.NET Core 3.1 - 2FA User Tokens
- ASP.NET Core 3.1 - 2FA Cookie Schemes
- ASP.NET Core 3.1 - 2FA Authenticating
- ASP.NET Core 3.1 - 2FA Sign In Service
- ASP.NET Core 3.1 - 2FA QR Code Generator
- ASP.NET Core 3.1 - Admin 2FA Requirement
- ASP.NET Core 3.1 - 2FA Admin Override
Enhanced User Series
- ASP.NET Core 3.1 - Enhanced User Without Identity
- ASP.NET Core 3.1 - 2FA Recovery Codes
- ASP.NET Core 3.1 - Login Lockout
- ASP.NET Core 3.1 - Created And Last Login Date
- ASP.NET Core 3.1 - Security Stamp
- ASP.NET Core 3.1 - Token Service
- ASP.NET Core 3.1 - Confirmed Email Address
- ASP.NET Core 3.1 - Password Recovery
The 2FA Series describes the implementation of multiple cookie schemes, authenticator key generation and verification, displaying the authenticator key in a QR Code formatted image, and a SignInService to manage the multi-factor authentication. The 2FA series added a related AppUserToken model like ASP.NET Core Identity's UserToken. Identity implements 2FA RecoveryCodes and stores the user's AuthenticatorKey and the RecoveryCodes in the UserToken model. UWIP v2 implements 2FA RecoveryCodes but the AppUser model implements the AuthenticatorKey and RecoveryCodes properties. The AppUserToken model has been removed.
Entities > AppUser:
[StringLength(32)] [Display(Name = "Authenticator Key")] public string AuthenticatorKey { get; set; } [StringLength(128)] [Display(Name = "Recovery Codes")] public string RecoveryCodes { get; set; }
The first time a user enables 2FA by configuring and verifying an Authenticator App, a set of recovery codes are generated. The UserService stores the merged codes in the AppUser's RecoveryCodes property. The codes are saved to a temp data property and the user is redirected to a ShowRecoveryCodes page which displays the codes from temp data. The codes are only displayed once immediately after generation.
Pages > Account > Manage > EnableAuthenticator > OnPostAsync:
var mergedCodes = appUser.RecoveryCodes ?? string.Empty; var splitCodes = mergedCodes.Split(';'); if (string.IsNullOrEmpty(mergedCodes) || splitCodes.Length == 0) { var newCodes = new List<string>(10); for (var i = 0; i < 10; i++) newCodes.Add(Guid.NewGuid().ToString().Substring(0, 8)); RecoveryCodes = newCodes.Distinct().ToArray(); mergedCodes = string.Join(";", RecoveryCodes); if (!await _userService.UpdateAppUserRecoveryCodesAsync(appUserId, mergedCodes).ConfigureAwait(false)) throw new InvalidOperationException($"Unable to replace AppUser RecoveryCodes."); return RedirectToPage("./ShowRecoveryCodes"); } else return RedirectToPage("./TwoFactorAuthentication");
Each recovery code can be used once to authorize the second factor of the user login when 2FA is enabled. The SignInService implements a TwoFactorRecoveryCodeSignInAsync function which validates the code and if valid removes the current code. The injected UserService is employed to update the AppUser's RecoveryCodes property. The authorization is not persistent, and the client is not remembered.
Services > SignInService > TwoFactorRecoveryCodeSignInAsync:
var mergedCodes = appUser.RecoveryCodes ?? string.Empty; var splitCodes = mergedCodes.Split(';'); if (splitCodes.Contains(recoveryCode)) { var updatedCodes = new List<string>(splitCodes.Where(s => s != recoveryCode)); mergedCodes = string.Join(";", updatedCodes); if (!await _userService.UpdateAppUserRecoveryCodesAsync(appUser.Id, mergedCodes).ConfigureAwait(false)) throw new InvalidOperationException($"Unable to replace AppUser RecoveryCodes."); await DoTwoFactorSignInAsync(appUser, isPersistent: false, rememberClient: false).ConfigureAwait(false); return SignInResult.Success; }
The Manage > TwoFactorAuthentication page checks the count of remaining codes and offers to generate new codes when the count is 3 or less.
Update 02/23/2021
I updated the article links.
Comments(0)