Turning off ASP.Net WebForms authentication for one sub-directory

天大地大妈咪最大 提交于 2019-11-28 23:22:08

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'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.

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.

arcain

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!