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/