MVC - anti-forgery token error - c#

I've been brought onto my first MVC and C# project so I appreciate any guidance.
I've created a new feature that checks to see if user has had security training when they log in. If they haven't, the user is directed to a training page where they simply agree/disagree to the rules. If the user agrees, login is completed. If user disagrees, he/she is logged off.
The issue that I have is that when I select the Agree/Disagree button in the training view, I get the following
It should route me to the homepage or logout the user.
Controller
public ActionResult UserSecurityTraining(int ID, string returnUrl)
{
// check if user already has taken training (e.g., is UserInfoID in UserSecurityTrainings table)
var accountUser = db.UserSecurityTraining.Where(x => x.UserInfoID == ID).Count();
// If user ID is not in UserSecurityTraining table...
if (accountUser == 0)
{
// prompt security training for user
return View("UserSecurityTraining");
}
// If user in UserSecurityTraining table...
if (accountUser > 0)
{
return RedirectToLocal(returnUrl);
}
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UserSecurityTrainingConfirm(FormCollection form, UserSecurityTraining model)
{
if (ModelState.IsValid)
{
if (form["accept"] != null)
{
try
{
// if success button selected
//UserSecurityTraining user = db.UserSecurityTraining.Find(); //Create model object
//var user = new UserSecurityTraining { ID = 1, UserInfoID = 1, CreatedDt = 1 };
logger.Info("User has successfully completed training" + model.UserInfoID);
model.CreatedDt = DateTime.Now;
db.SaveChanges();
//return RedirectToAction("ChangePassword", "Manage");
}
catch (Exception e)
{
throw e;
}
return View("SecurityTrainingSuccess");
}
if(form["reject"] != null)
{
return RedirectToAction("Logoff", "Account");
}
}
return View("UserSecurityTraining");
}
View
#model ECHO.Models.UserSecurityTraining
#{
ViewBag.Title = "Security Training";
Layout = "~/Views/Shared/_LayoutNoSidebar.cshtml";
}
<!--<script src="~/Scripts/RequestAccess.js"></script>-->
<div class="container body-content">
<h2>#ViewBag.Title</h2>
<div class="row">
<div class="col-md-8">
#using (Html.BeginForm("UserSecurityTrainingConfirm", "Account", FormMethod.Post, new { role = "form" }))
{
<fieldset>
#Html.AntiForgeryToken()
Please view the following security training slides:<br><br>
[INSERT LINK TO SLIDES]<br><br>
Do you attest that you viewed, understood, and promise to follow the guidelines outlined in the security training?<br><br>
<input type="submit" id="accept" class="btn btn-default" value="Accept" />
<input type="submit" id="reject" class="btn btn-default" value="Reject" />
</fieldset>
}
</div><!--end col-md-8-->
</div><!--end row-->
</div><!-- end container -->
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}

I don't think you've provided enough of your code to properly diagnose this particular error. In general, this anti-forgery exception is due to a change in authentication status. When you call #Html.AntiForgeryToken() on a page, a cookie is set as well in the response with the token. Importantly, if the user is authenticated, then the user's identity is used to compose that token. Then, if that user's authentication status changes between when the cookie is set and when the form is posted to an action that validates that token, the token will no longer match. This can either be the user being authenticated after the cookie set or being logged out after the cookie is set. In other words, if the user is anonymous when the page loads, but becomes logged in before submitting the form, then it will still fail.
Again, I'm not seeing any code here that would obviously give rise to that situation, but we also don't have the full picture in terms of how the user is being logged in an taken to this view in the first place.
That said, there are some very clear errors that may or may not be causing this particular issue, but definitely will cause an issue at some point. First, your buttons do not have name attributes. Based on your action code, it appears as if you believe the id attribute will be what appears in your FormCollection, but that is not the case. You need to add name="accept" and name="reject", respectively, to the buttons for your current code to function.
Second, on the user successfully accepting, you should redirect to an action that loads the SecurityTrainingSuccess view, rather than return that view directly. This part of the PRG (Post-Redirect-Get) pattern and ensures that the submit is not replayed. Any and all post actions should redirect on success.
Third, at least out of the box, LogOff is going to be a post action, which means you can't redirect to it. Redirects are always via GET. You can technically make LogOff respond to GET as well as or instead of POST, but that's an anti-pattern. Atomic actions should always be handled by POST (or a more appropriate verb like PUT, DELETE, etc.), but never GET.
Finally, though minor, the use of FormCollection, in general, is discouraged. For a simple form like this, you can literally just bind your post as params:
public ActionResult UserSecurityTrainingConfirm(string accept, string reject, ...)
However, then it'd probably be more logical and foolproof to introduce a single boolean like:
public ActionResult UserSecurityTrainingConfirm(bool accepted, ...)
Then, your buttons could simply be:
<button type="submit" name="accepted" value="true" class="btn btn-default">Accept</button>
<button type="submit" name="accepted" value="false" class="btn btn-default">Reject</button>
This essentially makes them like radios. The one that is clicked submits its value, so the accepted param, then, will be true or false accordingly. Also, notice that I switched you to true button elements. The use of input for buttons is a bad practice, especially when you actually need it to submit a value, since the value and the display are inherently tied together. With a button element, you can post whatever you want and still have it labeled with whatever text you want, independently.

