5
votes

A colleague asked me today how to configure IIS 7.5 to use integrated Windows authentication with impersonation for a simple intranet website with just static content that is restricted to a specific group in Active Directory (e.g., "Administrators").

Turns out that IIS sends a HTTP 401 response when the authenticated user does not have permission to the request resource. The permission denied could be the result of a NTFS file ACL, or a system.webServer/security/authorization ACL defined in the IIS config.

All the major browsers seem to interpret this 401 to mean that the end user provided invalid Windows username/password credentials, and thus prompts the user to enter their username/password. IE seems to prompt up to 3 times before showing the 401 response body/content. Chrome and Safari seems to repeatedly prompt the user.

This can be confusing to end users, who keep repeatedly entering a valid Windows username/password, only to be prompted again.

The better way would be for IIS to return a HTTP 403 instead of a HTTP 401:

403 Forbidden

The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated.

Source: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4

How do you configure IIS integrated Windows Authentication to send HTTP 401 for logon failures and HTTP 403 for permission denied?

1

1 Answers

9
votes

What did not work

I tried just about every IIS configuration permutation with no luck. I goofed around with the Windows authentication provider settings, like NTLM, Negotiate, and Negotiate:Kerberos. None of them seemed to do the trick. This isn't too much of a surprise since the browsers are deciding to try authentication again, even though they probably shouldn't.

401 Unauthorized

The request requires user authentication. The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource. The client MAY repeat the request with a suitable Authorization header field (section 14.8). If the request already included Authorization credentials, then the 401 response indicates that authorization has been refuse for those credentials.

Source: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2

What did work

I decided to leverage some down and dirty ASP.NET to fix this. Below implements response rewriting with the help of a couple of text files on the server and ASP.NET dynamic compilation. (I've never used dynamic compilation before, but, compiling seemed way overkill for a static site.)

The Global.asax file below hooks the EndRequest event, rewriting a HTTP 401 response into a HTTP 403 if the user successfully authenticated as a Windows user, but, for some other reason the request is being denied (I assume the reason must be an authorization failure).

The web.config file contains entries to route all requests through the ASP.NET pipeline, and deny access to any authenticated user who is not a member in the "Sales" Windows group.

This solution assumes you have an app running in IIS integrated pipeline mode (i.e. not classic mode), you enabled Windows authentication, and disabled all other authentication schemes.

/Global.asax:

<Script language="C#" runat="server">
     void Application_EndRequest() {
          // rewrite HTTP 401s to HTTP 403s if the user is authenticated using 
            // integrated Windows auth with impersonation, but, 
            // the user lacks permissions to the requested URL
            if (Context.User != null && 
                    Context.User.Identity != null &&
                        Context.User.Identity.IsAuthenticated &&
                            Context.User is System.Security.Principal.WindowsPrincipal && 
                                Context.Response.StatusCode == 401) 
            {
                Context.Response.Clear();
                Context.Response.StatusCode = 403;
            }
        }
</script>

/web.config:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <security>
            <authorization>
                <remove users="*" roles="" verbs="" />
                <add accessType="Allow" roles="Sales" />
            </authorization>
        </security>
      <modules runAllManagedModulesForAllRequests="true" />
    </system.webServer>
</configuration>

For future reference, I created a Gist @ https://gist.github.com/steve-jansen/6234700