.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/