Related

How to properly return a view from controller?

I have an application with login. I have a controller and a view. From controller, I try to decide if the user is admin or not, and then to show a specific message in view.
The problem is that when I run the application from Startup and I press the login button, the application redirects me to the home page. When I run the application from the view, it works (it's stupid, but it works).
The link when I run the app prof Startup: localhost:2627/Account/Login
The link when I run the app from view:localhost:2627/Account/LoginReturnUrl=%2FUsers%2FIndex
This is my controller:
public class UsersController : Controller
{
// GET: Users
[Authorize]
public ActionResult Index()
{
if (User.Identity.IsAuthenticated)
{
var user = User.Identity;
ViewBag.Name = user.Name;
ViewBag.displayMenu = "No";
if (isAdminUser())
{
ViewBag.displayMenu = "Yes";
}
return View();
}
else
{
ViewBag.Name = "Not Logged IN";
}
//return View();
return View();
}
This is my View:
#{
ViewBag.Title = "Index";
}
#if (ViewBag.displayMenu == "Yes")
{
<h1>Welcome Admin. Now you can create user Role.</h1>
<h3>
<li>#Html.ActionLink("Manage Role", "Index", "Role")</li>
</h3>
}
else
{
<h2> Welcome <strong>#ViewBag.Name</strong> :) .We will add user module soon </h2>
}
I was trying to follow this tutorial(the login part).
I can't figure out why it doesn't open my view. The user is authenticated, but I only see home view.
What am I doing wrong? Do you have any suggestions?
Thank you!
The reason it's working when you start the app from the User's view is that you are trying to access a protected resource ( i.e., a resource that requires authentication). When the user is not authenticated, the default behavior is to challenge the user ( i.e., authenticate the user) and the user is redirected to a login page. When the user is redirected, the returnUrl query parameter is being set so that the user can be redirected back to this resource after authentication. In this case, the returnUrl is the User's index view.
The link when I run the app from
view:localhost:2627/Account/LoginReturnUrl=%2FUsers%2FIndex
It sounds like you want the Users view to be your homepage or at least the page that the user is redirected to after login. In your AccountController, you will want to force this action if this is the desired behavior.
for example:
switch (result)
{
case SignInStatus.Success:
return return RedirectToAction("Index", "Users");
...
}
You will also want to beware if a returnUrl is present too. Based on your requirements, you might want to give the returnUrl the highest precedence.
If ( SignInStatus.Success)
{
return !string.IsNullOrEmpty(returnUrl) ?
RedirectToLocal(returnUrl):
RedirectToAction("Index", "Users");
}
Basically, it really comes down to where you want to send (aka route) the user after authentication. Maybe the user has a preference for their home page or that if the user is an admin that you always route them to the index view in the UsersController.
In the demo project, you should review the Start.Auth.cs file and become familiar with the authentication options.
For example, the LoginPath is being set in this file. This is the path that user will be redirected to for authentication challenges.
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{...}
});
Reference RedirectToAction:
http://msdn.microsoft.com/en-us/library/dd470594.aspx

How to resolve: The provided anti-forgery token was meant for a different claims-based user than the current user

