A complete guide to integrating Google Authenticator using AuthenticatorAPI.com
This guide shows how to integrate Google Authenticator-compatible 2FA into C# applications — covering ASP.NET Core MVC, ASP.NET Web Forms, and standalone console/service applications. Uses only HttpClient from the BCL, with no NuGet packages required.
HttpClient with async/await.Encapsulate all calls to the API in a single service class. Register it with the DI container as a singleton (sharing a single HttpClient instance is the .NET best practice).
using System.Net.Http; using System.Security.Cryptography; using System.Web; public class TotpService { private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; private const string ApiBase = "https://www.authenticatorApi.com"; private readonly HttpClient _http; public TotpService(HttpClient http) => _http = http; /// <summary>Generates a cryptographically random 32-character Base32 secret.</summary> public string GenerateSecret(int length = 32) { var bytes = RandomNumberGenerator.GetBytes(length); return new string(bytes.Select(b => Base32Alphabet[b % 32]).ToArray()); } /// <summary>Returns the HTML img tag QR code for the setup page.</summary> public async Task<string> GetQrCodeHtmlAsync(string appName, string userInfo, string secret) { var url = $"{ApiBase}/pair.aspx" + $"?AppName={HttpUtility.UrlEncode(appName)}" + $"&AppInfo={HttpUtility.UrlEncode(userInfo)}" + $"&SecretCode={HttpUtility.UrlEncode(secret)}"; return await _http.GetStringAsync(url); } /// <summary>Returns true if the PIN is valid for the given secret.</summary> public async Task<bool> ValidatePinAsync(string pin, string secret) { if (!pin.All(char.IsDigit) || pin.Length != 6) return false; var url = $"{ApiBase}/Validate.aspx" + $"?Pin={pin}" + $"&SecretCode={HttpUtility.UrlEncode(secret)}"; var response = await _http.GetStringAsync(url); return string.Equals(response.Trim(), "True", StringComparison.OrdinalIgnoreCase); } }
// Program.cs builder.Services.AddHttpClient<TotpService>(client => { client.Timeout = TimeSpan.FromSeconds(10); }); // Or as a plain singleton if not using typed HttpClient: builder.Services.AddSingleton<TotpService>(sp => new TotpService(sp.GetRequiredService<IHttpClientFactory>().CreateClient()));
public class TwoFactorController : Controller { private readonly TotpService _totp; private readonly IUserRepository _users; public TwoFactorController(TotpService totp, IUserRepository users) { _totp = totp; _users = users; } [HttpGet] public async Task<IActionResult> Setup() { var secret = _totp.GenerateSecret(); HttpContext.Session.SetString("Pending2FASecret", secret); var qrHtml = await _totp.GetQrCodeHtmlAsync( "MyApp", User.Identity!.Name!, secret); ViewBag.QrHtml = qrHtml; ViewBag.Secret = secret; return View(); } [HttpPost] public async Task<IActionResult> ConfirmSetup(string pin) { var secret = HttpContext.Session.GetString("Pending2FASecret"); if (await _totp.ValidatePinAsync(pin, secret!)) { await _users.SaveTotpSecretAsync(User.Identity!.Name!, secret!); return RedirectToAction("Index", "Home"); } ModelState.AddModelError("", "Invalid code — please try again."); return View("Setup"); } [HttpPost] public async Task<IActionResult> Verify(string pin) { var secret = await _users.GetTotpSecretAsync(User.Identity!.Name!); if (await _totp.ValidatePinAsync(pin, secret)) { HttpContext.Session.SetString("2FAVerified", "true"); return RedirectToAction("Dashboard"); } ModelState.AddModelError("", "Invalid code — please try again."); return View(); } }
using System; using System.Net.Http; using System.Security.Cryptography; using System.Web; public static class TotpHelper { private static readonly HttpClient _http = new HttpClient(); private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; public static string GenerateSecret() { var rng = new RNGCryptoServiceProvider(); var bytes = new byte[32]; rng.GetBytes(bytes); var sb = new System.Text.StringBuilder(); foreach (var b in bytes) sb.Append(Base32Alphabet[b % 32]); return sb.ToString(); } public static string GetQrCodeHtml(string appName, string userInfo, string secret) { var url = $"https://www.authenticatorApi.com/pair.aspx" + $"?AppName={HttpUtility.UrlEncode(appName)}" + $"&AppInfo={HttpUtility.UrlEncode(userInfo)}" + $"&SecretCode={HttpUtility.UrlEncode(secret)}"; return _http.GetStringAsync(url).Result; } public static bool ValidatePin(string pin, string secret) { var url = $"https://www.authenticatorApi.com/Validate.aspx?Pin={pin}&SecretCode={HttpUtility.UrlEncode(secret)}"; var result = _http.GetStringAsync(url).Result.Trim(); return string.Equals(result, "True", StringComparison.OrdinalIgnoreCase); } }
// Setup.aspx.cs protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { var secret = TotpHelper.GenerateSecret(); Session["Pending2FASecret"] = secret; QrCodeLiteral.Text = TotpHelper.GetQrCodeHtml("MyApp", User.Identity.Name, secret); SecretLabel.Text = secret; } } protected void btnVerify_Click(object sender, EventArgs e) { var secret = Session["Pending2FASecret"] as string; if (TotpHelper.ValidatePin(PinTextBox.Text.Trim(), secret)) { // Save secret to user's profile and redirect SaveSecretToProfile(secret); Response.Redirect("~/Default.aspx"); } else { ErrorLabel.Text = "Invalid code — please try again."; ErrorLabel.Visible = true; } }
RandomNumberGenerator.GetBytes() (.NET 6+) or RNGCryptoServiceProvider (.NET Framework)IHttpClientFactory pattern — never instantiate HttpClient directly in a loopIDataProtector (ASP.NET Core) or AES-256AspNetCoreRateLimit NuGet)