The whole .NET authentication/authorization infrastructure is centered around two interfaces: IIdentity (taking care of who the user is – authentication) and IPrincipal (coupling roles with the Identity – used for authorization). The designated place to store that information is Thread.CurrentPrincipal.
Afterwards you do role checks by calling Thread.CurrentPrincipal.IsInRole – or use PrincipalPermission.Demand or [PrincipalPermissionAttribute] (which does nothing else than calling IsInRole on Thread.CurrentPrincipal under the covers). So far so good.
ASP.NET encourages you to use the “convenience” Context.User / Page.User which also point to an IPrincipal implementation and call IsInRole from there. But because PrincipalPermission relies on Thread.CurrentPrincipal and you maybe want to port a library used in desktop applications (where the Thread.CurrentPrincipal style is used) – ASP.NET has to populate Thread.CurrentPrincipal too – and synchronize it with Context.User.
When you want to plug into Forms Authentication in ASP.NET 1.1, you typically handled the AuthenticateRequest event in the HTTP pipeline. There you fetched the roles for the authenticated users, created an IPrincipal implementation and set Context.User to this new principal. Directly after AuthenticateRequest a (mostly) undocumented event fires called “DefaultAuthentication” and a module called DefaultAuthenticationModule subscribes to this event. This module has only one job, to copy Context.User to Thread.CurrentPrincipal so both values are in-sync.
In ASP.NET 2.0 there is the new PostAuthenticateRequest event and I like to think about the relationship between these two events in this way: AuthenticateRequest is used to create the IIdentity (identity information) and PostAuthenticateRequest is used to couple roles to that identity (authorization). The RoleManagerModule which has the job to couple roles from the provider backed store to the authenticated identity subscribes to exactly this event.
There is one gotcha though – ASP.NET does not take care of syncing Context.User and Thread.CurrentPrincipal after PostAuthenticateRequest – which is a bug IMO. So if you follow the pattern you know from 1.1 and set Context.User in PostAuthenticateRequest, Context.User and Thread.CurrentPrincipal will have different values. You may write really complex ASP.NET application whithout ever noticing that, but recently I plugged a custom IPrincipal implementation for AzMan into an ASP.NET 2.0 application and saw strange errors until I found out what the problem was. I set Context.User in PostAuthenticateRequest and my implementation contains an [AzManPrincipalPermission] which relies on Thread.CurrentPrincipal – but this was set to the wrong value because I relied on ASP.NET to sync to Context.User – which is not the case.
I wonder why the DefaultAuthentication event fires before PostAuthenticateRequest – maybe to guarantee that Thread.CurrentPrincipal and Context.User are correctly populated when the even fires. But why doesn’t it also fire then after PostAuthenticateRequest to sync again??
So you basically have 2 choices:
1. Put the code in AuthenticateRequest and set Context.User. There are some implications:
- Make sure your module runs after the built-in authentication modules
- You run before RolesManager
- If you call code from AuthenticateRequest (or another module handles the event after you) that relies on Thread.CurrentPrincipal you have to set it anyway.
2. Put the code in PostAuthenticateRequest (which seems to be the designated place) and set Context.User = Thread.CurrentPrincipal = myPrincipal;
RoleManager also fires a GetRoles event where you can override the role fetching, but if this event is handled (and you set the EventArgs.RolesPopulated property to true) all RoleManager logic will be bypassed (including the step that syncs Context.User and Thread.CurrentPrincipal – aargh).
Proposed solution: Always set Thread.CurrentPrincipal and see those implementation details leak through or Microsoft has to sync both values after PostAuthenticateRequest.
This implementation detail was nicely hidden in 1.1 – why do i have to think about that in 2.0 ??