Turning off ASP.Net WebForms authentication for one sub-directory
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.
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.