Include custom content in ASP.NET Windows Authentication 401 response - asp.net

We have an internal system where users can authenticate using Windows authentication but we want to include some custom content in the 401 repsonse that takes the user to a custom username/password authentication page if the cancel off the Windows authentication dialog.
When an unauthenticated user accesses a page, Windows authentication responds with a 401 Unauthorized response, with the headers
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
This prompts the browser to show a dialog asking for Windows credentials. If the user enters their credentials, there is another handshake request-response, and then the user is authenticated and the requested page is shown.
If the user cancels the dialog, the browser displays the content of the original 401 Unauthorized reponse.
We use Application_EndRequest in the Global.asax to return some custom content here which takes the user to our custom login system.
/// <summary>
/// If we are about to send the NTLM authenticate headers, include this content. Then, if the user cancels off the
/// windows auth dialog, they will be presented with this page and redirected to the username / password login screen
/// </summary>
void Application_EndRequest(object sender, EventArgs e)
{
Logger.DebugFormat("in Application_EndRequest. Response.StatusCode: {0}", Response.StatusCode);
if (Response.StatusCode == 401)
{
Response.ContentType = "text/html";
var redirectUrl = string.Format("https://mycompany.com/SSO/Login?encryptedSessionRequestId={1}",
HttpUtility.UrlEncode(Request.QueryString["encryptedSessionRequestId"]));
var text = File.ReadAllText(Server.MapPath("~/UnauthorizedError.html"));
text = text.Replace("[USERNAME_PASSWORD_LOGON_REDIRECT]", redirectUrl);
Response.Write(text);
Logger.Debug("Done writing response");
Response.End();
}
}
UnauthorizedError.html contains the following script which forwards the user on
<script language="javascript" type="text/javascript">
window.location = "[USERNAME_PASSWORD_LOGON_REDIRECT]";
</script>
When running locally in (Win7/IIS7.5), this works great, we use Response.Write() to send our content and the user is able to see it when they cancel the dialog.
But when accessing the site remotely, e.g. when it running in our development environment, although we can see from the logs that Application_EndRequest is being called and our content written to the response, it is overridden at some point and all that reaches the browser is the authentication headers and the text You do not have permission to view this directory or page.
1) How can we prevent this content being overwritten?
2) Where in the pipeline might this be happening?
3) Does anyone have any suggestions of another way to implement this behaviour, e.g. with an HTTP module?
Thanks!

I wasted a lot to solve this problem but there isn't direct solution.
It because win auth works on iis not site level and you can't control how to auth current request.
There is several hacky ways to use redirection on different login pages depending on ip.

I know this is an old post but I would like to share a solution to this problem.
When altering the response message for remote requests (read: non-localhost) you will need to add the following to your config file:
<system.webServer>
<httpErrors existingResponse="PassThrough"></httpErrors>
</system.webServer>
If you do not allow the response to "pass through" remote clients will get the default "You do not have permission to view this directory or page".
I got this info from: https://stackoverflow.com/a/17324195/3310441

Related

AuthenticationProperties.RedirectUri is not passed to Google in Challenge()

