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