Reputation: 451
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 -
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.
Upvotes: 0
Views: 2718
Reputation: 451
I got it fixed - this SO post gave a clue. Instead of customizing the existing Windows principal claimsIdentity, creating a new identity and adding custom claims to this identity helped getting rid of 'safe handle has been closed' error.
In the CustomClaimsTransformer class - CustomClaimsTransformer.CustomizePrincipal
/* commented this earlier code of adding claims to userClaimsIdentity
userClaimsIdentity.AddClaims(new[] {
new Claim("CustGroup", "CustTeam"),
new Claim(ClaimTypes.Email, princi.EmailAddress),
... ///more claims added here
});
return new ClaimsPrincipal(userClaimsIdentity);
*/
and replaced with code of creating new ClaimsIdentity like below
List<Claim> newClaims = new List<Claim>();
newClaims.Add(new Claim("CustGroup ", "CustTeam"));
newClaims.Add(new Claim(ClaimTypes.Email, princi.EmailAddress));
... ///more claims added here
ClaimsIdentity ci = new ClaimsIdentity(newClaims, "CustomClaims");
return new ClaimsPrincipal(ci); //userClaimsIdentity);
and now its working good, the user authentication/identity/claims are getting saved in the SessionTokenCookie
Upvotes: 1