Question

I have a large enterprise application containing both WebForms and MVC pages. It has existing authentication and authorisation settings that I don't want to change.

The WebForms authentication is configured in the web.config:

 <authentication mode="Forms">
  <forms blah... blah... blah />
 </authentication>

 <authorization>
  <deny users="?" />
 </authorization>

Fairly standard so far. I have a REST service that is part of this big application and I want to use HTTP authentication instead for this one service.

So, when a user attempts to get JSON data from the REST service it returns an HTTP 401 status and a WWW-Authenticate header. If they respond with a correctly formed HTTP Authorization response it lets them in.

The problem is that WebForms overrides this at a low level - if you return 401 (Unauthorised) it overrides that with a 302 (redirection to login page). That's fine in the browser but useless for a REST service.

I want to turn off the authentication setting in the web.config, overriding the 'rest' folder:

 <location path="rest">
  <system.web>
   <authentication mode="None" />
   <authorization><allow users="?" /></authorization>
  </system.web>
 </location>

The authorisation bit works fine, but the authentication line (<authentication mode="None" />) causes an exception:

It is an error to use a section registered as allowDefinition='MachineToApplication' beyond application level.

I'm configuring this at application level though - it's in the root web.config - and that error is for web.configs in sub-directories.

How do I override the authentication so that all of the rest of the site uses WebForms authentication and this one directory uses none?

This is similar to another question: 401 response code for json requests with ASP.NET MVC, but I'm not looking for the same solution - I don't want to just remove the WebForms authentication and add new custom code globally, there's far to much risk and work involved. I want to change just the one directory in configuration.

Update

I want to set up a single web application and in that I want all the WebForms pages and MVC views to use WebForms authentication. I want one directory to use basic HTTP authentication.

Note that I'm talking about authentication, not authorisation. I want REST calls to come with the username & password in an HTTP header, and I want WebForm & MVC pages to come with the authentication cookie from .Net - in either case authorisation is done against our DB.

I don't want to rewrite WebForms authentication and roll my own cookies - it seems ridiculous that is the only way to add an HTTP authorised REST service to an application.

I can't add an additional application or virtual directory - it's got to be as one application.

Was it helpful?

Solution 2

I've worked around this the messy way - by spoofing the Forms authentication in the global.asax for all the existing pages.

I still don't quite have this fully working, but it goes something like this:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    // lots of existing web.config controls for which webforms folders can be accessed
    // read the config and skip checks for pages that authorise anon users by having
    // <allow users="?" /> as the top rule.

    // check local config
    var localAuthSection = ConfigurationManager.GetSection("system.web/authorization") as AuthorizationSection;

    // this assumes that the first rule will be <allow users="?" />
    var localRule = localAuthSection.Rules[0];
    if (localRule.Action == AuthorizationRuleAction.Allow &&
        localRule.Users.Contains("?"))
    {
        // then skip the rest
        return;
    }

    // get the web.config and check locations
    var conf = WebConfigurationManager.OpenWebConfiguration("~");
    foreach (ConfigurationLocation loc in conf.Locations)
    {
        // find whether we're in a location with overridden config
        if (this.Request.Path.StartsWith(loc.Path, StringComparison.OrdinalIgnoreCase) ||
            this.Request.Path.TrimStart('/').StartsWith(loc.Path, StringComparison.OrdinalIgnoreCase))
        {
            // get the location's config
            var locConf = loc.OpenConfiguration();
            var authSection = locConf.GetSection("system.web/authorization") as AuthorizationSection;
            if (authSection != null)
            {
                // this assumes that the first rule will be <allow users="?" />
                var rule = authSection.Rules[0];
                if (rule.Action == AuthorizationRuleAction.Allow &&
                    rule.Users.Contains("?"))
                {
                    // then skip the rest
                    return;
                }
            }
        }
    }

    var cookie = this.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (cookie == null ||
        string.IsNullOrEmpty(cookie.Value))
    {
        // no or blank cookie
        FormsAuthentication.RedirectToLoginPage();
    }

    // decrypt the 
    var ticket = FormsAuthentication.Decrypt(cookie.Value);
    if (ticket == null ||
        ticket.Expired)
    {
        // invalid cookie
        FormsAuthentication.RedirectToLoginPage();
    }

    // renew ticket if needed
    var newTicket = ticket;
    if (FormsAuthentication.SlidingExpiration)
    {
        newTicket = FormsAuthentication.RenewTicketIfOld(ticket);
    }

    // set the user so that .IsAuthenticated becomes true
    // then the existing checks for user should work
    HttpContext.Current.User = new GenericPrincipal(new FormsIdentity(newTicket), newTicket.UserData.Split(','));

}