I am getting this error:
The provided anti-forgery token was meant for a different claims-based user than the current user.
and I am not sure how to correct this..
I have a MVC5 site and in this site I have a login page.
This is the scenario that it occurs on.
User AAA logs in. (No issues)
I attempt to access a view where the user does not have access.
I have the class decorated with an Authorize(Roles="aa")
The view then logs the user off and puts them back to the login page.
User AAA logs in. (This time I get the error mentioned above)
To note:
I am using customErrors and this is where I see the error message.
When I log the user out I am running this method:
[HttpGet]
public void SignOut()
{
IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignOut(MyAuthentication.ApplicationCookie);
}
Could I possibly be missing something on the SignOut?
UPDATE:
This only occurs because of step #2 listed above.
If I log in, then log out (calling same code) then log back in, then I do not have this issue.
I think you've neglected to post some relevant code. The Signout action you have returns void. If you were to access this action directly in the browser, then the user would get a blank page after being signed out with no way to progress forward. As a result, I can only assume you are either calling it via AJAX or calling as a method from another action.
The way anti-forgery works in MVC is that a cookie is set on the user's machine containing a unique generated token. If the user is logged in, their username is used to compose that token. In order for a new cookie, without a username to be set, the user must be logged out and a new request must occur to set the new cookie. If you merely log the user out without doing a redirect or something, the new user-less cookie will not have been set yet. Then, when the user posts, the old user-based cookie is sent back while MVC is looking for the new user-less cookie, and boom: there's your exception.
Like I said, you haven't posted enough code to determine exactly why or where this is occurring, but simply, make sure there is a new request made after logging the user out, so the new cookie can be set.
I was able to reproduce by clicking on the login button more than once before the next View loads. I disabled the Login button after the first click to prevent the error.
<button type="submit" onclick="this.disabled=true;this.form.submit();"/>
Disable the identity check the anti-forgery validation performs. Add the following to your Application_Start method:
AntiForgeryConfig.SuppressIdentityHeuristicChecks = true.
try:
public ActionResult Login(string modelState = null)
{
if (modelState != null)
ModelState.AddModelError("", modelState);
return View();
}
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
AuthenticationManager.SignOut();
return RedirectToAction("Login", "Controller", new { modelState = "MSG_USER_NOT_CONFIRMED" });
}
I haved similar problem. I found this text "#Html.AntiForgeryToken() " in my project in 2 place. And one plase will was in "view file" Views - test.cshtml.
#using (Html.BeginForm())
#Html.AntiForgeryToken()
<div class="form-horizontal">
<div class="form-group">
#if (#Model.interviewed)
...
I deleted this code line ("#Html.AntiForgeryToken() ") and working fine.
PS: But I am not delete this code in file _LoginPartial.cshtml.
Good luck!

Antiforgery token error with SSL enabled

I've created default MVC 5 website application in VS 2013 and the only thing I did was enabling SSL.
Every time I'm submitting second form (first submits fine) on Firefox I get this error:
The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster.
Every time I'm submitting second form I get this error:
The required anti-forgery cookie "__RequestVerificationToken" is not present.
Using IE gives no errors no matter what.
I've tried to generate and paste machine keys in web.config and it didn't help
I've tried to recreate project/restart IIS/try IIS 7 instead of IIS Express and still no good.
Disabling SSL works, but I need SSL in my project.
[ValidateForgeryAttribute]'s are set before every POST action which is called from submitting form with #Html.AntiForgeryToken() set.
Update
This is example of form that causes exception on submit, but keep in mind that so does EVERY submit with #Html.AntiForgeryToken() (and same Attribute decoration on corresponding Action).
using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) {
#Html.AntiForgeryToken()
<div id="socialLoginList">
<p>
#foreach (AuthenticationDescription p in loginProviders) {
<button type="submit" class="btn btn-default" id="#p.AuthenticationType" name="provider" value="#p.AuthenticationType" title="Log in using your #p.Caption account">#p.AuthenticationType</button>
}
</p>
</div>
}
Action code:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}

Programming Button OnClick Event

I'm trying to make a method run once a user clicks a button on a page. I created a sample of code to test it but it doesn't work, though it may be because I'm using MessageBox.
<input id="upload-button" type="button" ondblclick="#ModController.ShowBox("Message");" value="Upload"/><br />
Here's the method I'm invoking.
public static DialogResult ShowBox(string message)
{
return MessageBox.Show(message);
}
Any idea on how I can make this function properly?
You could do something like this if your intent is to pass a message to the client and display a dialog:
In your view, add the following:
#using (Html.BeginForm("ShowBox","Home",FormMethod.Post, new { enctype = "multipart/form-data" })){
#Html.Hidden("message", "Your file has uploaded successfully.");
<input id="upload-button" type="submit" value="Click Me" />
<input id="file" name="file" type="file" value="Browse"/>
}
Then in your controller:
[HttpPost]
public ActionResult ShowBox(string message, HttpPostedFileBase file)
{
if (file == null || file.ContentLength == 0)
{
//override the message sent from the view
message = "You did not specify a valid file to upload";
}
else
{
var fileName = Path.GetFileName(file.FileName);
var path = Path.Combine(Server.MapPath("~/App_Data/Uploads"));
file.SaveAs(path);
}
System.Text.StringBuilder response = new System.Text.StringBuilder();
response.Append("<script language='javascript' type='text/javascript'>");
response.Append(string.Format(" alert('{0}');", message));
response.Append(" var uploader = document.getElementById('upload-button'); ");
response.Append(" window.location.href = '/Home/Index/';");
response.Append("</script>");
return Content(response.ToString());
}
NOTE:
I think this approach is less than ideal. I'm pretty sure that returning JavaScript directly like this from the controller is probably some sort of an anti-pattern. In the least, it does not feel right, even thought it works just fine.
It looks like you're using a Razor template. If so, and you're using MVC, I don't think you're approaching this right. MVC doesn't work on an event system like ASP.NET. In MVC you make a requst to an ACtion method, usually with a URL in the form of {controller}/{action}, or something like that.
you have few options:
Setup a javascript event for the dblClick event, and perform an AJAX request to the server in the event handler.
Use #ActionLink() and style it to look like a button.
If you are using ASP.NET, there are certain POST parameters you can set before posting to the server, which will tell ASP.NET to run a certain event handler. However, if you're using ASP.NET, I'd recommend using web forms instead of Razor. I've never used Razor with ASP.NET myself, but I don't think the two technologies Jive very well.

