Fetch, set-cookies and csrf - javascript

I m using Isomorphic fetch in my application and I m having some troubles dealing with CSRF.
Actually, I m having a backend that sends me a CSRF-TOKEN in set-cookies property :
I have read somewhere that it's not possible, or it's a bad practice to access this kind of cookies directly inside of my code.
This way, I tried to make something using the credentials property of fetch request :
const headers = new Headers({
'Content-Type': 'x-www-form-urlencoded'
});
return this.fetcher(url, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
email: 'mail#mail.fr',
password: 'password'
})
});
This way, I m able to send my CSRF cookie back to my server to serve my need (it's a different one, because it s not the same request) :
My problem
My problem is that my backend needs to receive a x-csrf-token header and so I can't set it to my POST request.
What I need
How can I do to put the value of set-cookies: CSRF-TOKEN into the next request x-csrf-token header ?

It looks like in your scenario you are supposed to read from CSRF-TOKEN cookie. Otherwise it would be marked HttpOnly as JSESSIONID. The later means you cannot access it from the web page but merely send back to server automatically.
In general there is nothing wrong in reading CSRF token from cookies. Please check this good discussion: Why is it common to put CSRF prevention tokens in cookies?
You can read your cookie (not HttpOnly, of cause) using the following code
function getCookie(name) {
if (!document.cookie) {
return null;
}
const xsrfCookies = document.cookie.split(';')
.map(c => c.trim())
.filter(c => c.startsWith(name + '='));
if (xsrfCookies.length === 0) {
return null;
}
return decodeURIComponent(xsrfCookies[0].split('=')[1]);
}
So fetch call could look like
const csrfToken = getCookie('CSRF-TOKEN');
const headers = new Headers({
'Content-Type': 'x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken
});
return this.fetcher(url, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
email: 'test#example.com',
password: 'password'
})
});

Yes header name depends on your server. For example django usecase to setup CSRF token using fetch is like this:
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8',
'X-CSRFToken': get_token
},

Related

Why do I get error code 401 when sending post request

From the following request I get status code 302 redirect. But I want to send cookies I've received from the response with the redirect to the next page. Right now I get status code 401 when sending the request, and I've figured out that it's because I need to send the cookies along with the redirect, but I don't get the cookies until I fetch the url that gives me the redirect. How do I do that?
async function login (url) {
let request = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type' : 'application/x-www-form-urlencoded' },
body: 'username=name&password=pass&submit=login',
})
return request
}
In general body data type must match the Content-Type header. so in your case you have to use
body: new URLSearchParams({
'userName': 'name',
'password': 'pass',
'grant_type': 'password'
}),

Response 400 for Fetch API call

I am trying to make a call using JavaScript's Fetch API to generate an OAuth Token but I keep receiving a 400 response code and I'm not sure why. I wrote the key and secret to the console to verify their values, and I made the same API call using cURL (with the response I expected). Is there a small issue in my syntax?
fetch('https://api.petfinder.com/v2/oauth2/token', {
method: 'POST',
body: 'grant_type=client_credentials&client_id=' + key + '&client_secret=' + secret
}).then(r => { response = r.json() });
If the request body is a string, the Content-Type header is set to text/plain;charset=UTF-8 by default. Since you're sending urlencoded data, you have to set the Content-Type header to application/x-www-form-urlencoded.
fetch('https://api.petfinder.com/v2/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials&client_id=' + key + '&client_secret=' + secret
})
As I mentioned in a comment, you shouldn't make the above request from a browser since it exposes the client secret.
Thanks to #Arun's recommendation of adding Content-Type, I am getting the right response now.
Also, for any other JavaScript newbies playing around with the petfinder API, this is the chain that I used to extract the token from the response:
fetch('https://api.petfinder.com/v2/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials&client_id=' + key + '&client_secret=' + secret
}).then(response => response.json().then(data => ({
data: data,
status: response.status})
).then(function(res) {
console.log(res.status, res.data.access_token);
}));

Django server reporting "Forbidden (CSRF token missing or incorrect.)" despite sending token correctly?

I am trying to send a JSON POST request to my Django server.
It reports this error: Forbidden (CSRF token missing or incorrect.):
In my Django template, options.html, I say this:
<script>const incomingToken = "{{ csrf_token }}";</script>
And this:
<input type="hidden" name="csrf-token" id="csrf-token" value="{{ csrf_token }}" />
Then in my JavaScript file that runs in the client I say:
const serverUrl = "http://127.0.0.1:8000/"
const headers = new Headers({
'Accept': 'application/json',
// 'X-CSRFToken': getCookie("CSRF-TOKEN")
"X-CSRFToken": document.getElementById("csrf-token").value
})
fetch(serverUrl, {
method: "POST",
headers: {
headers
},
mode: "same-origin",
body: JSON.stringify(editorState.expirationDate, editorState.contracts, editorState.theta) // FIXME: server goes "Forbidden (CSRF token missing or incorrect.)" and 403's
}).then(response => {
console.log(incomingToken)
console.log(document.getElementById("csrf-token").value)
console.log(response)
}).catch(err => {
console.log(err)
});
Both incomingToken and document.getElementById("csrf-token").value report the same value. So I know I'm getting the correct string for the CSRF token.
How can this be? What am I doing wrong?
For reference, here is what I see in another thread on the subject:
const csrfToken = getCookie('CSRF-TOKEN');
const headers = new Headers({
'Content-Type': 'x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken // I substitute "csrfToken" with my code's "incomingToken" value
});
return this.fetcher(url, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
email: 'test#example.com',
password: 'password'
})
});
Instead of running a function to retrieve the value from a cookie, I simply insert the value Django embeds using {{ csrf_token }}. I also tried pasting the code from the top answer in this thread, including function getCookie(name). Nothing. Client still says POST http://127.0.0.1:8000/ 403 (Forbidden), server still cries with the same Forbidden (CSRF token missing or incorrect.) error.
Suggestions please!
Update:
So I tried a function from Django's CSRF protection docs page that reads:
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
For whatever reason, this function returns a different value when I run getCookie("csrftoken") -- a value that is different from that of what is embedded by {{ csrf_token }}. Dunno what to make of that. Neither one works when inserting it into "X-CSRFToken" in my headers.
I found the solution to the problem.
The solution came when I ignored much of what I found on StackOverflow and instead opted just to use the Django docs.
I had my code written as it is in my OP -- see how it makes headers out of new Headers()? And how the fetch has serverUrl plugged in as the first argument?
Well, I changed it so that it reads like this:
const serverUrl = "http://127.0.0.1:8000/"
const request = new Request(serverUrl, { headers: { 'X-CSRFToken': getCookie("csrftoken") } })
fetch(request, {
method: "POST",
mode: "same-origin",
body: JSON.stringify(editorState.expirationDate, editorState.contracts, editorState.theta)
}).then(response => {
console.log(response)
}).catch(err => {
console.log(err)
});
And it worked!
The difference was using the new Request() object in the fetch argument.