I'm not really happy with this as a fix - it seems like a horrible hack and re-invention of the wheel, but it looks like this is the only way for my Forms-authenticated pages and HTTP-authenticated REST service to work in the same application.

OTHER TIPS

If "rest" is simply a folder in your root you are almost there: remove authentication line i.e.

<location path="rest">
  <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
  </system.web>
 </location>

Alternatively you can add a web.config to your rest folder and just have this:

<system.web>
     <authorization>
          <allow users="*" />
     </authorization>
</system.web>

Check this one.

I found myself with the same exact problem, the following article pointed me in the right direction: http://msdn.microsoft.com/en-us/library/aa479391.aspx

MADAM does exactly what you are after, specifically, you can configure the FormsAuthenticationDispositionModule to mute the forms authentication "trickery", and stop it from changing the response code from 401 to 302. This should result in your rest client receiving the right auth challenge.

MADAM Download page: http://www.raboof.com/projects/madam/

In my case, the REST calls are made to controllers (this is a MVC based app) in the "API" area. A MADAM discriminator is set with the following configuracion:

<formsAuthenticationDisposition>
  <discriminators all="1">
    <discriminator type="Madam.Discriminator">
      <discriminator
          inputExpression="Request.Url"
          pattern="api\.*" type="Madam.RegexDiscriminator" />
    </discriminator>
  </discriminators>
</formsAuthenticationDisposition>

Then all you have to do is add the MADAM module to your web.config

<modules runAllManagedModulesForAllRequests="true">
  <remove name="WebDAVModule" /> <!-- allow PUT and DELETE methods -->
  <add name="FormsAuthenticationDisposition" type="Madam.FormsAuthenticationDispositionModule, Madam" />
</modules>

Remember to add the valid sections to the web.config (SO didn't let me paste the code), you can get an example from the web project in the download.

With this setup any requests made to URLs starting with "API/" will get a 401 response instead of the 301 produced by the Forms Authentication.

I was able to get this to work on a previous project, but it did require using an HTTP module to perform the custom basic authentication, since account validation is against a database rather than Windows.

I set up the test as you specified with one one web application at the root of the test website, and a folder containing the REST service. The config for the root application was configured to deny all access:

<authentication mode="Forms">
  <forms loginUrl="Login.aspx" timeout="2880" />
</authentication>
<authorization>
  <deny users="?"/>
</authorization>

I then had to create an application for the REST folder in IIS, and place a web.config file into the REST folder. In that config, I specified the following:

<authentication mode="None"/>
<authorization>
  <deny users="?"/>
</authorization>

I also had to wire up the http module in the appropriate places within the REST directory's config. This module must go into a bin directory under the REST directory. I used Dominick Baier's custom basic authentication module, and that code is located here. That version is more IIS 6 specific, however there is a version for IIS 7 as well on codeplex, but I haven't test that one (warning: the IIS6 version does not have the same assembly name and namespace as the IIS7 version.) I really like this basic auth module since it plugs right into ASP.NET's membership model.

The last step was to ensure that only anonymous access was allowed to both the root application and the REST application within IIS.

I've included the full configs below for completeness. The test app was just a ASP.NET web form application generated from VS 2010, it was using the AspNetSqlProfileProvider for the membership provider; here's the config:

<?xml version="1.0"?>

<configuration>
  <connectionStrings>
    <add name="ApplicationServices"
      connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;Database=sqlmembership;"
    providerName="System.Data.SqlClient" />
  </connectionStrings>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />

    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login.aspx" timeout="2880" />
    </authentication>

    <authorization>
      <deny users="?"/>
    </authorization>

    <membership>
      <providers>
        <clear/>
        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices"
          enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
          maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
        applicationName="/" />
      </providers>
    </membership>

    <profile>
      <providers>
        <clear/>
        <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/"/>
      </providers>
    </profile>

    <roleManager enabled="false">
      <providers>
        <clear/>
        <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
        <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
      </providers>
    </roleManager>

  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
</configuration>

The REST directory contained an empty ASP.NET project generated from VS 2010, and I put a single ASPX file into that, however the contents of the REST folder didn't have to be a new project. Just dropping in a config file after the directory has had an application associated with it should work. The config for that project follows:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="customBasicAuthentication" type="Thinktecture.CustomBasicAuthentication.CustomBasicAuthenticationSection, Thinktecture.CustomBasicAuthenticationModule"/>
  </configSections>
  <customBasicAuthentication
    enabled="true"
    realm="testdomain"
    providerName="AspNetSqlMembershipProvider"
    cachingEnabled="true"
    cachingDuration="15"
  requireSSL="false" />

  <system.web>
    <authentication mode="None"/>
    <authorization>
      <deny users="?"/>
    </authorization>

    <compilation debug="true" targetFramework="4.0" />
    <httpModules>
      <add name="CustomBasicAuthentication" type="Thinktecture.CustomBasicAuthentication.CustomBasicAuthenticationModule, Thinktecture.CustomBasicAuthenticationModule"/>
    </httpModules>
  </system.web>
</configuration>

I hope this will meet your needs.

This may not be the most elegant of solutions but I think it is a good start

1)Create a HttpModule.

