.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

1
2
3
4
5
services.Configure<IISOptions>(iis =>
    {
        iis.AuthenticationDisplayName = "Windows";
        iis.AutomaticAuthentication = true;
    });

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

1
options.ClientId = $"{clientId}";

Do not forget about checking the token on authentication

because of this code

01
02
03
04
05
06
07
08
09
10
    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:

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

I also define an Controller that require this policy

1
2
3
4
5
6
7
8
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
[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/