.NET Core Multiple Authentication–Windows AD, Azure AD, Database

I was having a project about how to do in .NET Core multiple authentication: Windows AD, Azure AD, Database – and integrate with Roles.

It was an interesting project – and I decide to not make everything – but to construct on an existing project.

The most promising sounds IdentityServer – with the Federation Gateway. : http://docs.identityserver.io/en/latest/topics/federation_gateway.html

So I decided to give a twist , and after many readings and searching , I have found https://github.com/damienbod/AspNetCoreWindowsAuth . It was pretty amazing  – however, some problems occurred.

 

External Integration – with local Active Directory

See in Startup.cs the following

    services.Configure<IISOptions>(iis =>
        {
            iis.AuthenticationDisplayName = "Windows";
            iis.AutomaticAuthentication = true;
        });

When the user clicks the Windows authenticatio the following code is called

public class AccountController : Controller
{
    //ommitted code
    [HttpGet]
    public async Task<IActionResult> ExternalLogin(string provider, string returnUrl)
    {
        if (AccountOptions.WindowsAuthenticationSchemeName == provider)
        {
            // windows authentication needs special handling
            return await ProcessWindowsLoginAsync(returnUrl);
        }
        //ommitted code
    }
    //ommitted code
    private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
        {
            // see if windows auth has already been requested and succeeded
            var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
            if (result?.Principal is WindowsPrincipal wp)
            {
                //ommitted code
            }
            else
            {
                // trigger windows auth
                // since windows auth don't support the redirect uri,
                // this URL is re-triggered when we call challenge
                return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
            }
        }
}

External Integration – with Azure Active Directory

This was by far the most complicated
As a pre-requisites , we need to configure the Azure Active Directory and grab the client Id for the application.

The code in the startup.cs os

        services
        .AddAuthentication(IISDefaults.AuthenticationScheme)
        .AddOpenIdConnect("aad", "Sign-in with Azure AD", options =>
        {
            //options.Authority = $"https://login.microsoftonline.com/common/v2.0/";
            //options.Authority = $"https://ignatandreiyahoo.onmicrosoft.com";
            //options.Authority = $"https://login.windows.net/{tenantId}";
            options.Authority = "https://login.microsoftonline.com/common/v2.0/";
            options.ClientId = $"{clientId}";
            //options.RequireHttpsMetadata = true;
            options.RemoteAuthenticationTimeout = new System.TimeSpan(0,1,58);

            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
            options.SignOutScheme = IdentityServerConstants.SignoutScheme;

            options.ResponseType = OpenIdConnectResponseType.IdToken; //"id_token";
            options.CallbackPath = "/signin-aad";
            options.SignedOutCallbackPath = "/signout-callback-aad";
            options.RemoteSignOutPath = "/signout-aad";

            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false,
                //ValidAudience = "f59d5739-1ec9-46fc-961d-b01ef6fb3c51",

                NameClaimType = "name",
                RoleClaimType = "role"
            };
            options.Events.OnRemoteFailure = (context) =>
            {
                string s = context.ToString();
                return Task.CompletedTask;
            };
        })

        ;
        services.AddOidcStateDataFormatterCache("aad");
   

And the code that retrieves the user is:

public class AccountController : Controller
{

    [HttpGet]
    public async Task<IActionResult> ExternalLoginCallback()
    {
        //ommitted code
        // read external identity from the temporary cookie
        var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
        if (result?.Succeeded != true)
        {
            result = await HttpContext.AuthenticateAsync("aad");
            if (result?.Succeeded != true)
            throw new Exception("External authentication error");
        }

        // lookup our user and external provider info
        var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
        //ommitted code
    }
    //ommitted code
    private async Task<(ApplicationUser user, string provider, string providerUserId, IEnumerable<Claim> claims)> 
        FindUserFromExternalProviderAsync(AuthenticateResult result)
    {
        var externalUser = result.Principal;

        // try to determine the unique id of the external user (issued by the provider)
        // the most common claim type for that are the sub claim and the NameIdentifier
        // depending on the external provider, some other claim type might be used
        var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
                          externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
                          throw new Exception("Unknown userid");

        // remove the user id claim so we don't include it as an extra claim if/when we provision the user
        var claims = externalUser.Claims.ToList();
        claims.Remove(userIdClaim);

        var provider = result.Properties.Items["scheme"];
        var providerUserId = userIdClaim.Value;

        // find external user
        var user = await _userManager.FindByLoginAsync(provider, providerUserId);

        return (user, provider, providerUserId, claims);
    }
    

Configure Azure Active Directory

This implies to go to portal.azure.com.

First you create a new application in the Azure Active Directory

Please retain the applicationId in order to put to the code

    options.ClientId = $"{clientId}";
    

Do not forget about checking the token on authentication

because of this code

    options.ResponseType = OpenIdConnectResponseType.IdToken; //"id_token";
//ommitted code
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        //ValidAudience = "f59d5739-1ec9-46fc-961d-b01ef6fb3c51",

        NameClaimType = "name",
        RoleClaimType = "role"
    };

Roles for Windows


In Computer Management, I define the user : testUser that belongs to MyGroup

In startup.cs I define the Policy for this:

services.AddAuthorization(options =>
            {
                options.AddPolicy("MyGroupPolicy", policy => policy.RequireClaim("role",@"MyGroup"));
            });
    

I also define an Controller that require this policy

public class TestController : Controller
{
	[Authorize(Policy="MyGroupPolicy")]
	public IActionResult Index()
	{
		return Content(" you can see this because you are authorized to see MyGroupPolicy");
	}
}
    

So now if you logon on Windows with testuser, it will require the policy to be satisfied

Now, to understand the code

Read

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-3.1#multiple-policy-evaluation

and

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-3.1


Now some more code:

Transforming Groups to role claims

string pcName = Environment.MachineName;
// add the groups as claims -- be careful if the number of groups is too large
if (AccountOptions.IncludeWindowsGroups)
{
	var wi = wp.Identity as WindowsIdentity;
	var groups = wi.Groups.Translate(typeof(NTAccount));
	
	var roles = groups
		.Select(it=>it.Value)
		.Select(it=> it.StartsWith(pcName +"\\",StringComparison.InvariantCultureIgnoreCase)?
					it.Substring(pcName.Length+1): it)                        
		.Select(x => new Claim(JwtClaimTypes.Role, x));
	id.AddClaims(roles);
}

And putting back to user

[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
//omitted code
	// lookup our user and external provider info
	var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
//omitted code
    var principal = await _signInManager.CreateUserPrincipalAsync(user);

	foreach (var claim in claims)
	{
		additionalLocalClaims.AddRange(claims);
	}
//ommitted code
	await HttpContext.SignInAsync(user.Id, name, provider, localSignInProps, additionalLocalClaims.ToArray());


You can see the code at https://github.com/ignatandrei/identintegra and more documentation at https://ignatandrei.github.io/IdentIntegra/