External Authentication Providers — Google, Microsoft, Azure AD
Integrate external OAuth providers (Google, Microsoft, Azure AD) with ASP.NET Core Identity: provider setup, claim mapping, linking external logins to local accounts, and multi-tenant Azure AD.
External Login Architecture
External login flow:
1. User clicks "Login with Microsoft"
2. Identity provider authenticates the user and returns an external identity
3. Your app receives: external provider name + external user ID + claims
4. Your app maps the external identity to a local AppUser account
5. Local account issues your JWT — external provider is not involved after this
Result: your app controls the session, but identity verification is delegatedThis separation matters: if the external provider goes down, you can fall back to email/password. Your users' data stays in your system.
Adding Google Authentication
// NuGet: Microsoft.AspNetCore.Authentication.Google
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
// Request additional scopes
options.Scope.Add("email");
options.Scope.Add("profile");
// Map Google claims to standard claim types
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("picture", "picture");
options.CallbackPath = "/auth/google/callback";
});Adding Microsoft / Azure AD (Single Tenant)
// NuGet: Microsoft.Identity.Web
builder.Services.AddAuthentication()
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));// appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"CallbackPath": "/auth/microsoft/callback"
}
}Multi-Tenant Azure AD
// appsettings.json — multi-tenant
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "common", // "common" = any Azure AD tenant
"ClientId": "your-client-id"
}
}
// Validate that only your allowed tenants can log in
builder.Services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidateIssuer = false; // disable built-in issuer check
options.Events.OnTokenValidated = ctx =>
{
var tenantId = ctx.Principal?.FindFirstValue("tid");
var allowed = ctx.HttpContext.RequestServices
.GetRequiredService<IOptions<AllowedTenants>>().Value.Ids;
if (tenantId is null || !allowed.Contains(tenantId))
ctx.Fail("Tenant not authorized.");
return Task.CompletedTask;
};
});Production issue I've seen: A hospital SaaS app used
TenantId: "common"for multi-tenant login but forgot to validate the tenant on token validation. Any user with a valid Microsoft account — from any organization — could log in. Adding the allowed-tenant list check was a 10-line fix, but the gap was open for 3 months.
Linking External Login to Local Account
// Application/Auth/ExternalLoginHandler.cs
public sealed class ExternalLoginHandler
{
private readonly UserManager<AppUser> _users;
private readonly SignInManager<AppUser> _signIn;
private readonly TokenService _tokens;
public async Task<Result<LoginResponse>> Handle(
ExternalLoginCommand cmd, CancellationToken ct)
{
var info = await _signIn.GetExternalLoginInfoAsync();
if (info is null)
return Result.Failure<LoginResponse>(AuthErrors.ExternalLoginFailed);
// Try to sign in with existing link
var signInResult = await _signIn.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: false);
AppUser user;
if (signInResult.Succeeded)
{
user = await _users.FindByLoginAsync(info.LoginProvider, info.ProviderKey)
?? throw new InvalidOperationException("User not found after successful login.");
}
else
{
// First time — create or link account
var email = info.Principal.FindFirstValue(ClaimTypes.Email)
?? throw new InvalidOperationException("No email from provider.");
user = await _users.FindByEmailAsync(email) ?? new AppUser
{
UserName = email,
Email = email,
FirstName = info.Principal.FindFirstValue(ClaimTypes.GivenName) ?? "",
LastName = info.Principal.FindFirstValue(ClaimTypes.Surname) ?? "",
IsActive = true,
CreatedAt = DateTime.UtcNow,
};
if (user.Id == Guid.Empty)
await _users.CreateAsync(user);
await _users.AddLoginAsync(user, info); // link external login to local account
}
var accessToken = await _tokens.GenerateAccessTokenAsync(user);
var refreshToken = await _tokens.CreateRefreshTokenAsync(user.Id, cmd.IpAddress);
return Result.Success(new LoginResponse(accessToken, refreshToken.Token));
}
}External Login Endpoints
// Initiate external login
app.MapGet("/auth/login/{provider}", async (
string provider,
string returnUrl,
SignInManager<AppUser> signIn) =>
{
var redirectUrl = $"/auth/callback/{provider}";
var properties = signIn.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Results.Challenge(properties, [provider]);
});
// Handle callback
app.MapGet("/auth/callback/{provider}", async (
ExternalLoginHandler handler,
HttpContext ctx,
CancellationToken ct) =>
{
var result = await handler.Handle(new ExternalLoginCommand(ctx.Connection.RemoteIpAddress?.ToString() ?? ""), ct);
return result.IsSuccess
? Results.Ok(result.Value)
: Results.Unauthorized();
});Claim Mapping from External Providers
Google claim names: ASP.NET Core standard:
email → ClaimTypes.Email
given_name → ClaimTypes.GivenName
family_name → ClaimTypes.Surname
sub → ClaimTypes.NameIdentifier
picture → "picture" (custom)
Azure AD claim names: ASP.NET Core standard:
preferred_username → ClaimTypes.Email
given_name → ClaimTypes.GivenName
family_name → ClaimTypes.Surname
oid → ClaimTypes.NameIdentifier (preferred over sub)
tid → "tid" (tenant ID — validate this!)
roles → ClaimTypes.RoleRed Flag / Green Answer
Red Flag: "When a user logs in with Google, we create a new account every time — we don't link it to existing email/password accounts."
Users who originally registered with email/password cannot use Google login for the same account. Duplicate accounts appear in the database. Use
FindByEmailAsyncto check for an existing account before creating a new one.
Green Answer:
Check by email first. If account exists, add the external login link (
AddLoginAsync). If not, create the account and link. One user, multiple login methods.
Key Takeaway
External providers delegate identity verification but your app controls the session. The pattern: external provider authenticates the user, returns claims, your app maps those claims to a local AppUser, and issues your own JWT. One local account can have multiple external logins linked. Always validate tenant ID in multi-tenant Azure AD scenarios — any valid Microsoft account can authenticate to "common" without explicit tenant checks.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.