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