Within my web application I have registered Google as a single sign-on provider:
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions {
ClientId = "8765.......apps.googleusercontent.com",
ClientSecret = "Secret"
})
My app doesn't allow users to sign-up/register (instead their accounts are created by an administrator, but they can later link their account up with Google).
In my "Sign in with Google" controller, I am trying to issue a Challenge() to redirect to Google. This might not be thecorrect approach:
string redirectUri = "http://localhost:55262/SSO/Google/ProcessToken"; // actually created in code, but shown as string for clarity
AuthenticationProperties properties = new AuthenticationProperties();
properties.RedirectUri = Server.UrlEncode(redirectUri);
Context.GetOwinContext().Authentication.Challenge(properties, "Google");
This correctly sends the user to Google, but Google then presents Error: redirect_uri_mismatch, saying that:
The redirect URI in the request: http://localhost:55262/signin-google
did not match a registered redirect URI.
I've seen this error before when the return URI collection in the Google control panel does not contain the redirect_uri specified.
If I debug in VS2015, I can see the redirect_uri property being set correctly in the AuthenticationProperties, but it seems that OWIN/Katana is not passing it to Google. Instead, when I hit Google, the return_uri is the default one used by OWIN/Katana. The one I set is being ignored.
The Google request details seem to confirm this:
scope=openid profile email
response_type=code
redirect_uri=http://localhost:55262/signin-google
What am I doing wrong here please? Should I not be using Challenge() to allow users to link up their local application account with Google?
To provide additional information on the accepted answer...
Its okay to ignore /signin-google
It emerges that the /signin-google URI is internally-managed by OWIN/Katana. You, as a developer, do not need to be concerned by it, but you do need to add it in the Google developer console as an Authorized redirect URI.
In the Google request, note that OWIN always passes the redirect URI to Google as /signin-google, regardless of what custom URI you set in the AuthenticationProperties.RedirectUri property. Although at first this may seem like a bug/problem, it has a major advantage in that OWIN can manage all callbacks via a single callback URI. Your callback URI is not forgotten about either (see below)!.
So what about your own redirect URL?
Well, that's where the AuthenticationProperties() come into play. By specifying your own callback URL like so...
AuthenticationProperties properties = new AuthenticationProperties { RedirectUri = "https://my.app.com/custom/callback/uri" };
...after OWIN has examined the Google token and extracted the necessary details, the user is then redirected to your specified URL.
This was where I was getting confused, as I didn't understand what to do with /signin-google, when in actual fact no action was taken. This applies to both MVC and webforms - you do not need to concern yourself with what gets passed to Google. However, if using webforms, or specifying authorization rules in web.config, you will need this to prevent returning users hitting the logging page again:
<location path="signin-google">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
Here is all the code you need to send the user to Google, and return the token containing their details:
Outbound
Send the user to Google from a controller, button click event, page load, anything (regardless of your ASP/hosting stack):
// set the callback, for after OWIN finishes examining what comes back from Google
AuthenticationProperties properties = new AuthenticationProperties { RedirectUri = "https://www.myapp.com/some/callback/uri" };
// send the user to Google
Context.GetOwinContext().Authentication.Challenge(properties, "Google");
// Stop execution of the current page/method - the 401 forces OWIN to kick-in and do its thing
Response.StatusCode = 401;
Response.End();
Inbound
The user is returned from Google after validating their identity
Microsoft.AspNet.Identity.Owin.ExternalLoginInfo loginInfo = Context.GetOwinContext().Authentication.GetExternalLoginInfo();
Note that the OWIN's Open Authentication have predefined methods. In another words, in localhost:port/signin-google, the OWIN awaits for calling the signin-google by the external authentication service (Although you can't find its implementation inside the project). The signin-google is a valid and working path and I prefoundly exhort you not to change it (due to avoid writing a new implementation as a controller action).
I had similar trouble, After spending many weary days, finally, I found out the problem comes from the original user's URL which is effective on the sent redirect_uri by the OWIN. Clearly:
If you type www.site.com → redirect_uri equals to
www.site.com/signin-google
If you type site.com → redirect_uri equals to
site.com/signin-google
And Google will return redirect_uri_mismatch Error for one of the above cases based on entered redirect URLs in Console. I think your problem comes from this reality too and the solution is setting any possible URLs in console.

How do I ensure Breeze.js save doesn't trigger the browser's authentication dialogue?

I'm using Breeze in a single page application saving to an asp.net web api server. My app is using forms authentication and my Breeze controller is decorated with the Authorize attribute;
[System.Web.Http.Authorize]
[BreezeController]
public class ReminderController : ApiController
{ ... }
I'm seeing an issue where as soon as Breeze attempts to save, and before the request completes, the browser instantly pops up a username/password authentication dialog. I'm not entirely sure why this happens as the aspx cookie is not set to expire but I can replicate it by logging in to my app, deleting the cookie and then triggering a Breeze save. If I click cancel on the dialog, my server responds with a 401 which my client code handles. I want to ensure the auth dialog is never shown to the user. This happens in Chrome and IE currently but I'm sure it didn't do this in older releases of Chrome stable.
The request can be seen here, still listed as Pending while the dialog is displayed
Thanks
Here's the custom Authorize attribute I am using to get around this issue in our application (warning, auto converted from VB.NET to C#, so it may not be 100% correct!). It sends a 403 status instead of a 401 if the user is already authenticated, to stop the browser from prompting the user to log on. Tested in IE and Firefox.
/// <summary>
/// Custom <see cref="AuthorizeAttribute"/> for WebAPI that sends a 403 Forbidden status instead
/// of 401 Unauthorized, if the user is authenticated, so that the browser will not display a
/// logon prompt.
/// </summary>
/// <remarks></remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class MyAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
var user = Thread.CurrentPrincipal;
if (user != null && user.Identity.IsAuthenticated) {
// If the user is authenticated then we don't want to prompt them to authenticate.
// Ajax requests get a forbidden status code to stop the browser login prompt,
// since Unauthorized (401) would cause the browser to display a login dialog.
actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Forbidden");
} else {
// If the user isn't authenticated then do the normal thing
base.HandleUnauthorizedRequest(actionContext);
}
}
}
It seems there is no reliable, cross browser way to prevent the dialogue at the client end. Checking Fiddler I can see the actual response to the ajax request when the cookie has expired (it's annoying Chrome dev tools don't show this and just sits at 'Pending') contains WWW-Authenticate headers. Strangely, IIS Express running locally doesn't include these headers and the dialogue never shows. It's only apparent when I publish to full IIS on my provider. The plan is to override the Web Api Authorize attribute (the method which handles authorise failure) and remove the WWW-Authenticate headers.
I have no idea why IIS 8.0/Express return different headers.