Bad request response to fetch REST API

I have built an API and app that uses that API. When I POST method via Postman, it works fine, but when I try fetching it via app, I get a bad request 400 status response. What am I doing wrong?
Here is my JavaScript code:
const myForm = document.getElementById('loginForm');
myForm.addEventListener('submit', function(e) {
e.preventDefault();
const url = 'https://thawing-peak-69345.herokuapp.com/api/auth';
const myHeaders = new Headers();
myHeaders.append('Accept', 'application/json, text/html, */* ');
myHeaders.append('Content-Type', 'application/json, charset=utf-8')
const formData = {
email: this.email.value,
password: this.password.value
};
console.log(formData);
const fetchOptions = {
method: 'POST',
mode: 'no-cors',
cache: 'no-cache',
headers: myHeaders,
body: JSON.stringify(formData)
};
fetch(url, fetchOptions)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.log(err))
})
Request
Response
Headers request:
Headers response:
You said:
mode: 'no-cors',
This is a declaration that you are not doing anything that requires permission be granted with CORS. If you try to do anything that does need permission, it will be silently ignored.
myHeaders.append( 'Content-Type', 'application/json, charset=utf-8')
Setting the Content-Type header to a value not supported by the HTML form element's type attribute requires permission from CORS. application/json is not such a value.
Consequently, the request is sent as text/plain.
Since it isn't marked as being JSON, the server throws a 400 error.
You need to:
Remove mode: 'no-cors',
Make sure that the service you are making the request to will use CORS to grant you permission (or to use a service on the same origin as the request).

Unable to access cookie from fetch() response

Even though this question is asked several times at SO like:
fetch: Getting cookies from fetch response
or
Unable to set cookie in browser using request and express modules in NodeJS
None of this solutions could help me getting the cookie from a fetch() response
My setup looks like this:
Client
export async function registerNewUser(payload) {
return fetch('https://localhost:8080/register',
{
method: 'POST',
body: JSON.stringify(payload),
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
}
...
function handleSubmit(e) {
e.preventDefault();
registerNewUser({...values, avatarColor: generateAvatarColor()}).then(response => {
console.log(response.headers.get('Set-Cookie')); // null
console.log(response.headers.get('cookie')); //null
console.log(document.cookie); // empty string
console.log(response.headers); // empty headers obj
console.log(response); // response obj
}).then(() => setValues(initialState))
}
server
private setUpMiddleware() {
this.app.use(cookieParser());
this.app.use(bodyParser.urlencoded({extended: true}));
this.app.use(bodyParser.json());
this.app.use(cors({
credentials: true,
origin: 'http://localhost:4200',
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
credentials: true
}));
this.app.use(express.static(joinDir('../web/build')));
}
...
this.app.post('/register', (request, response) => {
const { firstName, lastName, avatarColor, email, password }: User = request.body;
this.mongoDBClient.addUser({ firstName, lastName, avatarColor, email, password } as User)
.then(() => {
const token = CredentialHelper.JWTSign({email}, `${email}-${new Date()}`);
response.cookie('token', token, {httpOnly: true}).sendStatus(200); // tried also without httpOnly
})
.catch(() => response.status(400).send("User already registered."))
})
JavaScript fetch method won't send client side cookies and silently ignores the cookies sent from Server side Reference link in MDN, so you may use XMLHttpRequest method to send the request from your client side.
I figured it out. The solution was to set credentials to 'include' like so:
export async function registerNewUser(payload) {
return fetch('https://localhost:8080/register',
{
method: 'POST',
body: JSON.stringify(payload),
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
}
After that I needed to enabled credentials in my cors middleware:
this.app.use(cors({
credentials: true, // important part here
origin: 'http://localhost:4200',
optionsSuccessStatus: 200
})
And then finally I needed to remove the option {httpOnly: true} in the express route response:
response.cookie('token', '12345ssdfsd').sendStatus(200);
Keep in mind if you send the cookie like this, it is set directly to the clients cookies. You can now see that the cookie is set with: console.log(document.cookie).
But in a practical environment you don't want to send a cookie that is accessible by the client. You should usually use the {httpOnly: true} option.

Categories

Resources