Why once SSL is enabled with [RequireHttps] at action level, it remains enabled forever? - asp.net-mvc

We want to use https only when strictly required. Why after calling an action like below it remains enabled forever?
[RequireHttps]
public ActionResult LogIn()
{
if(Request.IsAuthenticated)
return RedirectToAction("Index", "Account");
return View();
}
What can we do to disable it when not needed?
Thanks.

The [RequireHttps] attribute can be used on a controller type or action method to say "this can be accessed only via SSL." Non-SSL requests to the controller or action will be redirected to the SSL version (if an HTTP GET) or rejected (if an HTTP POST). You can override the RequireHttpsAttribute and change this behavior if you wish. There's no [RequireHttp] attribute built-in that does the opposite, but you could easily make your own if you desired.
There are also overloads of Html.ActionLink() which take a protocol parameter; you can explicitly specify "http" or "https" as the protocol. Here's the MSDN documentation on one such overload. If you don't specify a protocol or if you call an overload which doesn't have a protocol parameter, it's assumed you wanted the link to have the same protocol as the current request.
The reason we don’t have a [RequireHttp] attribute in MVC is that there’s not really much benefit to it. It’s not as interesting as [RequireHttps], and it encourages users to do the wrong thing. For example, many web sites log in via SSL and redirect back to HTTP after you’re logged in, which is absolutely the wrong thing to do. Your login cookie is just as secret as your username + password, and now you’re sending it in cleartext across the wire. Besides, you’ve already taken the time to perform the handshake and secure the channel (which is the bulk of what makes HTTPS slower than HTTP) before the MVC pipeline is run, so [RequireHttp] won’t make the current request or future requests much faster.
If you're hosting utube, change your embedding to use HTTPS rather than HTTP
If you drop down to HTTP from HTTPS without correctly signing out (see http://msdn.microsoft.com/en-us/library/system.web.security.formsauthentication.signout.aspx ) your username + password is wide open. It's not enough to call SignOut.

I use this action filter that redirects back to http when the https action is completed:
using System.Web.Mvc;
using System;
public class ExitHttpsIfNotRequiredAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
// abort if it's not a secure connection
if (!filterContext.HttpContext.Request.IsSecureConnection) return;
// abort if a [RequireHttps] attribute is applied to controller or action
if (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Length > 0) return;
if (filterContext.ActionDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Length > 0) return;
// abort if a [RetainHttps] attribute is applied to controller or action
if (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RetainHttpsAttribute), true).Length > 0) return;
if (filterContext.ActionDescriptor.GetCustomAttributes(typeof(RetainHttpsAttribute), true).Length > 0) return;
// abort if it's not a GET request - we don't want to be redirecting on a form post
if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return;
// redirect to HTTP
string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}

Related

Using OpenAM OAuth2 with MVC4 and OAuthWebSecurity

Before trying MVC5, I had a go at using OpenAM with MVC4, with slightly better results. I need to authenticate from an asp.net application using OpenAM, and don't fancy the Fedlet route - I can see anybody else who has ever tried that
So, this was my starting point. This shows how to use Google and Facebook, and it works a treat. This goes on to show how to use other providers. I'm using OpenAM with OAuth2, so created "OpenAMClient", inheriting from DotNetOpenAuth.AspNet.Clients.OAuth2Client, and registered this in AuthConfig.RegisterAuth:
OAuthWebSecurity.RegisterClient(new OpenAMClient("consumerKey", "consumerSecret"));
This is great, because it now appears automatically on the login page, and pretty much worked perfectly until I started to use it for authentication. In the GetServiceLoginUrl override, I constructed a path to my OpenAM server, appending the returnURL that had been generated by the application and passed in as a parameter:
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
var response =
string.Format(
"http://localopenam.hibu.com:9080/openam_10.1.0/oauth2/authorize?client_id={0}&response_type=code&scope=email&redirect_uri={1}",
_consumerKey, returnUrl);
return new Uri(response);
}
This got me to my OpenAM server login page, but after authenticating, I got an error saying that the redirection URI wasn't acceptable. Debugging the code, I can see that the ReturnURL starts off in the ExternalLoginResult as "/Account/ExternalLoginCallback", but by the time it reaches GetServiceLoginUrl, it has become:
http://localhost:60448/Account/ExternalLoginCallback?__provider__=OpenAM&__sid__=12e299cbac474b60a935f946f69d04a8
OpenAM isn't having any of that, as the "sid" parameter is dynamic, and it doesn't seem to acccept wildcards - it won't allow the returnURL provided by OAuthWebSecurity.
As a workaround, I intercept the ReturnURL, and switch to a new AccountController method:
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
var workingUrl = "http://localhost:60448/Account/OpenAMCallback";
var response =
string.Format(
"http://localopenam.hibu.com:9080/openam_10.1.0/oauth2/authorize?client_id={0}&response_type=code&scope=email&redirect_uri={1}",
_consumerKey, workingUrl);
return new Uri(response);
}
I add http://localhost:60448/Account/OpenAMCallback as a redirectURL in OpenAM, then added AccountController OpenAMCallback:
public ActionResult OpenAMCallback(string code)
{
Console.WriteLine(code );
//use the code to get the token, then user details etc
return RedirectToLocal(null);
}
This is great, because from here I get the access code, so I can make more requests for the token, get all the allowed user details, all of that kind of thing, but ... I'm jealous of the original ExternalLoginCallback method I've subverted away from, that all the other cool authentication servers use. I want to use OAuthWebSecurity.VerifyAuthentication and OAuthWebSecurity.GetOAuthClientData, but VerifyAuthentication is coming back as null, so that stops that party
I can use http://dotnetopenauth.net/ and do it by hand, but I'd rather use a framework so there's less to maintain. Am I missing something obvious, or is OAuthWebSecurity not really up to this so I should stick with dotnetopenauth?
thanks

