Provide AntiForgery Token with System.Net.Http.HttpClient and MVC

5.5k Views Asked by At

I have a WPF (could be any winform I guess) app that tries to login to a standard MVC 5 website using a HttpClient.

Normally I can login successfully with a call to PostAsync() where I provide the UserName and Password params in a HttpContent!

However, when I add the [ValidateAntiForgeryToken] to my controller's Login (POST) action, the PostAsync() call fails with Internal Server Error.

I have tried collecting the "__RequestVerificationToken" from a simple GET request and sending it with my POST request by adding it to the POST params, the Header of the request or the HttpHandler's CookieContainer (or any combination of the three) but still I get error 500 from the server.

I know it can be done with HttpWebRequests (apparently) but I don't know what I'm missing when using a HttpClient. I also don't know what exactly went wrong on the server side.. or how to check that since the code never reaches my controller method.

Did someone else try this by any chance?

EDIT 1:

I'm adding the raw data sent by the browser for both GET and POST:

GET http://localhost:57457/Account/Login HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Referer: http://localhost:57457/Account/Login
Accept-Language: en-US
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
DNT: 1
Host: localhost:57457
Cookie: NavigationTreeViewState=%5b%7b%27N0_1%27%3a%27T%27%2c%27N0%27%3a%27T%27%7d%2c%27N0_1_2%27%2c%7b%7d%5d; style=default; __RequestVerificationToken=Bak42Ga5sHJitYlmut6OgvmqXNmP7kKQRNaMSsLMAUh86iHGGmz5pnNfz_soKu46Wax9sG23arPOTnSh1bvaWyWqQ9NH4GJxFmendW8VFTg1

RESPONSE:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
Server: Microsoft-IIS/8.0
X-AspNetMvc-Version: 5.2
X-Frame-Options: SAMEORIGIN
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcRUJTLkNvZGVcUHJvamVjdHNcQ1ZSUE9TX1dlYlNpdGVcQ1ZSUE9TX1dlYlNpdGVcQWNjb3VudFxMb2dpbg==?=
X-Powered-By: ASP.NET
Date: Thu, 04 Dec 2014 10:00:00 GMT
Content-Length: 1734
[View page content]

POST http://localhost:57457/Account/Login HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Referer: http://localhost:57457/Account/Login
Accept-Language: en-US
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Content-Length: 180
DNT: 1
Host: localhost:57457
Pragma: no-cache
Cookie: NavigationTreeViewState=%5b%7b%27N0_1%27%3a%27T%27%2c%27N0%27%3a%27T%27%7d%2c%27N0_1_2%27%2c%7b%7d%5d; style=default; __RequestVerificationToken=Bak42Ga5sHJitYlmut6OgvmqXNmP7kKQRNaMSsLMAUh86iHGGmz5pnNfz_soKu46Wax9sG23arPOTnSh1bvaWyWqQ9NH4GJxFmendW8VFTg1

__RequestVerificationToken=Bak42Ga5sHJitYlmut6OgvmqXNmP7kKQRNaMSsLMAUh86iHGGmz5pnNfz_soKu46Wax9sG23arPOTnSh1bvaWyWqQ9NH4GJxFmendW8VFTg1&UserName=test&Password=test

RESPONSE:
HTTP/1.1 400 Bad request (user/password for testing purposes only)
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/8.0
X-AspNetMvc-Version: 5.2
X-Frame-Options: SAMEORIGIN
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcRUJTLkNvZGVcUHJvamVjdHNcQ1ZSUE9TX1dlYlNpdGVcQ1ZSUE9TX1dlYlNpdGVcQWNjb3VudFxMb2dpbg==?=
X-Powered-By: ASP.NET
Date: Thu, 04 Dec 2014 10:00:00 GMT
Content-Length: 4434
[View page content]

EDIT 2:

This is what my app sends for GET and POST:

GET http://localhost:57457/Account/Login HTTP/1.1
Host: localhost:57457
Connection: Keep-Alive