2)handle the AuthenticateRequest event.

3)in the event handler check that the request is to the directory that you want to allow access to.

4)If it is then manually set the auth cookie: (or see if you can find another way now that you have control and authentication has not yet happened)

FormsAuthentication.SetAuthCookie("Anonymous", false);

5) Oh almost forgot, you would want to make sure the auth cookie was cleared if the request was not to the directory that you wanted to grant access to.

After looking at your comments to my previous answer, I wondered if you could have your web app automate the deployment of an application on your REST directory. That would allow you to have the benefits of a second application, and would also reduce the deployment burden on your system admins.

My thought was that you could put a routine into the Application_Start method of the global.asax that would check that the REST directory exists, and that it does not already have an application associated with it. If the test returns true, then the process of associating a new application to the REST directory occurs.

Another thought I had was that you could use WIX (or another deployment technology) to build a install package that your admins could run to create the application, however I don't think that's as automatic as having the app configure its dependency.

Below, I've included a sample implementation that checks IIS for a given directory and applies an application to it if it does not already have one. The code was tested with IIS 7, but should work on IIS 6 as well.

//This is part of global.asax.cs
//This approach may require additional user privileges to query IIS

//using System.DirectoryServices;
//using System.Runtime.InteropServices;

protected void Application_Start(object sender, EventArgs evt)
{
  const string iisRootUri = "IIS://localhost/W3SVC/1/Root";
  const string restPhysicalPath = @"C:\inetpub\wwwroot\Rest";
  const string restVirtualPath = "Rest";

  if (!Directory.Exists(restPhysicalPath))
  {
    // there is no rest path, so do nothing
    return;
  }

  using (var root = new DirectoryEntry(iisRootUri))
  {
    DirectoryEntries children = root.Children;

    try
    {
      using (DirectoryEntry rest = children.Find(restVirtualPath, root.SchemaClassName))
      {
        // the above call throws an exception if the vdir does not exist
        return;
      }
    }
    catch (COMException e)
    {
      // something got unlinked incorrectly, kill the vdir and application
      foreach (DirectoryEntry entry in children)
      {
        if (string.Compare(entry.Name, restVirtualPath, true) == 0)
        {
          entry.DeleteTree();
        }     
      }
    }
    catch (DirectoryNotFoundException e)
    {
      // the vdir and application do not exist, add them below
    }

    using (DirectoryEntry rest = children.Add(restVirtualPath, root.SchemaClassName))
    {
      rest.CommitChanges();
      rest.Properties["Path"].Value = restPhysicalPath;
      rest.Properties["AccessRead"].Add(true);
      rest.Properties["AccessScript"].Add(true);
      rest.Invoke("AppCreate2", true);
      rest.Properties["AppFriendlyName"].Add(restVirtualPath);
      rest.CommitChanges();
    }
  }
}

Portions of this code came from here. Good luck with your app!

In .NET 4.5 you can now set

Response.SuppressFormsAuthenticationRedirect = true

Check this page: https://msdn.microsoft.com/en-us/library/system.web.httpresponse.suppressformsauthenticationredirect.aspx

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top