For an MVC5 based intranet app, I am trying to implement Windows-based authentication with a custom Claims on top of claims-aware Windows identity of the principal.
Everything is going great until when I try to read the identity from SessionSecurityToken earlier stored in a session cookie, which for some reason is giving me 'safe handle has been closed' error when I try to go to any other view after landing on my Home/index view.
Below is what I got so far -
- In project properties - Windows Auth = Enabled, Anonymous Auth = disabled.
- being MVC5 project on .net 4.5, the project by default was designed to use OWIN (yes VS does this if you haven't selected Windows Authentication at the time of project creation), I feel OWIN doesn't gel with Windows Auth, so I disabled OWIN right at the start by commenting call to 'ConfigAuth' in startup.cs
Wrote a class to customize the windows principal identity and write sessionSecurityToken to cookie using FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie
public class CustomClaimsTransformer : ClaimsAuthenticationManager { public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) { if (!incomingPrincipal.Identity.IsAuthenticated) { return base.Authenticate(resourceName, incomingPrincipal); } ClaimsPrincipal transformedPrincipal = CustomizePrincipal(ClaimsPrincipal.Current.Identities.First(), incomingPrincipal.Identity.Name); CreateSession(transformedPrincipal); return transformedPrincipal; } private void CreateSession(ClaimsPrincipal transformedPrincipal) { SessionSecurityToken sessionSecurityToken = new SessionSecurityToken(transformedPrincipal, TimeSpan.FromHours(12)); FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionSecurityToken); } private ClaimsPrincipal CustomizePrincipal(ClaimsIdentity userClaimsIdentity, String userName) { List<Claim> claims = new List<Claim>(); PrincipalContext princiContxt = null; ; UserPrincipal princi = null; ClaimsIdentity custClaimsIdentity = new ClaimsIdentity(); princiContxt = new PrincipalContext(ContextType.Domain); princi = UserPrincipal.FindByIdentity(princiContxt, userName);//); userClaimsIdentity.AddClaims(new[] { new Claim("CustGroup", "CustTeam"), new Claim(ClaimTypes.Email, princi.EmailAddress), ... ///more claims added here }); return new ClaimsPrincipal(userClaimsIdentity); } }
In Web.config, I added below things:
Under 'configSections':
<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
And in 'system.identityModel':
<system.identityModel>
<identityConfiguration>
<claimsAuthenticationManager type="myProject.CustomClaimsTransformer,myProject"/>
</identityConfiguration>
</system.identityModel>
Also added below module in system.webserver:
<modules>
<add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"></add>
</modules>
Next, I have custom Authorize filter attribute to use the above defined CustomClaimsTransformer class to authorize using custom claims:
public class PROJClaimsAuthorizeAttribute : AuthorizeAttribute { public string ClaimType { get; set; } public string ClaimValue { get; set; } //Called when access is denied protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext) { //User isn't logged in if (!filterContext.HttpContext.User.Identity.IsAuthenticated) { filterContext.Result = new RedirectToRouteResult( new RouteValueDictionary(new { controller = "Home", action = "Index" }) ); } //User is logged in but has no access else { filterContext.Result = new RedirectToRouteResult( new RouteValueDictionary(new { controller = "Error", action = "AccessDenied" }) ); } } //Core authentication, called before each action protected override bool AuthorizeCore(HttpContextBase context) { SessionSecurityToken token; ClaimsIdentity claimsIdentity = null; ClaimsIdentity userClaimsIdentity = null; ClaimsPrincipal customClaimsPrinci = null; ClaimsAuthenticationManager authManager = null; var isAuthorized = false; try { claimsIdentity = context.User.Identity as ClaimsIdentity; // get current user's ClaimsIdentity (Widnow's identity as ClaimsIdentity) isAuthorized = base.AuthorizeCore(context); if (!context.Request.IsAuthenticated || !isAuthorized) { return false; } ///////******* THE Error Causing IF statement ******************************************* //check if the SessionSecurityToken is available in cookie if (FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out token)) { //var accessToken = await tokenManager.GetTokenFromStoreAsync(token.ClaimsPrincipal.Identity.Name); claimsIdentity = token.ClaimsPrincipal.Identity as ClaimsIdentity; } else { //else get the principal with Custom claims identity using CustomClaimsTransformer, which also sets it in cookie ClaimsPrincipal currentPrincipal = ClaimsPrincipal.Current; CustomClaimsTransformer customClaimsTransformer = new CustomClaimsTransformer(); ClaimsPrincipal tranformedClaimsPrincipal = customClaimsTransformer.Authenticate(string.Empty, currentPrincipal); Thread.CurrentPrincipal = tranformedClaimsPrincipal; HttpContext.Current.User = tranformedClaimsPrincipal; } isAuthorized = checkClaimValidity(claimsIdentity, ClaimType, ClaimValue); } catch (Exception e) { // Error handling code var exptnMsg = "error setting AuthorizeCore" + e.Message; return false; } return isAuthorized; } // </ protected override bool AuthorizeCore > //checks Claim type/value in the given Claims Identity private Boolean checkClaimValidity(ClaimsIdentity pClaimsIdentity, string pClaimType, string pClaimValue) { Boolean blnClaimsValiditiy = false; //now check the passed in Claimtype has the passed in Claimvalue if (pClaimType != null && pClaimValue != null) { if ((pClaimsIdentity.HasClaim(x => x.Type.ToLower() == pClaimType.ToLower() && x.Value.ToLower() == pClaimValue.ToLower()))) { blnClaimsValiditiy = true; } } return blnClaimsValiditiy; } }
After this I can decorate my controller class with my custom Authorize attribute 'PROJClaimsAuthorizeAttribute' like below:
[PROJClaimsAuthorizeAttribute(ClaimType = "CustGroup", ClaimValue = "CustTeam")]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
This is working all fine. The problem is - when from index view, if I try to navigate to some other view - I get 'safe handle has been closed' error. (When I remove the 'if' part of the if statement marked with 'THE Error Causing IF statement****** above, and just keep whatever is in else part , then it works good, but then I am not making use of the sessionSecurityToken cookie).
I have been scratching my head to figure out this error for past couple of days, searched google/SO etc , but no luck so far. So finally thought of throwing this to SO expert community here, will really appreciate if someone shed some light on what/where the issue might be. Sincere thanks in advance for help.