views:

780

answers:

2

I'm using a HttpModule to rewrite the urls on a multi-lingual site. In the HttpModule, I'm adding a handler for the BeginRequest event, and looking for the first part of the path which contains the culture name.

For example, /fr-ca/index.aspx will be rewritten to /index.aspx and set the thread's culture and ui culture to 3084. This works fine.

Enter forms authentication. Forms authentication still works fine with the unmapped url, but if the user is unauthorized, it will redirect to the loginUrl as set in the authentication section of the web.config, and include the ?ReturnUrl= querystring parameter to handle redirecting back to the requested page once the user is authenticated.

There are two problems here if the user requests a page in a language other than the default:

  1. The loginUrl ignores the path before it's rewritten
  2. The ReturnUrl parameter also ignored the raw path.

It's equivalent to Request.Url.PathAndQuery instead of Request.RawUrl.

I can't jump into the pipeline at the AuthorizeRequest event instead because then I'd have to protect against all possible culture values, because I'm using one web.config file with multiple location paths. This also doesn't fix the first issue.

I've gone through the FormsAuthenticationModule in reflector and I see where I could change it to solve #1 and #2, but it's of course sealed.

I've also browsed around a lot, but I can't see any workable solutions.

The FormsAuthenticationModule is checking for a 401 header that's generated in UrlAuthorizationModule. If you look for the referrer (which is empty) on the loginUrl page it confirms that.

Any thoughts?

EDIT #1

I'm using IIS 6, and IIS 7 is not an option.

EDIT #2

The login page wasn't picking up the default culture/ui culture because when I generated the local resource (design view: Tools > Generate Local Resource), the IDE added the following to the page directive:

culture="auto" meta:resourcekey="PageResource1" uiculture="auto"

Be careful with this in vs.net 2008! At least this solves one of the problems with the default culture not being referenced, but #1 and #2 are still outstanding.

EDIT 3

I hoped that I could jump into one of the pipeline events to do my own redirect, but in Reflecting the System.Web.Security.UrlAuthorizationModule OnEnter method, I learned that once the 401 header is set, the method calls application.CompleteRequest, which as you can guess, brings us right to the EndRequest event. This is what the FA module jumps into the do the redirect, and I'm afraid I can't jump in front to do my own redirect! I'm surprised there aren't more people coming across this issue, or perhaps they haven't chimed in yet.


Example:

I have a member's section in the physical folder /members/ that's protected by forms authentication.

In the web.config, I have:

<authentication mode="Forms">
  <forms loginUrl="~/members/login.aspx" timeout="40" />
</authentication>

and...

  <location path="members">
      <system.web>
       <authorization>
        <deny users="?" />
        <allow roles="Members" />
        <deny users="*" />
       </authorization>
      </system.web>
  </location>

When an unauthenticated user requests the index page of /members/, they get redirected to the loginUrl from the authentication section above by the FormsAuthenticationModule. It also appends the ?ReturnUrl parameter with the requested page.

This works just fine if a user is browsing the website using the default culture, but my HttpModule sets the culture based on the presence of a culture name in the first part of the path.

So, /fr-ca/members/index.aspx gets rewritten to /members/index.aspx and sets the culture/ui culture to Canadian French. Unforunately, the FormsAuthenticationModule sends the user to the loginUrl page with the rewritten url, not the original. So, the culture setting is lost and the redirect url is incorrect.

Hope that helps @Greg

+1  A: 

About the only thing I can think of is doing the redirect to the Login form manually.

1) Allow all access to membership directory
2) All pages in the membership directly inherit from a subclass of Page.
3) In PreInit(?) of Page subclass, check to see if user is member of membership role.
4) If not, build URL of login page yourself, including ReturnURL parameter and redirect user to your login URL.

You might be also able to hack this by putting a a bunch of < location> sections in your web.config. ex: < location path="fr-ca/members">< authenication>< forms loginUrl="~/fr-ca/members/login.aspx">...but I really have no idea.

There's probably other ways to do it that I'm not familiar with.

Greg
+1 for a solution that will work for this particular application. Actually, it would be fairly simple as I already inherit from a base class in the member's section. I would like a solution that doesn't require this, as in more complicated scenarios with multiple protected sections it would be difficult. I was hoping that I could jump into the pipeline before EndRequest as called in the forms authentication module, but I can't seem to get that working.
ScottE
Yeah, I agree that it's not an ideal solution.
Greg
You can't have multiple <authentication> sections like that anyway.
ScottE
+3  A: 

Ok, problem solved.

Turns out the key was clearing the machine.config httpModules, adding my custom httpModule, then adding back the required default httpModules. This allowed my custom httpModule to jump into the EndRequest event before the FormsAuthenticationHttpModule.

<httpModules>
    <clear/>
    <!-- custom -->
    <add name="LocalizationHttpModule" type="LocalizationHttpModule"/>
    <!-- add back defaults, exlude PassportAuthentication, AnonymousIdentification, Profile -->
    <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
    <add name="Session" type="System.Web.SessionState.SessionStateModule" />
    <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
    <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
    <add name="RoleManager" type="System.Web.Security.RoleManagerModule" />
    <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
    <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />

</httpModules>

Then, in my custom httpModule, I just tap into the EndRequest, look for the 401 status code, and redirect as I wish. Basically I'm rewriting the code from the FormsAuthenticationHttpModule OnLeave method to suit my needs.

Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init

    Dim authentication As AuthenticationSection = WebConfigurationManager.GetSection("system.web/authentication")
    If authentication.Mode = AuthenticationMode.Forms Then
        Me._LoginUrl = authentication.Forms.LoginUrl
        AddHandler context.EndRequest, AddressOf Context_EndRequest
    End If

End Sub

Private Sub Context_EndRequest(ByVal sender As Object, ByVal e As EventArgs)
    Dim application As HttpApplication = DirectCast(sender, HttpApplication)
    Dim context As HttpContext = application.Context

    If (context.Response.StatusCode = &H191) Then                
       ' do custom redirect here
    End If

End Sub
ScottE