External login (OAuth/OpenID Connect) lets users sign in with their existing Google, Microsoft, or GitHub account โ no new password to create or remember. Two-factor authentication (2FA) adds a second verification step beyond the password, dramatically reducing account takeover risk from stolen credentials. Both are production-grade features that significantly improve your application’s security posture and user convenience. ASP.NET Core Identity has built-in support for both with minimal configuration.
Google OAuth Login
// dotnet add package Microsoft.AspNetCore.Authentication.Google
// โโ Program.cs โ configure OAuth providers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
builder.Services
.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]!;
options.Scope.Add("profile");
// Map Google profile picture to a claim
options.ClaimActions.MapJsonKey("picture", "picture");
})
.AddMicrosoftAccount(options =>
{
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"]!;
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"]!;
});
// โโ AccountController โ external login flow โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Step 1: Redirect to external provider
[HttpPost, ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string? returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback),
new { returnUrl });
var properties = signInManager.ConfigureExternalAuthenticationProperties(
provider, redirectUrl);
return Challenge(properties, provider);
}
// Step 2: Handle callback after external provider redirects back
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null)
{
var info = await signInManager.GetExternalLoginInfoAsync();
if (info is null) return RedirectToAction(nameof(Login));
// Sign in if this external login is already linked
var result = await signInManager.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
if (Url.IsLocalUrl(returnUrl)) return Redirect(returnUrl!);
return RedirectToAction("Index", "Home");
}
// First time โ create a new local account linked to the external login
var email = info.Principal.FindFirstValue(ClaimTypes.Email)!;
var user = new ApplicationUser { UserName = email, Email = email };
var createResult = await userManager.CreateAsync(user);
if (createResult.Succeeded)
{
await userManager.AddLoginAsync(user, info);
await signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
foreach (var err in createResult.Errors)
ModelState.AddModelError("", err.Description);
return View("ExternalLoginFailure");
}
AspNetUserLogins table. When a user returns and authenticates with Google, signInManager.ExternalLoginSignInAsync() looks up the provider key in this table to find the linked local account. One local user account can have multiple external logins (Google, Microsoft, GitHub) linked to it. This allows a user to sign in with any of their linked providers and always land on the same account.dotnet user-secrets set "Authentication:Google:ClientId" "your-client-id" to store OAuth credentials during development. Never commit OAuth credentials to source control โ they grant access to your OAuth application and can be used to impersonate your application or access API quotas. For production, use environment variables or Azure Key Vault. Register your OAuth application at console.cloud.google.com (Google) and portal.azure.com (Microsoft) and add https://localhost:PORT/signin-google as an authorised redirect URI during development.Two-Factor Authentication (2FA)
// โโ Enable 2FA โ generate authenticator key for the user โโโโโโโโโโโโโโโโโโ
[HttpGet, Authorize]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await userManager.GetUserAsync(User);
// Get or generate the shared key for TOTP (Google Authenticator, Authy)
var unformattedKey = await userManager.GetAuthenticatorKeyAsync(user!);
if (string.IsNullOrEmpty(unformattedKey))
{
await userManager.ResetAuthenticatorKeyAsync(user!);
unformattedKey = await userManager.GetAuthenticatorKeyAsync(user!);
}
// Format key as groups of 4 for readability: AAAA-BBBB-CCCC-DDDD
var sharedKey = FormatKey(unformattedKey!);
// Generate the QR code URI for authenticator apps
var email = await userManager.GetEmailAsync(user!);
var qrUri = GenerateQrCodeUri(email!, unformattedKey!);
return View(new EnableAuthenticatorViewModel { SharedKey = sharedKey, QrCodeUri = qrUri });
}
// โโ Verify the TOTP code and enable 2FA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[HttpPost, Authorize, ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid) return View(model);
var user = await userManager.GetUserAsync(User);
var code = model.Code.Replace(" ", "").Replace("-", ""); // normalise
var isValid = await userManager.VerifyTwoFactorTokenAsync(
user!, userManager.Options.Tokens.AuthenticatorTokenProvider, code);
if (!isValid)
{
ModelState.AddModelError("Code", "Verification code is invalid.");
return View(model);
}
await userManager.SetTwoFactorEnabledAsync(user!, true);
return RedirectToAction(nameof(TwoFactorAuthenticationConfirmation));
}
// โโ Login with 2FA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model)
{
if (!ModelState.IsValid) return View(model);
var code = model.TwoFactorCode.Replace(" ", "").Replace("-", "");
var result = await signInManager.TwoFactorAuthenticatorSignInAsync(
code, model.RememberMe, model.RememberMachine);
if (result.Succeeded)
return RedirectToAction("Index", "Home");
ModelState.AddModelError("", "Invalid authenticator code.");
return View(model);
}
Common Mistakes
Mistake 1 โ Not verifying external provider email matches existing account (account takeover)
โ Wrong โ attacker links their Google account to any local account by knowing the email.
โ Correct โ verify the external provider email claim matches the existing account’s email before linking.
Mistake 2 โ Committing OAuth ClientId/ClientSecret to source control
โ Wrong โ credentials in appsettings.json or code; attackers can impersonate the application.
โ Correct โ use User Secrets for development, environment variables or Key Vault for production.