Https: Hide controller method from http

Ok, so we have the RequireHttpsAttribute that we can use to ensure that a controller/controller method can only be called over SSL. In the case that we try to hit the method over HTTP, the server issues a 302 to the HTTPS version of the same controller (method).
This implies to my users that it is acceptable to issue the first request insecurely in the first place. I don't feel that this is acceptable. Before I trot out an attribute that issues a 404/500 status code in the case that the HTTP version is hit, does such an attribute already exist?
Before I trot out an attribute that issues a 404/500 status code in
the case that the HTTP version is hit, does such an attribute already
exist?
No, such attribute doesn't exist out of the box.
If the simply act of requesting the page using HTTP is not compromising any user data, I'd say the redirect should be enough and a perfect approach for your scenario. Why bother user with things we can take care of?
This implies to my users that it is acceptable to issue the first
request insecurely in the first place. I don't feel that this is
acceptable. Before I trot out an attribute that issues a 404/500
status code in the case that the HTTP version is hit, does such an
attribute already exist?
If you don't want your application to work at all for these URLs using http:// instead of https://, don't serve anything at all (404 or no connection).
Note that it's ultimately the user's responsibility to check that SSL/TLS is used (and used correctly with a valid certificate). Make sure the links to those address use https:// indeed, and that the users expect https:// to be used, at least for the start page. You could consider using HSTS if their browser support it (or possibly permanent redirects to the entry point that would be cached).
From another comment:
I don't want any info about the url leaked in any way to any third parties
Once the request has been made using this http:// URL from the client, there's little point doing anything on the server. It's too late: an eavesdropper could have seen the request. (If your own page doesn't link to external websites, they wouldn't see that address in the referrer either.)
Even if your server doesn't even listen on the plain HTTP port, an active MITM attacker (or more simply, a proxy) could potentially listen to that request and get the URL, without it even reaching your server.
Again: make sure your users expect https:// to be used, and once they're on a secure page, make sure your links/form actions to other sections of your site all use https://.
So for reference, here's my new attribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
Inherited = true,
AllowMultiple = false)]
public class HttpsOnlyAttribute : FilterAttribute, IAuthorizationFilter
{
private readonly bool disableInDebug;
public HttpsOnlyAttribute(bool disableInDebug = false)
{
this.disableInDebug = disableInDebug;
}
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
#if DEBUG
if (disableInDebug) return;
#endif
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
var context = filterContext.HttpContext;
var request = context.Request;
var isSecure = request.IsSecureConnection;
if (!isSecure)
{
throw new HttpException(404, "Not found");
}
}
}

In Asp.Net MVC 2 is there a better way to return 401 status codes without getting an auth redirect

