tiwarib
tiwarib

Reputation: 451

'safe handle has been closed' error when using FederatedAuthentication.SessionAuthenticationModule

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 -

  1. In project properties - Windows Auth = Enabled, Anonymous Auth = disabled.
  2. 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
  3. 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); 
       }
    }
    
  4. 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>
  1. 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;
     }
    }
    
  2. 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

Answers (1)

tiwarib
tiwarib

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

Related Questions