Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20264 min read
OAuth2External ProvidersAzure ADASP.NET Core.NET
Share:𝕏

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 delegated

This 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

C#
// 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)

C#
// NuGet: Microsoft.Identity.Web
builder.Services.AddAuthentication()
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
JSON
// 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

C#
// 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

C#
// 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

C#
// 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.Role

Red 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 FindByEmailAsync to 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.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.