I have a portion of my site that has a lightweight xml/json REST API. Most of my site is behind forms auth but only some of my API actions require authentication.
I have a custom AuthorizeAttribute for my API that I use to check for certain permissions and when it fails it results in a 401. All is good, except since I'm using forms auth, Asp.net conveniently converts that into a 302 redirect to my login page.
I've seen some previous questions that seem a bit hackish to either return a 403 instead or to put some logic in the global.asax protected void Application_EndRequest()
that will essentially convert 302 to 401 where it meets whatever criteria.
Previous Question
Previous Question 2
What I'm doing now is sort of like one of the questions, but instead of checking the Application_EndRequest() for a 302 I make my authorize attribute return 666 which indicates to me that I need to set this to a 401.
Here is my code:
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == MyAuthAttribute.AUTHORIZATION_FAILED_STATUS)
{
//check for 666 - status code of hidden 401
Context.Response.StatusCode = 401;
}
}
Even though this works, my question is there something in Asp.net MVC 2 that would prevent me from having to do this? Or, in general is there a better way? I would think this would come up a lot for anyone doing REST api's or just people that do ajax requests in their controllers. The last thing you want is to do a request and get the content of a login page instead of json.
How about decorating your controller/actions with a custom filter:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class RequiresAuthenticationAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
var user = filterContext.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.End();
}
}
}
and in your controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[RequiresAuthentication]
public ActionResult AuthenticatedIndex()
{
return View();
}
}
Another way of doing this is to implement a custom ActionResult. In my case, I wanted one anyway, since I wanted a simple way of sending data with custom headers and response codes (for a REST API.) I found the idea of doing a DelegatingActionResult and simply added to it a call to Response.End(). Here's the result:
public class DelegatingActionResult : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
Command(context);
// prevent ASP.Net from hijacking our headers
context.HttpContext.Response.End();
}
private readonly Action<ControllerContext> Command;
public DelegatingActionResult(Action<ControllerContext> command)
{
if (command == null)
throw new ArgumentNullException("command");
Command = command;
}
}
The simplest and cleanest solution I've found for this is to register a callback with the jQuery.ajaxSuccess() event and check for the "X-AspNetMvc-Version" response header.
Every jQuery Ajax request in my app is handled by Mvc so if the header is missing I know my request has been redirected to the login page, and I simply reload the page for a top-level redirect:
$(document).ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
// if request returns non MVC page reload because this means the user
// session has expired
var mvcHeaderName = "X-AspNetMvc-Version";
var mvcHeaderValue = XMLHttpRequest.getResponseHeader(mvcHeaderName);
if (!mvcHeaderValue) {
location.reload();
}
});
The page reload may cause some Javascript errors (depending on what you're doing with the Ajax response) but in most cases where debugging is off the user will never see these.
If you don't want to use the built-in header I'm sure you could easily add a custom one and follow the same pattern.
TurnOffTheRedirectionAtIIS
From MSDN, This article explains how to avoid the redirection of 401 responses : ).
Citing:
Using the IIS Manager, right-click the
WinLogin.aspx file, click Properties,
and then go to the Custom Errors tab
to Edit the various 401 errors and
assign a custom redirection.
Unfortunately, this redirection must
be a static file—it will not process
an ASP.NET page. My solution is to
redirect to a static Redirect401.htm
file, with the full physical path,
which contains javascript, or a
meta-tag, to redirect to the real
ASP.NET logon form, named
WebLogin.aspx. Note that you lose the
original ReturnUrl in these
redirections, since the IIS error
redirection required a static html
file with nothing dynamic, so you will
have to handle this later.
Hope it helps you.
I'm still using the end request technique, so I thought I would make that the answer, but really
either of the options listed here are generally what I would say are the best answers so far.
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == MyAuthAttribute.AUTHORIZATION_FAILED_STATUS)
{
//check for 666 - status code of hidden 401
Context.Response.StatusCode = 401;
}
}

How to step out from https to http mode in asp.net mvc

I have made my Login page as Https enabled by adding the attribute [RequireSSL] on controller Action and it works fine. But after successful login it remains in https environment, however the page is non https page.
Can anybody give me workaround how to step out from https to http mode?
Any help in this regard will be greatly appreciated.
You basically need to do the opposite, which is have a [DoesNotRequireSSL] attribute, which effectively does the opposite of the {RequireSSL] attribute, i.e., redirect to http protocol
public class DoesNotRequireSSL: ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
var response = filterContext.HttpContext.Response;
if (request.IsSecureConnection && !request.IsLocal)
{
string redirectUrl = request.Url.ToString().Replace("https:", "http:");
response.Redirect(redirectUrl);
}
base.OnActionExecuting(filterContext);
}
}
Also, if you would like to ensure that multiple pages have this behaviour, you can set up a base controller, from which all your non-http controllers can inherit from so you dont have to worry about having to repeat yourself for every page which needs this.
CAUTION: I had a similar question. One important thing I learnt was that your auth cookie will be sent over plain text after switching back to HTTP. See this.
CAUTION 2 : Don't forget to consider the dreaded You are about to be redirected to a connection that is not secure message
If you're writing a bank application you need to be real careful - and also realize the increasing number of users on public wifi connections that could well [esily] be funneled through some sneaky proxy. Probably a much bigger concern for mainstream sites but a concern for us all to be aware of.
See also my other question (no answers at time of writing - but then I only just asked it!)
I know this is quite an old question, but many of the links presented above are dead and this code addresses it for ASP.NET MVC 5 by making some slight modifications to the RequireHttpsAttribute that is included in the System.Web.Mvc namespace:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ForbidHttpsAttribute : FilterAttribute, IAuthorizationFilter
{
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.HttpContext.Request.IsSecureConnection)
{
HandleHttpsRequest(filterContext);
}
}
protected virtual void HandleHttpsRequest(AuthorizationContext filterContext)
{
// only redirect for GET requests, otherwise the browser might not propagate the verb and request
// body correctly.
if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("The requested resource can only be accessed *without* SSL.");
}
// redirect to HTTP version of page
var url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
The code comes from this article, which briefly discusses some of the security concerns of forcing users to redirect from HTTPS to HTTP as well.

