Simple claims transformation and caching w/ windows authentication

John picture John · Jun 3, 2013 · Viewed 8.2k times · Source

For the past few days I've been reading about the windows identity foundation and how it's so good and flexible and built right into .net 4.5. Despite going over dozens of apis, blog posts, how-to's etc. I can't for the life of me get a simple implementation working.

I'm using windows authentication only and I can get the principal and view the claims that come with it (which is where every example seems to end). However I want to then transform them into useful claims and cache the results so that the transformation doesn't happen on every single request.

In my web.config I have:

  <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" />
  </configSections>

  <system.identityModel>
    <identityConfiguration>
      <claimsAuthenticationManager type="SecurityProj.MyClaimsTransformationModule,SecurityProj" />
      <claimsAuthorizationManager type="SecurityProj.MyClaimsAuthorizationManager,SecurityProj" />
    </identityConfiguration>
  </system.identityModel>

However the authentication manager never gets called. The only way I can get it to sort of work is by adding:

protected void Application_PostAuthenticateRequest()
{
    ClaimsPrincipal currentPrincipal = ClaimsPrincipal.Current;
    ClaimsTransformationModule customClaimsTransformer = new MyClaimsTransformationModule();
    ClaimsPrincipal tranformedClaimsPrincipal = customClaimsTransformer.Authenticate(string.Empty, currentPrincipal);
    HttpContext.Current.User = tranformedClaimsPrincipal;
}

To my global.asax.cs file. It works on the first request but then I get "Safe handle has been closed" errors after that and have no idea what is causing it. Clearly this isn't the correct way to do it, so does anyone know what a best or simply working practice is? This is just for windows authentication, I don't need anything more complicated than that.

For the caching, I was trying to use:

        SessionSecurityToken token = FederatedAuthentication.SessionAuthenticationModule
            .CreateSessionSecurityToken(
            currentPrincipal,
            "Security test",
            System.DateTime.UtcNow,
            System.DateTime.UtcNow.AddHours(1),
            true);

        if (FederatedAuthentication.SessionAuthenticationModule != null &&
            FederatedAuthentication.SessionAuthenticationModule.ContainsSessionTokenCookie(HttpContext.Current.Request.Cookies))
        {
            return;
        }
        FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(token);

but I'm not sure about that part either and the transformation problems need to be fixed first.

Any help would be appreciated. Just need the lookup/transform to be called and a cookie set, thanks.

Answer

John picture John · Jun 6, 2013

I've got everything working now, here's how I went about it:

On this page: http://msdn.microsoft.com/en-us/library/ee517293.aspx Was a key paragraph:

If you want to make your RP application claims-aware, but you do not have an STS (for example, the RP uses Forms authentication or Windows integrated authentication), you can use the ClaimsPrincipalHttpModule. This module sits in your application’s HTTP pipeline and intercepts authentication information. It generates a IClaimsPrincipal for each user based on that user’s username, group memberships, and other authentication information. ClaimsPrincipalHttpModule must be inserted at the end of the <httpModules> pipeline, which is the first element in the <modules> section of <system.webServer> on IIS 7.

And this page:

http://leastprivilege.com/2012/04/04/identity-in-net-4-5part-2-claims-transformation-in-asp-net-beta-1/

Gives you the whole class:

public class ClaimsTransformationHttpModule : IHttpModule
{
    public void Dispose()
    { }

    public void Init(HttpApplication context)
    {
        context.PostAuthenticateRequest += Context_PostAuthenticateRequest;
    }

    void Context_PostAuthenticateRequest(object sender, EventArgs e)
    {
        var context = ((HttpApplication)sender).Context;

        // no need to call transformation if session already exists
        if (FederatedAuthentication.SessionAuthenticationModule != null &&
            FederatedAuthentication.SessionAuthenticationModule.ContainsSessionTokenCookie(context.Request.Cookies))
        {
            return;
        }

        var transformer =
        FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
        if (transformer != null)
        {
            var transformedPrincipal = transformer.Authenticate(context.Request.RawUrl, context.User as ClaimsPrincipal);

            context.User = transformedPrincipal;
            Thread.CurrentPrincipal = transformedPrincipal;
        }
    }
}

Now add that class to the web.config:

<modules>
  <add name="ClaimsTransformationHttpModule" type="TestSecurity.ClaimsTransformationHttpModule" />
</modules>

Now it will call the transformation and I can remove the post authenticate method in global.asax.

In the authenticate method, I call this to set the cookie:

private void CreateSession(ClaimsPrincipal transformedPrincipal)
{
    SessionSecurityToken sessionSecurityToken = new SessionSecurityToken(transformedPrincipal, TimeSpan.FromHours(8));
    FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionSecurityToken);
}

The module from before is already set up to look at it and skip authentication if it's present.

Lastly for the safe handle error that I kept getting. I'm not exactly sure of the cause, but I discovered that if I modified the principal that gets passed to Authenticate and then returned it (which is what it shows on msdn), then the error would show up on all subsequent requests. However if I created and returned a new principal then it would not occur. This also turns out to be useful for dropping claims that you don't need.

List<Claim> newClaims = new List<Claim>();

var keeper = ((ClaimsIdentity)incomingPrincipal.Identity).Claims.First(c =>
    c.Type == ClaimTypes.Name);
newClaims.Add(keeper);

ClaimsIdentity ci = new ClaimsIdentity(newClaims, "Negotiate");

return new ClaimsPrincipal(ci);

So now I can windows authenticate, bring in custom claims, and have them cached with a cookie. Hope this helps anyone else trying to do the same and if I'm not doing something right please let me know.