Response.StatusDescription not being sent from JsonResult to jQuery

Please keep in mind that I'm rather new to how MVC, Json, jQuery, etc. works, so bear with me. I've been doing Web Forms for the past 5 years...
I'm working on validating a form within a Modal popup that uses a JsonResult method for posting the form data to the server. I wish I could have just loaded a Partial View in that popup and be done with it, but that's not an option.
Anyway, I have some code that was working yesterday, but after I did a pull / push with Git, something went a bit wrong with my validation. I do some basic validation with regular JavaScript before I pass anything to the server (required fields, correct data types, etc.), but some things, like making sure the name the user types in is unique, require me to go all the way to the business logic.
After poking around the internet, I discovered that if you want jQuery to recognize an error from a JsonResult in an AJAX request, you must send along a HTTP Status Code that is of an erroneous nature. I'm fairly certain it can be any number in the 400's or 500's and it should work...and it does...to a point.
What I would do is set the Status Code and Status Description using Response.StatusCode and Response.StatusDescription, then return the model. The jQuery would recognize an error, then it would display the error message I set in the status description. It all worked great.
Today, it seems that the only thing that makes it from my JsonResult in my controller to my jQuery is the Status Code. I've traced through the c# and everything seems to be set correctly, but I just can't seem to extract that custom Status Description I set.
Here is the code I have:
The Modal Popup
<fieldset id="SmtpServer_QueueCreate_Div">
#Form.HiddenID("SMTPServerId")
<div class="editor-label">
#Html.LabelFor(model => model.ServerName)
<br />
<input type="text" class="textfield wide-box" id="ServerName" name="ServerName" title="The display name of the Server" />
<br />
<span id="ServerNameValidation" style="color:Red;" />
</div>
<div class="editor-label">
<span id="GeneralSMTPServerValidation" style="color:Red;" />
</div>
<br />
<p>
<button type="submit" class="button2" onclick="onEmail_SmtpServerQueueCreateSubmit();">
Create SMTP Server</button>
<input id="btnCancelEmail_SmtpServerQueueCreate" type="button" value="Cancel" class="button"
onclick="Email_SmtpServerQueueCreateClose();" />
</p>
</fieldset>
The Controller
[HttpPost]
public virtual JsonResult _QueueCreate(string serverName)
{
Email_SmtpServerModel model = new Email_SmtpServerModel();
string errorMessage = "";
try
{
Email_SmtpServer dbESS = new Email_SmtpServer(ConnectionString);
model.SMTPServerId = System.Guid.NewGuid();
model.ServerName = serverName;
if (!dbESS.UniqueInsert(model, out errorMessage))
{
return Json(model);
}
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
Response.StatusCode = 500;
Response.StatusDescription = errorMessage;
return Json(model);
}
The jQuery Ajax Request
$.ajax({
type: 'POST',
data: { ServerName: serverName },
url: getBaseURL() + 'Email_SmtpServer/_QueueCreate/',
success: function (data) { onSuccess(data); },
error: function (xhr, status, error) {
$('#GeneralSMTPServerValidation').html(error);
}
});
Like I mentioned, yesterday, this was showing a nice message to the user informing them that the name they entered was not unique if it happened to exist. Now, all I'm getting is a "Internal Server Error" message...which is correct, as that's what I am sending along in my when I set my Status Code. However, like I mentioned, it no longer sees the custom Status Description I send along.
I've also tried setting it to some unused status code to see if that was the problem, but that simply shows nothing because it doesn't know what text to show.
Who knows? Maybe there's something now wrong with my code. Most likely it was a change made somewhere else, what that could be, I have no idea. Does anyone have any ideas on what could be going wrong?
If you need any more code, I'll try to provide it.
Thanks!
In your error callback, try using
xhr.statusText
Also, if you're using Visual Studio's webserver, you may not get the status text back.
http://forums.asp.net/post/4180034.aspx

Categories