Why does AuthorizeAttribute redirect to the login page for authentication and authorization failures?

In ASP.NET MVC, you can mark up a controller method with AuthorizeAttribute, like this:
[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
// ...
}
This means that, if the currently logged-in user is not in the "CanDeleteTags" role, the controller method will never be called.
Unfortunately, for failures, AuthorizeAttribute returns HttpUnauthorizedResult, which always returns HTTP status code 401. This causes a redirection to the login page.
If the user isn't logged in, this makes perfect sense. However, if the user is already logged in, but isn't in the required role, it's confusing to send them back to the login page.
It seems that AuthorizeAttribute conflates authentication and authorization.
This seems like a bit of an oversight in ASP.NET MVC, or am I missing something?
I've had to cook up a DemandRoleAttribute that separates the two. When the user isn't authenticated, it returns HTTP 401, sending them to the login page. When the user is logged in, but isn't in the required role, it creates a NotAuthorizedResult instead. Currently this redirects to an error page.
Surely I didn't have to do this?
When it was first developed, System.Web.Mvc.AuthorizeAttribute was doing the right thing -
older revisions of the HTTP specification used status code 401 for both "unauthorized" and "unauthenticated".
From the original specification:
If the request already included Authorization credentials, then the 401 response indicates that authorization has been refused for those credentials.
In fact, you can see the confusion right there - it uses the word "authorization" when it means "authentication". In everyday practice, however, it makes more sense to return a 403 Forbidden when the user is authenticated but not authorized. It's unlikely the user would have a second set of credentials that would give them access - bad user experience all around.
Consider most operating systems - when you attempt to read a file you don't have permission to access, you aren't shown a login screen!
Thankfully, the HTTP specifications were updated (June 2014) to remove the ambiguity.
From "Hyper Text Transport Protocol (HTTP/1.1): Authentication" (RFC 7235):
The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.
From "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content" (RFC 7231):
The 403 (Forbidden) status code indicates that the server understood the request but refuses to authorize it.
Interestingly enough, at the time ASP.NET MVC 1 was released the behavior of AuthorizeAttribute was correct. Now, the behavior is incorrect - the HTTP/1.1 specification was fixed.
Rather than attempt to change ASP.NET's login page redirects, it's easier just to fix the problem at the source. You can create a new attribute with the same name (AuthorizeAttribute) in your website's default namespace (this is very important) then the compiler will automatically pick it up instead of MVC's standard one. Of course, you could always give the attribute a new name if you'd rather take that approach.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
Add this to your Login Page_Load function:
// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
Response.Redirect("Unauthorized.aspx");
When the user is redirected there but is already logged in, it shows the unauthorized page. If they are not logged in, it falls through and shows the login page.
I always thought this did make sense. If you're logged in and you try to hit a page that requires a role you don't have, you get forwarded to the login screen asking you to log in with a user who does have the role.
You might add logic to the login page that checks to see if the user is already authenticated. You could add a friendly message that explains why they've been bumbed back there again.
Unfortunately, you're dealing with the default behavior of ASP.NET forms authentication. There is a workaround (I haven't tried it) discussed here:
http://www.codeproject.com/KB/aspnet/Custon401Page.aspx
(It's not specific to MVC)
I think in most cases the best solution is to restrict access to unauthorized resources prior to the user trying to get there. By removing/graying out the link or button that might take them to this unauthorized page.
It probably would be nice to have an additional parameter on the attribute to specify where to redirect an unauthorized user. But in the meantime, I look at the AuthorizeAttribute as a safety net.
Try this in your in the Application_EndRequest handler of your Global.ascx file
if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
HttpContext.Current.Response.ClearContent();
Response.Redirect("~/AccessDenied.aspx");
}
If your using aspnetcore 2.0, use this:
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Core
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
context.Result = new UnauthorizedResult();
return;
}
}
}
}
In my case the problem was "HTTP specification used status code 401 for both "unauthorized" and "unauthenticated"". As ShadowChaser said.
This solution works for me:
if (User != null && User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
//Do whatever
//In my case redirect to error page
Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}

Resources