I have a website that is doing the auth as follows:
you click a Login button that triggers a javascript function which shows an existing form marked as hidden. This means that the form cannot be submitted directly with a POST webrequest (this worked on another site, but not on this one)
when submitting the form another javascript function is triggered that calls an AJAX call to /ajax/login
Javascript functions:
ajax: function() {
if (!Login.blocked) {
Login.blocked = true;
var fields = Login.getFormData();
ajax('/ajax/login', Login.getResponse, Login.packData(fields));
}
packData: function(obj) {
var data = [];
for (key in obj) {
data.push(key + '=' + obj[key]);
}
Tampered data:
Host=*********
User-Agent=Mozilla/5.0 (Windows NT 6.3; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept=*/*
Accept-Language=en-US,en;q=0.5
Accept-Encoding=gzip, deflate, br
X-Requested-With=XMLHttpRequest
Content-Type=application/x-www-form-urlencoded
Referer=https://*********/
Content-Length=105
Cookie=utc=0918a5d99a7; _ga=GA1.2.1988660761.1492530049; __gads=ID=4db40c28b6af8254:T=1492530048:S=ALNI_MYCC-8HnoEW3ecRExG9wg9AA9b9mw; XSRF-TOKEN=************************************; _gid=GA1.2.41413306.1493735447; surveymonkey=true; SRV=s2; acceptcookies=true
Connection=keep-alive
POSTDATA=_token=*********&email=*********%40gmail.com&password=*********&=Authentication
The token that is being sent is part of the login form and it is generated automatically when the page is accessed:
input name="_token" type="hidden"
value="rpt8UZiKGq8ARRvDVEZslU9erqT7Xzmv35F2QjxN"
I'm using cURL to test and I've been through like 100 tries with no result. Doing "--dump-header cookie.txt" to check the result each time.
Examples:
curl --dump-header cookie.txt -d "_token=4l8bOObleRsquW******&email=******%40gmail.com&password=******&=Authentication" -H "Contenty-Type: application/x-www-form-urlencoded" https://*******/ajax/login/
-H "Contenty-Type: application/x-www-form-urlencoded"
-H "Accept:*/*" -H "X-Requested-With:XMLHttpRequest"
-H "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0"
-H "Accept: application/json"
Also tried the format {"email":"******#gmail.com","password":"******"} for data.
Any ideas?
#for anyone wondering
The token needed was actually in a httponly cookie so even if I would've succeeded I had no guarantee that curl was able to capture that cookie.
To work-around this issue I have done IE automation in powershell using COM InternetExplorer.Application, logged in to the site, extracted the token from the cookie file on disk, constructed a cookie object in powershell, added it to a websession and invoking webrequests/restrequests using that websession (all in powershell).
I never close IE (visible = $false), refresh it every few minutes (even that the page has a keep alive) and renew the token from disk.
-Dizzy
Related
I'm currently working on site that uses various Ajax-requests to save, load and autocomplete data. It is build using C#, MVC and JQuery. All actions on the MVC controllers require the users to be authorized, and we use IdentityServer3 for authentication. It was installed using NuGet, and the current version is 2.3.0.
When I open the page and push buttons, everything is working just fine. The problem seem to occur when a certain session expires. If I stay idle for a while, and try to use an Ajax-function, it generates the following error:
XMLHttpRequest cannot load https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email+phone+roles [...]. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:12345' is therefore not allowed access.
From what I know about Ajax, the problem itself is pretty simple. The MVC site has lost track of the current session, and it is asking the client to authenticate again. The response I get from the Ajax-request is a "302 Found", with a Location-header that points to our IdentityServer. The IdentityServer happens to be on another domain, and while this works fine when you are performing regular HTTP-requests, it does not work particularly well for Ajax-requests. The "Same Origin Policy" is straight up blocking the Ajax-function from authenticating. If I refresh the page, I will be redirected to the IdentityServer and authenticate normally. Things will then go back to normal for a few minutes.
The solution is probably to add an extra header in the response message from the IdentityServer, that explicitly states that cross-origin requests are allowed for this service.
I am currently not getting this header from the IdentityServer (checked in Fiddler).
According to the docs, it should be enabled by default. I have checked that we have indeed enabled CORS this way:
factory.CorsPolicyService = new Registration<ICorsPolicyService>(new DefaultCorsPolicyService { AllowAll = true });
This is one of my clients:
new Client
{
Enabled = true,
ClientName = "Foo",
ClientId = "Bar",
ClientSecrets = new List<Secret>
{
new Secret("Cosmic")
},
Flow = Flows.Implicit,
RequireConsent = false,
AllowRememberConsent = true,
AccessTokenType = AccessTokenType.Jwt,
PostLogoutRedirectUris = new List<string>
{
"http://localhost:12345/",
"https://my.domain.com"
},
RedirectUris = new List<string>
{
"http://localhost:12345/",
"https://my.domain.com"
},
AllowAccessToAllScopes = true
}
These settings do not work. I am noticing that I have an extra forward slash in the URIs here, but if I remove them, I get the default IdentityServer-error that states that the client is not authorized (wrong URI). If I deploy the site (instead of running a localhost debug), I use the domain name without a trailing slash, and I get the exact same behaviour as I do in debug. I do notice that there is no trailing slash in the error message above, and I figured this could be the problem until I saw the same thing in the deployed version of the site.
I also made my own policy provider, like this:
public class MyCorsPolicyService : ICorsPolicyService
{
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(true);
}
}
... and I plugged it into the IdentityServerServiceFactory like this:
factory.CorsPolicyService = new Registration<ICorsPolicyService>(new MyCorsPolicyService());
The idea is for it to return true regardless of origin. This did not work either; exactly the same results as before.
I've read about a dozen other threads on this particular subject, but I'm getting nowhere. To my knowledge, we are not doing anything unusual when it comes to the setup of the different sites. It's all pretty much out-of-the-box. Any advice?
----- UPDATE -----
The problem persists. I have now tried some fresh tactics. I read somewhere that cookie authentication was bad for Ajax-requests, and that I should be using bearer tokens instead. I set this up in Ajax like this:
$(function () {
$(document).ajaxSend(function (event, request, settings) {
console.log("Setting bearer token.");
request.setRequestHeader("Authorization", "Bearer " + $bearerToken);
});
});
Both the console in Chrome and Fiddler confirms that the token is indeed present and sent by JQuery. The token I use comes from the access_token-property on claims principal object from HttpContext.GetOwinContext().Authentication.User.
This didn't do much. I still get a 302-response from the server, and Fiddler reveals that the token is not sent on the following Ajax-request (which is a GET-request) to the IdentityServer.
From there, I read this thread:
Handling CORS Preflight requests to ASP.NET MVC actions
I tried to put this code in to the startup.cs of the IdentityServer, but there does not appear to be a "preflight" request going in. All I see in Fiddler is this (from the beginning):
1 - The initial Ajax-request from the client to the MVC controller:
POST http://localhost:12345/my/url HTTP/1.1
Host: localhost:12345
Connection: keep-alive
Content-Length: pretty long
Authorization: Bearer <insert long token here>
Origin: http://localhost:12345
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Referer: http://localhost:12345/my/url
Accept-Encoding: gzip, deflate
Accept-Language: nb-NO,nb;q=0.8,no;q=0.6,nn;q=0.4,en-US;q=0.2,en;q=0.2
Cookie: OpenIdConnect.nonce.<insert 30 000 lbs of hashed text here>
param=fish&morestuff=salmon&crossDomain=true
2 - The redirect response from the MVC controller:
HTTP/1.1 302 Found
Cache-Control: private
Location: https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345%2f&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email [...]
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
Set-Cookie: OpenIdConnect.nonce.<lots of hashed text>
X-SourceFiles: <more hashed text>
X-Powered-By: ASP.NET
Date: Fri, 15 Jan 2016 12:23:08 GMT
Content-Length: 0
3 - The Ajax-request to the IdentityServer:
GET https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345%2f&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email [...]
Host: identityserver.domain.com
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://localhost:12345
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://localhost:12345/my/url
Accept-Encoding: gzip, deflate, sdch
Accept-Language: nb-NO,nb;q=0.8,no;q=0.6,nn;q=0.4,en-US;q=0.2,en;q=0.2
4 - The response from IdentityServer3
HTTP/1.1 302 Found
Content-Length: 0
Location: https://identityserver.domain.com/login?signin=<some hexadecimal id>
Server: Microsoft-IIS/8.5
Set-Cookie: SignInMessage.<many, many, many hashed bytes>; path=/; secure; HttpOnly
X-Powered-By: ASP.NET
Date: Fri, 15 Jan 2016 12:23:11 GMT
5 - The meltdown of Chrome
XMLHttpRequest cannot load https://identityserver.domain.com/connect/authorize?client_id=Bar&blahblahblah. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:12345' is therefore not allowed access.
I was having a similar issue using OWIN Middleware for OpenIDConnect with a different identity provider. However, the behavior occurred after 1 hour instead of 5 minutes. The solution was to check if the request was an AJAX request, and if so, force it to return 401 instead of 302. Here is the code that performed this:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = oktaOAuthClientId,
Authority = oidcAuthority,
RedirectUri = oidcRedirectUri,
ResponseType = oidcResponseType,
Scope = oauthScopes,
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = true,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
//...
},
RedirectToIdentityProvider = n => //token expired!
{
if (IsAjaxRequest(n.Request))
{
n.Response.StatusCode = 401;//for web api only!
n.Response.Headers.Remove("Set-Cookie");
n.State = NotificationResultState.HandledResponse;
}
return Task.CompletedTask;
},
}
});
Then, I used an Angular interceptor to detect a statusCode of 401, and redirected to the authentication page.
I came across this problem as well and UseTokenLifetime = false was not solving the problem since you loose the token validity on STS.
When I tried to reach the authorized api method, I still got 401 even if I was valid on Owin.
The solution I found is keeping UseTokenLifetime = true as default but to write a global ajax error handler (or angular http interceptor) something like this:
$.ajaxSetup({
global: true,
error: function(xhr, status, err) {
if (xhr.status == -1) {
alert("You were idle too long, redirecting to STS") //or something like that
window.location.reload();
}
}});
to trigger the authentication workflow.
I had this issue recently, it was caused by the header X-Requested-With being sent with the AJAX request. Removing this header or intercepting it and handling it with a 401 will put you on the right track.
If you don't have this header, the issue is most likely being caused by a different header triggering the Access-Control-Allow-Origin response.
As you found, nothing you do in Identity Server regarding CORS will solve this.
As it turns out, the problem was in the client configuration in MVC. I was missing the UseTokenLifetime property, which should have been set to false.
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "Bar",
Scope = "openid profile email phone roles",
UseTokenLifetime = false,
SignInAsAuthenticationType = "Cookies"
[...]
For some reason, IdentityServer sets all these cookies to expire within 5 minutes of them being distributed. This particular setting will override IdentityServer's tiny expiration time, and instead use aprox. 10 hours, or whatever the default is in your client application.
One could say that this is good enough for solving the problem. It will however inevitably return if the user decides to spend 10 hours idling on the site, clicking nothing but Ajax-buttons.
https://github.com/IdentityServer/IdentityServer3/issues/2424
Assumptions:
.NET Framework 4.8 WebForms
OWIN-based auth lib i.e. Microsoft.Owin.Security.OpenIdConnect v4.2.2.0
UseOpenIdConnectAuthentication() with Azure AD endpoint
UseTokenLifetime=true
In Layout.Master:
$.ajaxSetup({
global: true,
error: function (xhr, status, err) {
if (xhr.status == 401) {
window.location.reload();
}
}
});
In startup.cs:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
...
Notifications = new OpenIdConnectAuthenticationNotifications()
{
...
RedirectToIdentityProvider = RedirectToIdentityProvider
}
});
...
public Task RedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
if (IsAjaxRequest(context.Request))
{
context.Response.StatusCode = 401;
context.Response.Headers.Remove("Set-Cookie");
context.State = NotificationResultState.HandledResponse;
}
}
public bool IsAjaxRequest(this IOwinRequest request)
{
if (request == null)
{
throw new ArgumentNullException("Woopsie!");
}
var context = HttpContext.Current;
var isCallbackRequest = false;
if (context != null && context.CurrentHandler != null && context.CurrentHandler is System.Web.UI.Page page)
{
isCallbackRequest = page.IsCallback;
}
return isCallbackRequest || (request.Cookies["X-Requested-With"] == "XMLHttpRequest") || (request.Headers["X-Requested-With"] == "XMLHttpRequest");
}
For some reason this:
return jquery.ajax('my url', {
crossDomain : true
, data : JSON.stringify({"brand": self.current})
, type : 'POST'
}).success(function(data){
scope.results = data;
});
and/or this:
curl -X POST -H "Content-Type: application/json" -d '{"brand":"target"}' myUrl
work fine, but this:
var req = {
method: "POST"
, url : "my url"
, data : JSON.stringify({"brand": self.current})
};
return $http(req).
success(function(data){
scope.results = data;
});
fails miserably with
"OPTIONS my url (anonymous function) # angular.js:9866sendReq # angular.js:9667$get.serverRequest # angular.js:9383processQueue # angular.js:13248(anonymous function) # angular.js:13264$get.Scope.$eval # angular.js:14466$get.Scope.$digest # angular.js:14282$get.Scope.$apply # angular.js:14571(anonymous function) # angular.js:21571jQuery.event.dispatch # jquery.js:4430jQuery.event.add.elemData.handle # jquery.js:4116
(index):1 XMLHttpRequest cannot load my url. No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://localhost:5000' is therefore not allowed access. The response had HTTP status code 404."
They're the same url. Wtf.
I have a sneaking suspicion that the "crossDomain : true" option in jquery is why the jquery one works, but if that's the case, then the question is:
how do I do that with angular?
-- When using jquery's default ajax method, the scope isn't updating with the results, but i know the data is being assigned because i'm logging it out, and if i submit the request again, the scope does update with the second value.
Second question- why isn't my view updating with the results?
update:
The reason this is failing has nothing to do with the response I'm getting back from the server, the problem is that Angular is transforming this POST request into an OPTIONS request:
(taken from google chromes' xhr tool:)
Remote Address: the remote address
Request URL:the request endpoint
Request Method:OPTIONS <-------------
Status Code:404 Not Found
Further inspection reveals:
OPTIONS /my url HTTP/1.1 <--------------
Host: my urls host
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: POST
Origin: http://localhost:5000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36
Access-Control-Request-Headers: accept, charset, content-type
Accept: */*
Referer: http://localhost:5000/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
which is not what it should be doing because I'm specifically saying in the req object i'm passing to $http that this is a POST request.
...
So how do I make angular... NOT do that?
also- why is it doing that?
When you do a cross-origin request from your browser, all browsers hit the URL (provided in AJAX call) to confirm if the cross-origin request is available or not which is known as preflight request. https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
So, your server's endpoint must allow the preflight request in order to make this call work by setting some response headers like (an example in Groovy):
response.setHeader "Access-Control-Allow-Headers", "Content-Type"
response.setHeader "Access-Control-Allow-Methods", "POST,DELETE,PUT"
response.setHeader "Access-Control-Allow-Origin", "*"
I want to sent a Symfony form through AJAX (AngularJS).
However, even if data is clearly sent, the form is said by Symfony to be "not submitted" (no error actually, but some debug show that isSubmitted returns false)
Here is a test on FOSUserBundle registration form.
EDIT: although pictures appear small, they are sufficiently resolved so that they are perfectly readable if displayed at their full size.
Here are the logs when I use a standard submission from an HTTP form:
Here are the logs when I use AJAX submission:
I have disabled CSRF token
The method I use from AJAX is "post" so it should be ok since it is the default Symfony expects (and FOSUserBundle does not override that default AFAIK).
Other forms (outside of FOSUserBundle scope) are correctly sent with AJAX
Would you have any idea on the matter? Any pointer? Or any other log I could check?
EDIT:
I use the default FOSUserBundle registration controller.
I almost use the default FOSUserBundle registration form, but I have added a (currently) empty child form which I'll need it further in the development.
namespace NONG\SecurityBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class UserRegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {}
public function getParent() {
return 'fos_user_registration';
}
public function getName() {
return 'nong_user_registration';
}
}
Headers and data sent with the HTML form (NB. added a dot before star in Accept so that the coloration stays correct):
POST /server/web/testfosuser/register/ HTTP/1.1
Host: mywebsite.com
Connection: keep-alive
Content-Length: 229
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/.*;q=0.8
Origin: http://mywebsite.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://mywebsite.com/server/web/testfosuser/register/
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: PHPSESSID=xxxxxxxxxxxxxxxxxxxxxxxxx
fos_user_registration_form%5Bemail%5D=bidon%40bidon.com&
fos_user_registration_form%5Busername%5D=bidon&
fos_user_registration_form%5BplainPassword%5D%5Bfirst%5D=bidon&
fos_user_registration_form%5BplainPassword%5D%5Bsecond%5D=bidon
Headers sent with the AJAX request: (NB. added a dot before star in Accept so that the coloration stays correct):
POST /server/web/testfosuser/register HTTP/1.1
Host: mywebsite.com
Connection: keep-alive
Content-Length: 229
Accept: application/json, text/plain, */.*
Origin: http://mywebsite.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36
Content-Type: application/json;charset=UTF-8 application/x-www-form-urlencoded; charset=UTF-8
Referer: http://mywebsite.com/client/
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: PHPSESSID=xxxxxxxxxxxxxxxxxxxxxxxxx
fos_user_registration_form%5Bemail%5D=bidonqdsfqsdf%40qsdf&
fos_user_registration_form%5Busername%5D=charles222&
fos_user_registration_form%5BplainPassword%5D%5Bfirst%5D=bidon&
fos_user_registration_form%5BplainPassword%5D%5Bsecond%5D=bidon
I had similar problems and i changed the following inside of the controller:
$user = new User();
$form = $this->createForm(
new UserRegistrationType(),
$user,
array(
'attr' => array('novalidate' => 'novalidate'),
'action' => $this->generateUrl('user_registration')
)
);
Turning of the html5 error bubbles and setting the action URL directly worked for me. If this is not working, show your JS code.
I finally managed to find the correction:
The correct URL is ...../register/ with a final / that I forgot when I did the AJAX request.
However, I'm not sure what are the mechanisms at stake here. The deepest I could go debuging (when using the wrong URL) was in HttpFoundationRequestHandler, where the $request->getMethod() call returned GET (instead of POST), thus not submitting the form.
I tried to post username and password to api, but looks like it doesnt work as simple as jquery post. I keep geting this 400 error.
Code:
$http({
method: 'POST',
url: apiLink + '/general/dologin.json',
data: {"username":"someuser","password": "somepass"}
}).success(function(response) {
console.log(response)
}).error(function(response){
console.log(response)
});
But if I add this line:
$http.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";
and change data to:
data: "username=someuser&password=somepass"
it works. But the thing is, that I have to use json.
And detailed informations from Google Chrome:
Request URL:http://coldbox.abak.si:8080/general/dologin.json
Request Method:POST
Status Code:400 Bad Request
Request Headersview source
Accept:application/json, text/plain, */*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en,sl;q=0.8,en-GB;q=0.6
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:57
Content-Type:application/x-www-form-urlencoded
Host:coldbox.abak.si:8080
Origin:http://localhost:8888
Referer:http://localhost:8888/
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36
Form Dataview sourceview URL encoded
{"username":"someuser","password":"somepass"}:
Response Headersview source
Access-Control-Allow-Origin:*
Connection:close
Content-Length:49
Content-Type:application/json;charset=utf-8
Date:Wed, 02 Apr 2014 07:50:00 GMT
Server:Apache-Coyote/1.1
Set-Cookie:cfid=b5bbcbe2-e2df-4eef-923f-d7d13e5aea42;Path=/;Expires=Thu, 31-Mar-2044 15:41:30 GMT;HTTPOnly
Set-Cookie:cftoken=0;Path=/;Expires=Thu, 31-Mar-2044 15:41:30 GMT;HTTPOnly
I'm betting it's a CORS issue if your angular app isn't on the exact same domain as the server to which you're posting your JSON.
See this answer for details: AngularJS performs an OPTIONS HTTP request for a cross-origin resource
Try
data: {username:"someuser",password: "somepass"}
without the quotes around the username and password and see if that makes a difference.
You would have to transform the data with a JSON.stringify when you assign that to the data
I'm trying to serialize a form and pass it into a controller as a model. What I'm doing I've done in the past, but it's not working for some reason, so I suspect I am missing something stupid. Perhaps you can find it.
In my controller I have a method:
[HttpPost]
public ActionResult AddShippingLocation(PricingRequestModel model)
{
model.ShippingLocationsModel.Add(new ShippingLocationsModel());
return PartialView("shiplocationPartial", model);
}
In my view I have a script that looks like this:
function AddShippingLocation() {
$.ajax({
data: { model: $('#shippinginfoform').serialize() },
type: "POST",
url: "/PricingRequest/AddShippingLocation",
success: function (response) {
$('#shiplocation-wrapper').html(response);
}
})
}
This is called from a link that gets clicked. Also in the view I have a form that uses this:
#using (Html.BeginForm("AddShippingLocation", "PricingRequest", FormMethod.Post, new { id = "shippinginfoform" }))
{
I put the Addshippinglocation in as the method because I wanted to test to see if the model would be serialized using the built in helper. The model gets passed in properly using Html.BeginForm, it also gets passed in properly when using Ajax.BeginForm. When using jquery.serialize, though, it doesn't get passed in properly. On a side note, I'm using MVC 4. Any ideas? Thanks.
EDIT: Here's the request headers. The top one is a successful post of the model to the method, the bottom is the .serialize() that passes in a null model. I examined the post strings and the are the exact same.
Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Connection keep-alive
Cookie .ASPXAUTH=9F06BF2A7D03211E0D2ACEC26D7A568754C89F8A265EE61D9F8010BB8DF1D97670212F1E853FDE960E87AAC5DC7D364A251F670560448482517DA7C072864F62AC0C5C3E1EE8D375ACC1EA8F4D63CFC3C1DD28BBDCAC945155D15289DCDDA3B540756C0609611C13A438B5FF4CA747219290AFB51F58B8AD35AE40C01D3AFAF8B32ADD7E200148B1E1646400CAC0F116; ASP.NET_SessionId=v3qwt02dn1pd13posl5zzk3n
Host localhost:2652
Referer http://localhost:2652/PricingRequest/custinfo
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0
Request Headers From Upload Stream
Content-Length 471
Content-Type application/x-www-form-urlencoded
Accept */*
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Cache-Control no-cache
Connection keep-alive
Content-Length 555
Content-Type application/x-www-form-urlencoded; charset=UTF-8
Cookie .ASPXAUTH=9F06BF2A7D03211E0D2ACEC26D7A568754C89F8A265EE61D9F8010BB8DF1D97670212F1E853FDE960E87AAC5DC7D364A251F670560448482517DA7C072864F62AC0C5C3E1EE8D375ACC1EA8F4D63CFC3C1DD28BBDCAC945155D15289DCDDA3B540756C0609611C13A438B5FF4CA747219290AFB51F58B8AD35AE40C01D3AFAF8B32ADD7E200148B1E1646400CAC0F116; ASP.NET_SessionId=v3qwt02dn1pd13posl5zzk3n
Host localhost:2652
Pragma no-cache
Referer http://localhost:2652/PricingRequest/custinfo
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0
X-Requested-With XMLHttpRequest
The request bodies are the same? Somehow, I'm doubtful.
Your ajax request body is going to have
model=....
where .... is your form serialized, which url encodes the inputs, and then the serialization itself is urlencoded. You're urlencoding twice with your ajax request. That doesn't happen with normal form posts, and urlencoding is not idempotent with respect to equal signs.
Try
data: $('#shippinginfoform').serialize(),
If the shippinginfoform form is the same form that's posted, I believe that should post the same data (well, generally: there may be some corner cases with values associated with submit buttons and such.).
I'll admit that there's some chance that I'm wrong, in which case I'll promptly delete this answer.