User authentication is a single-handedly most required feature when building a modern web or mobile apps. It allows verifying users, user sessions, and most importantly it provides the base for implementing user authorization (roles and permissions).
Basically, you develop a login screen and allow the user to input their username/email and appropriate password and send a request to the server. If the server responds positively, that’s it. Your user is logged in. But the server returned one more thing: some kind of user identification you need to pass together with other requests to access certain data etc. Also, when the user closes the app without logging out, thanks to this we can keep him logged in and skip the login step every time the user opens the app.
It is either token-based authentication or session-based authentication. This diagram describes the main differences between the two:
Chart credit: https://dzone.com/articles/cookies-vs-tokens-the-definitive-guide
As you can see, in cookie-based authentication, after successful login, the server creates the session and return sessionId value as Cookie. Subsequent requests contain that cookie with sessionId which is verified against sessionId on the server to determine if the session is valid.
On the other hand, we have token-based authentication. After successful login server returns the signed token. That token is then usually stored in local storage. Subsequent requests are sent together with the saved token in the Authorization header. The server decodes the token and if it is valid, process the request.
Without further due, like the title states – this article will go through cookie-based authentication in React Native because it is not as straightforward as you may think.
The Problem
As you know, React Native relies on the native (Android and iOS) APIs written in Java and Objective-C. You may think the cookie usage is as straightforward as using it within the browser but unfortunately, it isn’t.
Native networking APIs are saving the cookies by default and it may seem perfectly okay at the beginning, but after some time and few requests made, requests can become inconsistent causing the server to deny the access because the cookies we sent were invalid even though there is nothing wrong with them when they initially were passed along the request.
The Solution
The first thing that came to my mind is to take the cookie management in to my own hands by simply storing them on the device (eg. Async Storage)
Now the flow goes like this:
- after successful login, the server responds with the status and cookies
- cookies are saved on the device (Async Storage)
- each subsequent request’s Header is populated with the cookie from device storage
And I thought this is the final solution. EZ right? But let’s see what the situation really looks like now.
It was working fine for a while but then the same problems started to occur and I was at the starting point again. As mentioned above, React Native has its own cookie management and now I implemented my own on top of it. Naturally, native API interfered with my implementation and won every time, overriding the cookie I sent with its own and causing the same problems to appear.
NOTE: I’m not 100% sure yet this is what is happening on the native side.
After some research, I stumbled on the react-native-cookies. It’s a cookie management library for React Native and lets you manage cookies natively. Now there is actually a way to manipulate native cookie management, the approach with storing the cookie on the device can be further improved.
As already mentioned, native cookie management interfered with the stored cookies. So let’s completely remove native cookies and work just with the ones stored on the device. The easiest way would be to just clean up the cookies stored natively.
That’s where the above-mentioned library comes into play:
import CookieManager from 'react-native-cookies'
import AsyncStorage from '@react-native-community/async-storage';
const client = async () => {
await CookieManager.clearAll() //clearing cookies stored
//natively before each
//request
const cookie = await AsyncStorage.getItem('cookie')
return await fetch('api/data', {
headers: {
'cookie': cookie
}
})
}
With natively stored cookies cleaned up before each request, it’s certain that the only cookies that are passed along the request are the ones stored manually on the device. Using this simple fix there are no more cookies interfering with each other and the main benefit is consistent sessions while using the app.
Wrap up
I spent a significant amount of time wrestling with this issue because it wasn’t so straightforward to figure out what seems to be the problem. I decided to write it down so you don’t have to.
I think it’s worth mentioning to say this is not the only possible solution to this problem because the problem itself is not fully investigated. There are also solutions without using any library which is described in this article.