I'm creating some user profile edit forms in MVC4 at the moment and for testing I was rendering the UserId property into a readonly textbox on the form like this:
<li>
#Html.LabelFor(model => model.UserId)
#Html.TextBoxFor(model => model.UserId, new { #readonly="readonly"})
</li>
As I'm nearing completion of the edit form I removed this textbox as it's just using up real estate. Once I had done this the model sent back to the controller when saving had the integer default value of 0 and then the Entity Framework blows up as it cannot update any rows. So I added this to the form:
<li>
#Html.HiddenFor(model => model.UserId, new { #readonly="readonly"})
</li>
Is this a safe move? Should I be using the ViewBag for things like this? On the profile details page I render an edit button like this:
#Html.ActionLink("Edit", "Edit", new { id=Model.UserId })
Meaning that the UserId is rendered in the link. Is this safe and secure or do I need to rethink how I move the models and ids around the UI?
TIA,
Is this a safe move?
This will do the job of sending the id to the server. Just get rid of the readonly="readonly" attribute which makes very little sense for a hidden input.
Should I be using the ViewBag for things like this?
This doesn't change anything in terms of security. Any user could still put whatever id he wants. Whether you are using a hidden field or an ActionLink you are still sending the id as plain text to the server and anyone could forge a request and put whatever id he wants. So if you site uses some form of authentication you must absolutely check on the server side that the id that you received actually is a resource that belongs to the currently authenticated user before attempting to perform any actions on it. Otherwise some authenticated user could supply the id of a resource that belongs to another user and be able to update it. Of course that's just a hypothetical scenario, it's not clear at all if this is your case and whether this id needs to be secured.
If UserId is sensitive, then there are other options
Keep UserId server side only with Session state (if your architecture allows for Session)
Put it in an encrypted cookie. Note as per Darin, that these can be compromised.
If it isn't sensitive, then your HiddenFor is fine - post it back with the rest of the form.
Don't put it in your ActionLink Querystring unless this is part of your route (i.e. /Controller/Action/id)
I would strongly suggest using ValueInjecter. Here is a code snippet doing the same thing
[HttpGet]
public new ActionResult Profile()
{
var model = new ProfileModel();
model.InjectFrom<UnflatLoopValueInjection>(this.GetCurrentUser());
return View(model);
}
[HttpPost]
public new ActionResult Profile(ProfileModel model)
{
if (ModelState.IsValid)
{
this.GetCurrentUser().InjectFrom<UnflatLoopValueInjection>(model);
try
{
_userService.SaveOrUpdate(this.GetCurrentUser());
TempData["Success"] = "User was successfully updated.";
return RedirectToAction("Profile");
}
catch (Exception)
{
ModelState.AddModelError("Exception", "Unexpected error");
}
}
return View(model);
}
And here is the view...
#using (Html.BeginForm("Profile", "Account", FormMethod.Post, new { #class = "form-horizontal" }))
{
#Html.ValidationSummary(true, "Unable to update profile. Please correct the errors and try again.", new { #class = "alert alert-block alert-error" })
#Html.EditorForModel()
<div class="form-actions">
<input type="submit" value="Update" class="btn btn-primary" />
</div>
}
Related
This is maybe a little bit of an open ended question.
Basically, I have an MVC project, people can log in and create a profile. If they are logged in and on their profile page they display Edit buttons to enable them to be able to edit sections of their profile.
This is done via an attribute in the View Model that checks if the profile username matches the logged in username.
What I'm having a little trouble with is when posting back to update details via ajax, how can I ensure the user is updating his profile and no one else's. I obviously want to make sure no one can hack the site and update someone else's profile.
Do I just have a check in the action that checks if the logged in user matches the profile they are on... if it doesn't then return a 401? Is there a sexier way of doing this? Custom Attribute etc...?
First of all, you should use the AuthorizeAttribute (or a subclass of it) to protect your controller actions from unauthorized access.
[HttpPost]
[Authorize]
public ActionResult Edit(Model model)
{
// edit model here
}
Secondly, to ensure that the user doesn't update somebody else's profile, you should design your action method to use the current user instead of putting it into the URL.
[HttpPost]
[Authorize]
public ActionResult Edit(Model model)
{
// Only allow the logged in user to edit their own profile
// TODO: Update the user's database record by using the user variable as the key.
var user = User.Identity.Name;
// edit model here
}
Third, you should use the ValidateAntiForgeryToken in conjunction with the AntiForgeryToken HTML helper to ensure your pages are posted to the server by the same browser that requested them.
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Edit(Model model)
{
// Only allow the logged in user to edit their own profile
// TODO: Update the user's database record by using the user variable as the key.
var user = User.Identity.Name;
// edit model here
}
#using (Html.BeginForm("Edit", "Account")) {
#Html.AntiForgeryToken()
#Html.ValidationSummary()
<fieldset>
<legend>Change Password Form</legend>
<ol>
<li>
#Html.LabelFor(m => m.OldPassword)
#Html.PasswordFor(m => m.OldPassword)
</li>
<li>
#Html.LabelFor(m => m.NewPassword)
#Html.PasswordFor(m => m.NewPassword)
</li>
<li>
#Html.LabelFor(m => m.ConfirmPassword)
#Html.PasswordFor(m => m.ConfirmPassword)
</li>
</ol>
<input type="submit" value="Change password" />
</fieldset>
}
Another thing you could do is to add a second identifier to the user table that is not available outside of the application and require that any post that edits data have a one-way hash of this second identifier in it or it will be rejected. This identifier should of course be unique per user (best to do a Guid) and then use a one-way hash algorithm followed by URL encoding.
/Account/Edit/?hash=AKXHAonyrOtruBO%2FVI%2FGr%2FM%2B4ZadjrS3YRt21ILSLntssu23l%2FN6hpUilZM8Hkgn%2Bg%3D%3D
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Edit(Model model, string hash)
{
// Only allow the logged in user to edit their own profile
// TODO: Update the user's database record by using the user variable as the key.
var user = User.Identity.Name;
// After looking up the user, hash the hidden ID field using the same algorithm
// and ensure that the hashes match. If not, throw a 401 error.
// edit model here
}
This may help if you have a record with a sequential ID in the URL and you don't want the user to just change the ID to some other value and post it.
It helps to analyze the Account controller that is generated when you make a brand new MVC project from the VS template to see how it is put together.
With the help of several SO questions, I've figured out how to use two models on the same view, using the tuple form. At the top of my file is this:
#using Project.Models;
#{
ViewBag.Title = "Details";
Layout = "~/Views/Shared/_Layout.cshtml";
#model Tuple<Foo, Bar>
}
For the Foo stuff, it uses jQuery like this:
#Html.DisplayFor(tuple => tuple.Item1.ID)
and works fine. However, for my second model, it isn't displaying info, but is a submission form. Currently, this is what I have:
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "createFoo", #action = "/api/Foo" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="editor-field">
#Html.TextAreaFor(tuple => tuple.Item2.Text)
#Html.ValidationMessageFor(tuple => tuple.Item2.Text)<br />
</div>
<input type="submit" value="Post Response" />
}
Mind you, this is mostly copy paste from other views since I'm new to MVC and it worked fine with other forms. For my FooController, I have this:
public void Post([FromBody] Foo foo)
{
Foo existingFoo = this.fooRepository.GetFoo(foo.ID);
if (existingFoo != null)
{
// throw error
}
else
{
System.Diagnostics.Debug.WriteLine("MESSAGE POSTING: " + foo.Text);
}
}
When submitting from the view, the received foo isn't null (checked in other tests), but the foo.text is empty. I've tried lots of different inputs and such, but I'm just so unfamiliar with the #Html.* functions and ASP.net-MVC in general that I'm not sure where I could be going wrong.
If you need to see my repository code let me know, but I doubt it'd have an effect on this. Thanks in advance for any help.
There are 2 issues I can see with your code above:
The way the Html helper will output your fields and how that feeds into your api post
Not having your ID in the controller.
1: Outputting a field like this (#Html.TextAreaFor(tuple => tuple.Item2.Text)) will give the field the name "Item2.Text". This is an issue as that is what gets passed on the post. You will get form data with Item2.Text:dfsdsgdfg. This won't match when your API controller tries to bind. So, try outputting the text field with a set name:
#Html.TextArea("Text", #Model.Item2.Text)
2: Your Id field is not in the form... thus it won't be sent. Try using a hidden field:
#Html.Hidden("ID", #Model.Item1.ID)
Also, just a clarification, this (#Html.DisplayFor(tuple => tuple.Item1.ID)) is not jQuery.
This very well may end up being a very silly question in a way but basically I have this "form" in a model that gets attached to my View as the form but I haven't been able to actually pass any data do it from the View. It only has two properties: an Id property and a String property. I've been trying to fill the String property with text from a hidden text box on the page with no luck.
Form code:
public class AllocateListForm
{
public int Id { get; set; }
public virtual string HiddenText { get; set; }
}
Relevant View code:
<% using (Html.BeginForm("SaveExit", "User", new { }, FormMethod.Post, new { id = "selectExitPoints" })) { %>
<fieldset>
<input type="hidden" id="HiddenText" />
</fieldset>
<% } %>
There is JQuery behind the scenes that fills HiddenText with text and I can assure you that it is filling. There is also JQuery behind the scenes that performs an Ajax submission and I can promise you that code works as it is used elsewhere in the application without a problem. When I perform the action that submits the form to the server and I go to my controller code that this points to, I have a breakpoint set so I can go into the console and check if the HiddenText field on the form has any data it is null. Can anybody point me in the right direction?
If you assign the input's name to be "HiddenText" the model binder should pick it up. I'm assuming that your controller action accepts an AllocateListForm as a parameter.
<input type="hidden" name="HiddenText" id="HiddenText" />
You can also use Html Helpers like so:
#Html.HiddenFor(model => model.HiddenText, new { id = "HiddenText" })
EDIT: Add an AllocateListForm as a property of your main model and then change the helper to be #Html.HiddenFor(model => model.MyAllocateListForm.HiddenText)
This should do the trick, if you want to do it the Razor-way.
#Html.HiddenFor(model => model.HiddenText);
First off I am new to MVC, I was a webforms guy...
I want to show my login control on my home/index page, but no matter what I do I run in to errors. I have tried various techniques and have gotten many errors with each technique, so I wont list them all here.
Right now I have it set that if Request.IsAuthenticated then show the username else show the login form. But the form is obviously looking at the home controller and nothing happens when I submit the login.
Any advice would be much appreciated. I have been dabbling for days on this.
Thanks :)
Here is the Code:
'#{
ViewBag.Title = "MyApp";
}
<p>Code for main Index Page here</p>
#model Application.Models.LoginModel
#{
ViewBag.Title = "Log in";
}
#if (Request.IsAuthenticated) {
<text>
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", routeValues: null, htmlAttributes: new { #class = "username", title = "Manage" })!
#using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm" })) {
#Html.AntiForgeryToken()
Log off
}
</text>
} else {
<section id="loginForm">
<h2>Use a local account to log in.</h2>
#using (Html.BeginForm(new { ReturnUrl = "RedirectToAction" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Log in Form</legend>
<ol>
<li>
#Html.LabelFor(m => m.UserName)
#Html.TextBoxFor(m => m.UserName)
#Html.ValidationMessageFor(m => m.UserName)
</li>
<li>
#Html.LabelFor(m => m.Password)
#Html.PasswordFor(m => m.Password)
#Html.ValidationMessageFor(m => m.Password)
</li>
<li>
#Html.CheckBoxFor(m => m.RememberMe)
#Html.LabelFor(m => m.RememberMe, new { #class = "checkbox" })
</li>
</ol>
<input type="submit" value="Log in" />
</fieldset>
<p>
#Html.ActionLink("Register", "Register", "Account") if you don't have an account.
</p>
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
</section>
}
'
you have to modify your form like this:
#using (Html.BeginForm("Login","Account",FormMethod.Post, new { ReturnUrl = ViewBag.ReturnUrl }))
the key is the "Login","Account" part, that tells the form to post the data to the login action of the account controller instead of the home controller. I fought with the same thing for a few hours then figured it out.
if the data does not post to the account controller (the controller that actually validates the username and password and logs the user in) it will never try to log them in.
reason is :
by default in the routeconfig.cs class sends any links or forms that do not specify an action and controller send all stuff to the Index action of the Home controller.
Hope this helps
You should use a Child Action:
In your controller:
[ChildActionOnly]
public ActionResult LoginForm()
{
return View("_LoginFormPartialViewHere");
}
Then in your homepage view:
#{ Html.RenderAction("LoginForm"); }
In your partial view for your login form, you can strongly-type the view for your login view model, but make sure to specify a different post action for your form than the default "postback" model. This action will handle the login and only the login (so not your homepage action).
Your problem here is that you are attempting to write code without actually understanding what it does. Without understanding what it does, you are simply left randomly changing things hoping to find something that works. This is a very poor way to write software.
Your problem is rooted in the fact that you don't understand that MVC is merely generating HTML, and without understanding what that HTML is supposed to be doing, you have little hope of randomly figuring this out.
First, MVC has no concept of a "login control". They're just HTML form fields, and they sit within an HTML form element. Those form fields are posted to your controller using standard HTML, which means you have to ensure your form action method is correct, and that the action url is correct.
Secondly, when those form fields are posted, you have to have an action method that will receive the post. If that action method is not the same as the action method used in the GET, then you will have to tell the BeginForm() helper where to post to.
In MVC, you can't think like Webforms and just assume everything gets done for you. You have to do everything yourself, and make sure every link, every action, every selector, every bit of javascript, etc.. is all correct. This is more work than WebForms, but it's also more powerful and more efficient.
I don't see this problem too often but I've got a .cshtml that uses a layout. In the layout I've got:
#using (Html.BeginForm(null, null, FormMethod.Post, new { #class = "someCssClass", #id = "UserForm" }))
{
...rest of the code
}
My main .cshtml using this layout has the model defined at the top as we always do:
#model CarViewModel
#{
Layout = "~/Views/Shared/_CarLayout.cshtml";
}
When It gets back to my action method, I get nulls for all values of the model:
public ActionResult Cars(CarViewModel model)
{
carBL.RemoveCars(model.CarIds, model.DealerId);
...
}
Not sure what I need to do here and why this is happening. Usually I just get it back successfully via autobind. It seems to me when the model is used via RAzor in the markup- that gets posted back fine with the returned ViewModel but if I'm not using those fields, it doesn't...so I assume that's how that works and if I don't use them in mark-up I need to send them back as hidden values then to force the persistence since I am not using x fields from the ViewModel (Which would have automatically persisted those fields if I had used them in the form)?
If the values are not bound to a form field, they will come back null.
in the form use the below for things like ID fields.
#Html.HiddenFor(x => x...)
A quick test, to see if the form is being posted correctly would be to modify the signature of your action:
public ActionResult Cars(FormCollection form)
{
...
}
If form is not populated then you have an issue with the form post. As a side, note you could accomplish this when reviewing the post data of the form with a tool like FireBug, Chrome Dev tools or Fiddler if you prefer.
If the form is posting correctly, then I you should check to make sure the name's of the input fields on the form align with the names of the CarViewModel you are expecting.
Not sure if this has been resolved yet, but this is how I do it (partial code):
#model MyProject.ViewModels.MyViewModel
#using (Html.BeginForm())
{
<table>
<tr>
<td>First Name:</td>
<td>#Html.TextBoxFor(x => x.FirstName, new { maxlength = "50" })
#Html.ValidationMessageFor(x => x.FirstName)
</td>
</tr>
</table>
<button id="btnSave" type="submit">Save</button>
<button id="btnCancel" type="button">Cancel</button>
}
Then my action method to handle the HTTP post request:
[HttpPost]
public ActionResult Create(MyViewModel viewModel)
{
// Check for null on viewModel
// Do what needs to be done
}
Doing it this way should not let you loose your values filled in on the form/view.