POST http://localhost:57457/Account/Login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost:57457
Cookie: __RequestVerificationToken=df9nBSP_J1IiLrv84RwrkmvbYBrnH4iqv97wRvz6HMPLWBhgI4XzGeAFcschovHwD8mTtHU6xrmVxz1Ku96_BaoB79le_vLTcrgGemU4gjc1
Content-Length: 163
Expect: 100-continue

__RequestVerificationToken=df9nBSP_J1IiLrv84RwrkmvbYBrnH4iqv97wRvz6HMPLWBhgI4XzGeAFcschovHwD8mTtHU6xrmVxz1Ku96_BaoB79le_vLTcrgGemU4gjc1&UserName=test&Password=test

And finally this is the error:

[HttpAntiForgeryException (0x80004005): Validation of the provided anti-forgery token failed. The cookie "__RequestVerificationToken" and the form field "__RequestVerificationToken" were swapped.]

Thanks!

3

There are 3 best solutions below

4
On BEST ANSWER

You d probably need to include aspnet session id cookie with your requests

EDIT: OK ur right, it is not the session id, but you need two token to send back to your post action.

I think what you re doing wrong is using same value for both tokens, but they should be different, altho name of both tokens is __RequestVerificationToken. Token grabbed from cookie should be send back as cookie and token grabbed from form field goes back as form field.

4
On

It's because you're missing the anti-forgery token from HtmlHelper.AntiForgeryToken() in your POST from your application.

You'll need to load a page from your WPF application with HtmlHelper.AntiForgeryToken() on the view. Then take the value of the hidden input element with the name __RequestVerificationToken and attach it to your login POST request to the server.

0
On

I just had to do figure this out myself. Below is the code that I wrote that works. You'll need to modify some variables that make sense for your application including the web client's .BaseAddress, the path to your login page, and the shape of the model that you POST to successfully log in.

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace **FIXME**;

public class WebClient
{
    private readonly CookieContainer _cookieContainer;
    private readonly HttpClient _client;
    private readonly string _loginPath;

    public WebClient(Uri baseAddress, string loginPath)
    {
        _cookieContainer = new CookieContainer();
        _client = new HttpClient(new HttpClientHandler { CookieContainer = _cookieContainer }) { BaseAddress = baseAddress };
        _loginPath = loginPath;
    }

    public async Task<string> GetAntiforgeryToken()
    {
        var response = await _client.GetAsync(_loginPath);
        var content = await response.Content.ReadAsStringAsync();

        var match = Regex.Match(content, "name=\"__RequestVerificationToken\" type=\"hidden\" value=\"(.*?)\"");

        return match.Success ? match.Groups[1].Value : throw new Exception("Unable to get anti-forgery token");
    }

    public async Task<HttpResponseMessage> LogIn(string username, string password, string antiforgeryToken)
    {
        _client.DefaultRequestHeaders.Accept.Clear();
        _client.DefaultRequestHeaders.Add("__RequestVerificationToken", antiforgeryToken);

        var content = new FormUrlEncodedContent
        (
            new Dictionary<string, string>
            {
                { "Username", username },
                { "Password", password },
                { "__RequestVerificationToken", antiforgeryToken }
            }
        );

        return await _client.PostAsync(_loginPath, content);
    }

    public CookieContainer CookieContainer => _cookieContainer;

    public async Task<HttpResponseMessage> NavigateTo(string uri)
    {
        return await _client.GetAsync(uri);
    }

    public Cookie? GetCookie(string cookieName)
    {
        return _cookieContainer.GetCookies(_client.BaseAddress).Cast<Cookie>().SingleOrDefault(c => c.Name == cookieName);
    }
}

Usage:

var baseAddress = new Uri("**FIXME**");
var loginPath = "**FIXME**"; 
var username = "**FIXME**";
var password = "**FIXME**";

var webClient = new WebClient(baseAddress, loginPath);
var antiforgeryToken = await webClient.GetAntiforgeryToken();
var loginResponse = await webClient.LogIn(username, password, antiforgeryToken);