Where exactly does Forms Authentication exist in the Http Pipeline?

Where exactly does Forms Authentication exist in the Http Pipeline?
This is handled by an HTTP module, System.Web.Security.FormsAuthenticationModule. If you look at the system-wide web.config file, c:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\web.config, you can see where it's mentioned in the <httpModules> section. The site-specific web.config file will inherit the configuration in that file.
On each request, the module will look for an authentication cookie. If it's not present, the request is redirected to the login page. On a successful login, an authentication cookie is sent back to the browser. Then on subsequent requests, the browser will send the cookie, which will be validated by the module, and then the request is handled as usual.
Guess I should've thought of this first but it didn't dawn on me until I saw the answer from #Carl Raymond that I can just crack it open in reflector. So to answer my own question
public void Init(HttpApplication app)
{
if (!_fAuthChecked)
{
_fAuthRequired = AuthenticationConfig.Mode == AuthenticationMode.Forms;
_fAuthChecked = true;
}
if (_fAuthRequired)
{
FormsAuthentication.Initialize();
app.AuthenticateRequest += new EventHandler(this.OnEnter);
app.EndRequest += new EventHandler(this.OnLeave);
}
}
OnEnter calls the private method OnAuthenticate which passes in the application context and this is where it validates/writes out the Form Auth tickets.
In OnExit it checks the response for a Http Status Error Code 401 and if it finds it, that's when it redirects to the Login Url.

Store cookie for other site

I have multiple asp.net sites. When a user logs unto one of the sites, I want to store a cookie telling me that a user has logged on. When the user later visits one of the other sites I have, I would like to read the cookie from that site.
AFAIK you neither can read cookies from or write cookies to other sites, so what could a workaround be?
Perhaps making a redirect to http://www.othersite.com/SaveCookie.aspx ?
Give me some ideas :-)
One of our clients has exactly the same requirement (logging into multiple sites on different domains), complicated by the fact that one of the sites requires that the user is logged in to a classic ASP application, a .NET 1.1 application and a .NET 3.5 application running on different hardware, but under the same domain...
We've basically implemented a system of round-robin style redirects, where each domain logs the user in, then bounces them on to the next domain until they return to the original domain at which point they are redirected to their original request.
So (pages and domains changed to protect the innocent):
User requests www.example1.com/page1.aspx
A cookie is set that tells us the user was attempting to access page1.aspx, and the user is sent to the www.example1.com/login.aspx
The user logs in, and is then redirected to www.example2.com/processlogin.aspx?token=EncryptedToken
ProcessLogin.aspx checks for a cookie telling it where to direct the user, if it can't find one, it decrypts the token, logs the user in on example2.com, and then redirects them to www.example1.com/processlogin.aspx?token=EncryptedToken (or example3.com - repeat as required)
As in 4, ProcessLogin.aspx checks for the cookie, finds it, deletes it and redirects the user to /page1.aspx.
If the user later on visits a page on www.example2.com, before the authentication ticket timeout, they will still be logged in on that site as well.
Edit to respond to comment
That depends on how you are making the "request to the other pages". If you make the request from your code behind, what you're doing is effectively setting the cookie on the server, rather than on the users browser.
Cookies need to be issued by the server to the client browser, and that is done in the headers of the page response - so you need to direct the users browser to a page on the other site to issue the cookie from that domain.
You could generate a request to the other page in an IFrame, or try and do it in a self closing pop-up window - but that has other issues like pop-up blockers, flickering windows, etc.
After some investigation we found that a round-robin set of redirects like this was the simplest and most reliable solution.
A very basic code setup:
An .aspx page, containing a Login control, with a method "OnLoggedIn" attached to the LoggedIn event of the control:
void OnLoggedIn(object sender, EventArgs e){
string returnUrl = Request.QueryString["returnUrl"];
// Create new cookie, store value, and add to cookie collection
HttpCookie myCookie = new HttpCookie("WhereTo");
myCookie["ReturnUrl"] = ReturnUrl;
Response.Cookies.Add(myCookie);
// Redirect user to roundtrip login processor on next domain.
// Work out domain as required.
string redirect = GetNextDomain();
// Add encoded user token
redirect += "?token=" + EncodeUserToken();
// Redirect the user, and end further processing on this thread
Response.Redirect(redirect, true);
}
Then on both servers you have ProcessLogin.aspx, that has something like this in it:
protected void Page_Load(object sender, EventArgs e){
// Look for redirect cookie
if (Request.Cookies["WhereTo"]["ReturnUrl"] != null){
// Save value from cookie
string redirect = Request.Cookies["WhereTo"]["ReturnUrl"];
// Delete original cookie by creating an empty one, and setting it
// to expire yesterday, and add it to the response.
HttpCookie myCookie = new HttpCookie("WhereTo");
myCookie.Expires = DateTime.Now.AddDays(-1d);
Response.Cookies.Add(myCookie);
// Redirect the user, and stop processing
Response.Redirect(redirect, true);
}
// Still here, so log in and redirect
string encryptedToken = Request.QueryString["token"];
if (!string.IsNullOrEmpty(encryptedToken)){
// Decrypt token, and log user in
// This will vary depending on your authentication mechanism
PerformLogin(encryptedToken);
}
// Redirect user to roundtrip login processor on next domain.
// Work out domain as required.
string redirect = GetNextDomain();
// Add encoded user token - no need to recalculate, it will be the same
redirect += "?token=" + encryptedToken;
// Redirect the user, and end further processing on this thread
Response.Redirect(redirect, true);
}
You're looking for a Single Sign-On (SSO) solution.
If it's possible for you to host your sites at different subdomains below the same domain, you can save cookies that are shared for the whole domain, e.g.:
"site1.yourdomain.com" and
"site2.yourdomain.com"
can both read cookies saved to the domain "yourdomain.com"
Another alternative is to tell the other site about the login via a request to it, as in your redirect suggestion. You could do this in several ways, e.g. by loading the page in an iframe, sending the data directly from one server to another, and so on. None of these are particularly elegant, though, and in the case of login, as Tomas Lycken says, you should really be going for a proper SSO implementation.

