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.
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.
This answer is intended to provide further clarification on John's answer above after experiencing several frustrating days trying to solve some similar problems.
1. ClaimsPrincipalHttpModule
As John discovered, if you are using Windows Auth or Forms Auth, ASP.NET will not automatically invoke your ClaimsAuthenticationManager (it is not a Federated scenario). You must do so yourself after ASP.NET has authenticated the user. Using the ClaimsPrincipalHttpModule, which used to be part of IdentityModel, will effectively ensure this occurs.
However, exercise caution using this module. It was removed for a reason. My theory as to why it was removed from IdentityModel is because it does not play nicely if you host WCF services in your ASP.NET app and have aspNetCompatibilityEnabled=true. It will cause your WCF authentication to break (the module will execute BEFORE the WCF pipeline and my experience is that your WCF clients will no longer be able to properly authenticate - I've confirmed this when using Windows Auth).
If you are hosting WCF services in this scenario you must somehow ensure that the ClaimsAuthenticationManager is only invoked for non-WCF requests. For WCF requests, it seems you have to rely on the WCF pipeline to do so (<serviceCredentials useIdentityConfiguration="true" />
). The easiest fix is to simply turn off aspNetCompatibilityEnabled. If this is not an option, you should not use ClaimsPrincipalHttpModule but must somehow examine the incoming request and only invoke the ClaimsAuthenticationManager if the request is not destined for WCF.
2. Safe handle error (ObjectDisposedException)
This occurs if you create a SessionSecurityToken that is based on a WindowsIdentity. The SessionAuthenticationModule has special logic to handle WindowsIdentity claims read from a SessionSecurityToken and will attempt to rehydrate the WindowsIdentity using data that is no longer valid. (I'm not sure of the circumstances in which it would work but it consistently failed in all the scenarios I tested). Thus, as John explained, the lesson here is that when attempting to use WIF with Windows auth, SessionSecurityTokens should not be created from a WindowsPrincipal (or more correctly a WindowsIdentity). Any other type of transformed ClaimsPrincipal should be fine.