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");
}
}
}
Related
I'm using spring-security-core and have setup the secure-channel capabilities, which work fine on my development machine. I've got the following in Config.groovy
grails.plugins.springsecurity.secureChannel.definition = [
'/order/checkout': 'REQUIRES_SECURE_CHANNEL',
'/order/paymentComplete': 'REQUIRES_INSECURE_CHANNEL'
]
Also, deploying to Heroku the associated order processing works fine, as long as I comment out the above lines. As soon as I put them back in, I get:
I see many requests come in on the server, and the Firebug net view shows:
I've got the PiggyBack SSL added on to Heroku, and I'm able to specify an https://... address to navigate to other parts of the site, in which case the browser stays in SSL mode. But if I access the
https:/www.momentumnow.co/order/checkout
address directly, I get the same redirect loop problem. Do you know what the problem is or how I can debug this further. If the latter, would you please update the comment area, and I will respond with updates to the problem area. Thanks
PiggyBack SSL documentation indicates:
"Piggyback SSL will allow you to use https://yourapp.heroku.com, since it uses the *.heroku.com certification. You don't need to buy or configure a certificate, it just works. https://yourcustomdomain.com will work, but it will produce a warning in the browser."
I'll probably switch to another mode as I add a certificate, however that does not seem to be the problem, based on the previous statement.
On the server, I get:
You need to fix the values for the ports since they default to 8080 and 8443. See the section on Channel Security in the docs - http://grails-plugins.github.com/grails-spring-security-core/docs/manual/ - about the grails.plugins.springsecurity.portMapper.httpPort and grails.plugins.springsecurity.portMapper.httpsPort config attributes.
For anyone else stumbling into this (as I did) the problem is that your app doesn't actually receive the request as HTTPS. Rather, Heroku replaces the HTTPS with a "X-Forwarded-Proto" header. Spring-security's HTTPS redirection is then putting you into an infinite redirect loop because it always detects the request as HTTP.
You can write your own SecureChannelProcessor to deal with this:
public class HerokuSecureChannelProcessor extends SecureChannelProcessor {
#Override
public void decide(FilterInvocation invocation, Collection<ConfigAttribute> config)
throws IOException, ServletException {
Assert.isTrue((invocation != null) && (config != null),
"Nulls cannot be provided");
for (ConfigAttribute attribute : config) {
if (supports(attribute)) {
String header = invocation.getHttpRequest().getHeader("X-Forwarded-Proto");
if(header == null){
// proceed normally
if (!invocation.getHttpRequest().isSecure()) {
getEntryPoint().commence(invocation.getRequest(), invocation.getResponse());
}
} else {
// use heroku header instead
if("http".equals(header)) {
getEntryPoint().commence(invocation.getRequest(), invocation.getResponse());
}
}
}
}
}
}
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);
}
}
Trying to use the StartSignInWithTwitter method. When the method is called soon after an exception is thrown. This is using the latest version of DotNetOpenAuth. Would it have anything to do with me developing and running with locally? (VS2010) Is this how I should be doing authentication in the first place? I do see some different ways in the Samples pack that is included with the source.
{"The remote server returned an error: (401) Unauthorized."}
My code looks like below:
public void TwitAuthInit()
{
TwitterConsumer.StartSignInWithTwitter(false).Send();
}
public ActionResult TwitAuth()
{
if (TwitterConsumer.IsTwitterConsumerConfigured)
{
string screenName;
int userId;
if (TwitterConsumer.TryFinishSignInWithTwitter(out screenName, out userId))
{
FormsAuthentication.SetAuthCookie(screenName, false);
return RedirectToAction("Home", "Index");
}
}
return View();
}
To answer your question about "Is this how I should be doing authentication in the first place?":
You probably shouldn't be calling SetAuthCookie(screenName, false) with your screenName, since screen names (I believe) can be recycled. You should instead log the user in using a unique ID, either one you create in your own user database or Twitter's, and then use the screen name only as an alias that is displayed to the user (and perhaps other users if this user were to post something for public viewing). Otherwise, when Twitter recycles a username, that user will inherit all the data from the old user on your site -- not good.
Wanted to confirm that the 401 error is indeed solved by setting a non-empty callback URL on the twitter app config page.
From the Application Type block of the settings page:
To restrict your application from using callbacks, leave this field
blank.
You have to go into TwitterConsumer.cs and change the following URLs:
Request token URL https://api.twitter.com/oauth/request_token
Authorize URL https://api.twitter.com/oauth/authorize
Access token URL https://api.twitter.com/oauth/access_token
As Twitter changed their URLs. I didn't get the memo and spent way too much time debugging this.
For a POST method, the W3 specs say:
If a resource has been created on the origin server, the response
SHOULD be 201 (Created) and contain an entity which describes the
status of the request and refers to the new resource, and a Location
header (see Section 10.4).
http://www.ietf.org/internet-drafts/draft-ietf-httpbis-p2-semantics-05.txt (section 8.5)
The standard response actually seems to be to send a Redirect to the newly created resource.
I'm building my site with ASP.NET MVC, and tried to follow the spec, so created a ResourceCreatedResult class:
public class ResourceCreatedResult : ActionResult
{
public string Location { get; set; }
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.Clear();
context.HttpContext.Response.StatusCode = 201;
context.HttpContext.Response.ClearHeaders();
context.HttpContext.Response.AddHeader("Location", Location);
}
}
And my action looks something like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreateNew(string entityStuff)
{
Entity newEntity = new Entity(entityStuff);
IEntityRepository entityRepository = ObjectFactory.GetInstance<IEntityRepository>();
entityRepository.Add(newEntity);
ActionResult result = new ResourceCreatedResult()
{ Location = Url.Action("Show", new { id = newEntity.Id }) };
return result;
}
However, IE, Firefox and Chrome all fail to redirect to the new resource. Have I messed up generating the correct response, or do web browsers not expect this type of response, instead relying on servers to send a Redirect response?
To be explicit, browsers (including modern browsers like Firefox 3 and IE8) do not "take the hint" and follow up an HTTP 201: Created response with a GET request to the URI supplied in the Location header.
If you want browsers to go to the URI supplied in the Location header, you should send an HTTP 303: See Other status instead.
Redirect after post or post/redirect/get is something your application must do to be user friendly.
Edit. This is above and beyond the HTTP specifications. If we simply return a 201 after a POST, the browser back button behaves badly.
Note that Web Services requests (which do NOT respond to a browser) follow the standard completely and do NOT redirect after post.
It works like this.
The browser POSTS the data.
Your application validates the data. If it's invalid, you respond with the form so they can fix it and POST.
Your application responds with a redirect.
The browser gets the redirect and does a GET.
Your application sees the GET and responds.
Now -- hey presto! -- the back button works.
My solution is to respond with a '201 Created' containing a simple page with a link to the new resource, and a javascript redirect using location.replace().
This lets the same code work for API and browser requests, plays nicely with Back and Refresh buttons, and degrades gracefully in old browsers.
As stated in the spec the response SHOULD be a HTTP 201 with redirect. So it isn't mandatory for a browser vendor to implement the correct answer...
You should try to change to a 30x code to see if it is correctly redirected. If so, it's a browser problem, else it may come from your code (I don't know anything in ASP.NET so I can't "validate" your code)
Shouldn't that only count for when something is "Created" and therefore a simple redirect to action should be genuinely sufficient?
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" });
}