Securely Storing JWTs in (Flutter) Web Apps
Tags: webdev, dart, security, tutorial
Published: 2020-04-29 13:50:26

I recently wrote [a post about how to implement JWT Authorization in Flutter apps](https://carmine.dev/posts/jwtauth/). I only considered the use case of writing a mobile app, so I recommended the use of the `flutter_secure_storage` package to store the tokens.

As it later emerged, some people wanted to use that tutorial as a guide for Flutter Web apps. `flutter_secure_storage` doesn't work for Web apps.

This first post about this topic will simply look to address that, later I'll post a more general overview of what needs to be taken in consideration when writing cross-platform apps in Flutter, including a deeper dive into storage on the different platforms. This will be more of a hands-on tutorial like the ones I previously posted, whereas the other one will be more of an opinion/overview post that I hope will help understand the thinking that is required before trying to deploy on multiple platforms.

For the sake of keeping each post focused, I'll keep this one more practical and focused on how to make that example work on the Web.

# The Basic, Compromising, Simple Approach

The simple solution to that problem that will work on every platform is to use the `shared_preferences` package, which uses `SharedPreferences` on Android, `NSUserDefaults` on iOS and `localStorage` on the Web.

This is actually only one half of a solution, as using `localStorage` means that the value can be retrieved by any JS (or Dart code compiled to JS like Flutter Web code) on your page. This means you have to be extra careful with user input to avoid XSS attacks, and that comes into play especially if you have non-Flutter Web content on the same domain (which can access the same `localStorage`), which can access the same data.

Also, you need to keep in mind that the user can actually very easily access the token through JavaScript, so if their end gets compromised in any way that token can be easily read.

This should already give you an idea of the kind of issues you might have to deal with when writing cross-platform apps. Also, especially `NSUserDefaults` on iOS is not supposed to be used for sensitive information (nor is `SharedPreferences` actually), so you should use `flutter_secure_storage` on those platforms anyway, and at that point you can just use `dart:html` directly for the Web part and skip having `shared_preferences` as a dependency entirely.

# The Better Approach

`flutter_secure_storage` on mobile should be your first and only choice. It uses the proper Keychain API on iOS and it encrypts the data, stores the encrypted data in `SharedPreferences` and the cryptographic key is stored in the Android KeyStore, which is a safe approach.

On the Web though, you need to use a Web-based solution, so you need to think about our Flutter app as if it was any old boring HTML, CSS and JavaScript website.

The place where tokens are stored in Web apps are `httpOnly` cookies, which are sent to the backend automatically along with each request, but aren't accessible by JavaScript.

The issue with that is that *automatically* part. It makes you vulnerable to CSRF, which is an attack that sends requests to your server from one of your users clients when they visit a page that isn't yours. GET requests are particularly vulnerable because a GET request query is just a link that can be clicked by your users.

This can be prevented by having another token (which shouldn't be the same token you use for authorization, obviously, but it can be generated based on that) that is stored in `localStorage` (so only your website's code can access it) and send that along with each request in a dedicated header.

You still MUST NOT be vulnerable to XSS because, even though your token can't be read, an attacker can still send requests through your own page, take your CSRF token, and bypass your CSRF protection entirely, but at least the JWT isn't in their hands so they haven't permanently stolen your user's identity. Escape all user-provided data before ever doing anything with it both on the front-end if you have another website running and on the back-end when you put data in a database: many libraries to interact with databases have that feature built-in, you just need to use them properly instead of just concatenating strings. If you really have to use string concatenation because of your stack, remember to sanitize the input before doing anything with it.

Now, let's get hands-on and see the code needed to make everything work!

# Putting It In Practice

If you're new to my posts (and there will be a few of you), you might not be aware of [my earlier post](https://carmine.dev/posts/jwtauth/) about JWT authentication with Flutter and Node (which supposed you were writing a mobile app), which I'll use as a starting point, and that you therefore should at least be aware of in case something confuses you. I won't be starting from scratch here.

## The Node Side

Before we can build the app, we need to get the backend API straight. Remember, I won't explain the stuff I already covered in my previous post. The starting point is this repository

{% github carzacc/jwt-tutorial-backend no-readme %}.

The code we're going to end up with is in this repository

{% github carzacc/jwt-tutorial-backend-web no-readme %}.

The usual Node imports and the signup route don't require any changes:

{% gist https://gist.github.com/carzacc/ac22c56e71a4ba9dd9c60bf79756833a file=index_imports.js %}

{% gist https://gist.github.com/carzacc/ac22c56e71a4ba9dd9c60bf79756833a file=index_signup.js %}

The login route, though, is where the *fun part* starts (you're reading this because you find this all fun, don't you?). The way I'm going to do this is the following: I'm going to generate two tokens: an *access* token and a an *anti-CSRF* token, and we're going to send the first as a cookie and the second in the response body. I'm not setting the `Secure` flag because this code isn't meant for production, but that should be set in production code given that your app would run with TLS in a production environment:

{% gist https://gist.github.com/carzacc/ac22c56e71a4ba9dd9c60bf79756833a file=index_login.js %}

The data route needs to get both values, one in the cookies and one as a header in order to work:

{% gist https://gist.github.com/carzacc/ac22c56e71a4ba9dd9c60bf79756833a file=index_data.js %}

and here is the entire `index.js`:

{% gist https://gist.github.com/carzacc/ac22c56e71a4ba9dd9c60bf79756833a file=index.js %}

## The Flutter Side

On the Flutter side, the starting point is this repository

{% github carzacc/jwt-tutorial-flutter no-readme %}.

The code we're going to end up with is in this repository

{% github carzacc/jwt_tutorial_flutter_web no-readme %}.

The approach is going to be the following, in order to make it as obvious as possible we're actually building a Web app: the JWT is going to be in the cookies, so it's beyond our control, whereas we're going to store the anti-CSRF token in the `localStorage` directly using `dart:html`.

This means that we are going to add to our imports `import 'dart:html' show window;` and take out the `flutter_secure_storage` dependency given that we are not using it:

{% gist https://gist.github.com/carzacc/3796cffa3aac72dd3dbe17d33c91e846 file=main_imports_runapp.dart %}


`MyApp` is, as always, where we decide whether we need to show the data page or the login page. In order to change as little as possible from the original, I switched the `home` from a `FutureBuilder` to a `Builder` (we don't need `Future`s in the case of HTML `localStorage`) and moving from calling a `jwtOrEmpty` getter to generating our very own `csrfTokenOrEmpty`. The rest stays the same: if it's expired, the access token has expired too, so we need the user to log in again, if we don't have one we need to show the user the login page:

{% gist https://gist.github.com/carzacc/3796cffa3aac72dd3dbe17d33c91e846 file=main_myapp.dart %}

The `LoginPage` gets changes in the callback to log in, as that needs to store the JWT to local storage too.

`window.localStorage` can be accessed just like any `Map` like you see in the following snippet we're going to use in the login page:

~~~
var username = _usernameController.text;
var password = _passwordController.text;
var jwt = await attemptLogIn(username, password);
if(jwt != null) {
  window.localStorage["csrf"] = jwt;
  Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => HomePage.fromBase64(jwt)
  )
);
~~~

I kept the variable names as they were for maximum comparability with the previous post.

here's the entire `LoginPage`:

{% gist https://gist.github.com/carzacc/3796cffa3aac72dd3dbe17d33c91e846 file=main_login.dart %}

The data (home) page is going to be the simplest one, as all that needs to change is the name of the header at which we send the anti-CSRF token, given that the access token is sent along with the request as a cookie automatically and the rest it taken care of by the backend:

{% gist https://gist.github.com/carzacc/3796cffa3aac72dd3dbe17d33c91e846 file=main_datapage.dart %}

Here's the full `main.dart` for that:

{% gist https://gist.github.com/carzacc/3796cffa3aac72dd3dbe17d33c91e846 file=main.dart %}

In order to make that Flutter app look and behave nicer on bigger screens and browsers in general, you might want to check out [the tutorial on responsive and Web development with Flutter I wrote for Smashing Magazine](https://www.smashingmagazine.com/2020/04/responsive-web-desktop-development-flutter/).

# Onwards: Your Next Steps

You might have found this post useful, and perhaps you'd like to see more Flutter content from me. What you can do is:

* [follow me on Twitter @carminezacc](https://twitter.com/carminezacc);
* [subscribe to my newsletter](https://carmine.dev/newsletter/)
* check out [my blog at carmine.dev](https://carmine.dev);
* check out [my book](https://pragprog.com/book/czflutr/programming-flutter), for which paperback copies are available in many places, including Amazon (when and if they deliver given the current pandemic).