Forms Authentication and POST requests from AJAX

We have an ASP.NET app protected by forms authentication. The app uses MS AJAX heavily to call its web-services.
When the forms authentication times out, and a GET-request happens - all is fine (the user is redirected to a login page).
BUT when the forms authentication times out and a POST-request happens (ajax) - no redirect happens, instead the app returns "401 unathorized" and the browser prompts for username and password (not a login form, but a browsers built-in dialog). Of course entering ANY username/password never helps.
How do I handle this?
UPDATE: After looking with firebug, I just found out that regular POST requests redirect to login fine, it's only web-service calls that throw "401 Unauthorizes".
The difference between a regular request and web-service is URL. Which is "page.aspx" for a regular post-request and "service.asmx/MethodName" for webservices...
Ok, answering my own questin.
After looking into this issue and researching a bit more I found that when a web-app is protected by Forms-Authentication and the user is not authenticated, this is what happens:
If it's a GET-request - the user is
redirected to the login page.
If it's a POST-request to a page - the user is
redirected to the login page.
If it's a POST-request to a web-service - the
user gets 401-unauthorized
Thats how ASP.NET works
And if a web-service is called by AJAX (xmlHttpRequest object) and returns 401 - of course the browser shows a pop-up login box.
Now, what should you do is add some code to Application_PostAuthenticateRequest that will prevent throwing 401 for webservices.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
if (Request.RequestType == "POST" //if its POST
&& !User.Identity.IsAuthenticated //if user NOT authed
&& !HasAnonymousAccess(Context) //if it's not the login page
)
{
//lets get the auth type
Configuration config = WebConfigurationManager.OpenWebConfiguration("~");
SystemWebSectionGroup grp = (SystemWebSectionGroup)config.GetSectionGroup("system.web");
AuthenticationSection auth = grp.Authentication;
//if it FORMS auth
if(auth.Mode== AuthenticationMode.Forms)
{
//then redirect... this redirect won't work for AJAX cause xmlHttpRequest can't handle redirects, but anyway...
Response.Redirect(FormsAuthentication.LoginUrl, true);
Response.End();
}
}
}
public static bool HasAnonymousAccess(HttpContext context)
{
return UrlAuthorizationModule.CheckUrlAccessForPrincipal(
context.Request.Path,
new GenericPrincipal(new GenericIdentity(string.Empty), null),
context.Request.HttpMethod);
}
I see two solutions:
(1) "Heart beat" mechanism. On each page include a script that will "ping" the server by some dummy ajax request, like:
<script>
setInterval(ping, 60000); // based on comment by John
function ping()
{
$.get('/do/nothing');
}
</script>
This way the session shouldn't expire as long as the browser window is open.
(2) On each ajax request check the status of the response. If the response has "401 unauthorized" code (or any other code different that 200), that means that the session expired and instead of loading the response into some dialog box in the page redirect the user to login page.
Conclusion based on comments:
The best solution would be to combine the two above mechanisms. Heartbeat mechanism will help to keep the session alive as long as the page is displayed in the browser. But in doesn't guarantee that for sure. The connection to the server can be broke and reopened when the session is expired. So you should check the response status anyway.

Resources