Cover Photo by James Sutton on Unsplash
The series of posts about advanced networking topics applied to Flutter apps continues. As always, this is meant for people who already have a good understanding of Flutter and know how to use Flutter widgets and the basics of the http
library.
That's the kind of stuff you can learn by reading my Flutter book.
In particular, this post supposes you know how to make HTTP requests, how to use the most basic widgets (Scaffold
, Column
, Text
, FlatButton
, etc.) and that you know how to use a TextField
with a TextEditingController
, the basics of asynchronous functions and Future
s and how to use them with a FutureBuilder
, how to display dialogs and how to work with JSON.
You can learn about them online too, if you hate books. I won't be judging you.
Regarding the backend, I'm going to suppose you know about how to use the Express Node.js framework, and that you know how to use basic SQL commands like CREATE
, INSERT
, and SELECT
.
What We’re Going to Build
The app we’re going to build is the simplest example of an app that requires authentication: it allows anyone to sign up, and any logged in user can access a piece of data. We’re going to implement the back-end with Node and the front-end with Flutter.
What is JWT
JWT (JSON Web Token) is a standard that specifies a very secure way to transmit session tokens between an user-accessible front-end (that we’ll write using Flutter) and a back-end (that we’ll write using Node).
Unlike a more traditional user session implementations, in which the session token is stored both on the server and on the client, the client sends the token along with every request and the server can check whether a session exists with that token for that user and decide whether to grant access based on that and what the user is actually allowed to do.
JWT is different. The server doesn’t store the token: at the time of authentication, it sends a signed token, but it doesn’t store it, instead relying on the signature it attaches to the token (obtained either with RSA, ECDSA or HMAC with SHA256 usually), which allows it to verify both the authenticity of the token and whether it was tampered with.
This means the token's payload can contain both data the front-end needs, since it can be freely accessed by it, and data (like the user name/ID and/or an expiration date) the server needs to validate the request and the token.
The actual structure of the JWT is made of three base64-encoded strings separated by a .
character: the first contains information needed to verify the signature, the second contains the payload, the third contains the signature.
Analyzing an Example
I've taken an example of a JWT generated by the backend we'll build as an example in this post. It is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNTgxOTY2MzkxLCJleHAiOjE1ODMyNjIzOTF9.IDXKR0PknG96OyVgRf7NEX1olzhhLAiwE_-v-uMbOK0
.
If you pay attention, you'll notice there are two dots. The first (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
) can be decoded to ASCII to {"alg":"HS256","typ":"JWT"}
, which is a JSON object, which can be formatted like this:
{
"alg": "HS256",
"typ": "JWT"
}
~~~
`HS256` is short for `HMAC`+`SHA256`, and `typ` is quite self-explanatory, it tells us what the string we're looking at is.
The second string (`eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNTgxOTY2MzkxLCJleHAiOjE1ODMyNjIzOTF9`) can be decoded and formatted to:
{
"username": "username",
"iat": 1581966391,
"exp": 1583262391
}
This is a JWT for an user called {% raw %}`username`, issued at (`iat`) second 1581966391 after the Unix epoch (the 17th of February 2020 at 19:06) and that expires at (`exp`) second 1583262391 (03/03/2020 at the same time as when it was created).
The third string is just the signature obtained as an HMAC with SHA256.
# Our API Interface
Our backend is going to have three routes:
* `/signup`, which accepts POST requests in urlencoded format, containing two self-explanatory text fields: an *username* field and a *password* field and either responds with status code 201 if it was able to create the user, or status code 409 if it wasn't;
* `/login`, which accepts POST requests in urlencoded format and accepts the same fields as `/signup`, and either responds with status code 200 and the JWT in the body of the response, or with status code 401 if there is no user with the given username and password;
* `/data`, which accepts GET requests, which must have a JWT attached to the `Authorization` request header, and which will either return the "secret data" only authenticated users can access (with status code 200) or a response with status code 401, meaning the JWT is invalid or has expired.
# Implementing the Front-End App with Flutter
You can find the code for this Flutter app [on GitHub by clicking here](https://github.com/carzacc/jwt-tutorial-flutter).
## Using The flutter_secure_storage Package
The *flutter_secure_storage* package is very easy to use: it's simply key-value pair storage, but using iOS's Keychain directly or by encrypting the data, storing the encrypted data in Android's Shared Preferences.
You can write the *myvalue* and associate it to the *mykey* key using it by using `storage.write()` in the following way:
var storage = FlutterSecureStorage();
storage.write(key: "mykey", value: "myvalue");
{% raw %}`storage.write` is asynchronous (it has a return type of `Future<void>`), so you should use `await` if you need to pause execution while it executes.
You can read the value associated to the *mykey* key by using `storage.read()`, which is also asynchronous, so you need to wait for it to return in order to be able to read the value.
var storage = FlutterSecureStorage();
var value = await storage.read(key: "mykey");
You can use {% raw %}`storage.delete()` to delete a value (it's also asynchronous):
var storage = FlutterSecureStorage();
storage.delete(key: "mykey");
Two methods exist, called {% raw %}`readAll()` and `deleteAll()` (both asynchronous), which respectively return a `Map` of all the stored values and delete all of the stored values.
## Implementation
The Flutter app doesn’t need to be particularly complicated to be able to work with JWT: it’s mostly about writing an authentication flow, storing the JWT token and sending it with each request.
In addition to that, for this example we’ll check whether the token has expired on the front-end and we’ll show the username after the user logs in, so we’ll actually have to decode the payload.
We are going to store the JWT using the *flutter_secure_storage* package, which is the simplest way to access the secure storage interfaces provided by iOS and Android from a Flutter app.
Here are two screenshots of what we want to achieve:
![](https://carmine.dev/img/Screenshot_1581601687.png)
![](https://carmine.dev/img/Screenshot_1581601694.png)
Add the *flutter_secure_storage* and *http* packages to the app's dependencies in `pubspec.yaml`:
Then, set the minimum supported Android version to SDK level 18 (Android 4.3) because that's required by flutter_secure_storage.
Create the usual lib/main.dart
, import the packages, initialize a FlutterSecureStorage
object and insert the IP and port where the Node server backend will be running, so that we can use that going forward without having to change anything else from the example code I provided:
The Structure of Our Flutter App
The structure of our Flutter app is going to be the following:
- the
MyApp
class, which is going to check whether the user has previously logged in, and decide whether to run the LoginPage
or the HomePage
;
- the
LoginPage
is where we are going to allow the user to log in or sign up;
- the
HomePage
is where we are going to show the user the secret data that can only be accessed by logged-in users. The HomePage
needs to be able to be constructed from either just the JWT Base 64 string
Creating a Log-In Page
The MaterialApp
object we're launching is called MyApp
, but we'll worry about that later, given that it needs to check whether we're already logged in, and then choose whether to display a log-in page or the home page.
That's a bit boring to worry about now, let's build some UI and create a log-in page!
The log-in page itself will be a StatelessWidget
called LoginPage
:
class LoginPage extends StatelessWidget {
}
The nature of the login page and our implementation means we are going to make extensive use of dialogs. This means we'll create a helper method in order to make everything more succint:{% raw %}
void displayDialog(BuildContext context, String title, String text) =>
showDialog(
context: context,
builder: (context) =>
AlertDialog(
title: Text(title),
content: Text(text)
),
);
We'll also create methods that attempt to login and signup the user, and they're very simple POST requests, as we saw earlier on when we talked about our app's API interface.
We'll return a string for the login method, which will be {% raw %}`null` in case of an error (i.e. wrong username/password) and the JWT if the authentication process succeded:
Future attemptLogIn(String username, String password) async {
var res = await http.post(
"$SERVER_IP/login",
body: {
"username": username,
"password": password
}
);
if(res.statusCode == 200) return res.body;
return null;
}
The sign-up method doesn't actually have to return anything useful to the app, so we can just return the status code and deal with that later to establish whether the operation was successuful or not:{% raw %}
Future attemptSignUp(String username, String password) async {
var res = await http.post(
'$SERVER_IP/signup',
body: {
"username": username,
"password": password
}
);
return res.statusCode;
}
Here comes the fun part!{% raw %}
@override
Widget build(BuildContext context) =>
Scaffold(
appBar: AppBar(title: Text("Log In")),
body: /* TODO:INSERT BODY HERE */
);
what's the {% raw %}`body` going to be? A `Column` with two `TextFields` that allow the user to insert username and password and two `FlatButton`s: one to log in and one to sign up. We'll also add some padding around it given that it looks horrible to my eyes without it:
body: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username'
),
),
TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password'
),
FlatButton(
child: Text("Log In"),
onPressed: /* TODO:HANDLE LOG IN /
),
FlatButton(
child: Text("Sign Up"),
onPressed: / TODO:HANDLE SIGN UP */
)
)
]
)
)
The {% raw %}`labelText` in the `InputDecoration` is the nicest looking way to tell users what each `TextField` is for.
Define the `_usernameController` and `_passwordController` `TextEditingController`s somewhere in the class definition, like this:
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
Let's worry about the function to handle logging in, which needs to call the {% raw %}`attemptLogIn` method with the `username` and `password` taken from the `TextEditingController`s, then check the JWT returned by the `attemptLogIn` method. If it is `null`, we need to display a dialog to inform the user we were unable to log them in. If it isn't, we can switch to the `HomePage` and save the JWT:
onPressed: () async {
var username = _usernameController.text;
var password = _passwordController.text;
var jwt = await attemptLogIn(username, password);
if(jwt != null) {
storage.write(key: "jwt", value: jwt);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage.fromBase64(jwt)
)
);
} else {
displayDialog(context, "An Error Occurred", "No account was found matching that username and password");
}
},
The function that handles sign-up is going to be complicated by the checking of a few conditions.
We are going to check on the front-end that the user doesn't try to use un username or password less than 4 characters long. This should also be done on the back-end, but this tutorial focuses more on Flutter than it does on Node, so we're just going to do this on the front-end.
If those two inputs are valid, we are going to attempt to sign up, and check the response. If we get HTTP status code 201, it means the user was created and we can simply tell the user to log in with those credentials.
If we get HTTP status code 409, it means the username is already in use, and we can tell that to the user. If the response's status code is neither 409 nor 201, the request failed (probably because of a network error or an internal server error), we just tell the user an unknown error occurred:{% raw %}
onPressed: () async {
var username = _usernameController.text;
var password = _passwordController.text;
if(username.length < 4)
displayDialog(context, "Invalid Username", "The username should be at least 4 characters long");
else if(password.length < 4)
displayDialog(context, "Invalid Password", "The password should be at least 4 characters long");
else{
var res = await attemptSignUp(username, password);
if(res == 201)
displayDialog(context, "Success", "The user was created. Log in now.");
else if(res == 409)
displayDialog(context, "That username is already registered", "Please try to sign up using another username or log in if you already have an account.");
else {
displayDialog(context, "Error", "An unknown error occurred.");
}
}
},
The entire {% raw %}`LoginPage` definition looks like this in the end:
Creating an Home Page
We already know what we need as constructors for the HomePage
. Getting the payload from the base64 JWT string should be pretty self-explanatory if you understood the section at the start of this post about the structure of a JWT, and you only need to keep in mind that base64.decode needs a padded base64 string, and that can be obtained with base64.normalize()
:
class HomePage extends StatelessWidget {
HomePage(this.jwt, this.payload);
factory HomePage.fromBase64(String jwt) =>
HomePage(
jwt,
json.decode(
ascii.decode(
base64.decode(base64.normalize(jwt.split(".")[1]))
)
)
);
final String jwt;
final Map payload;
}
Instead of {% raw %}`ascii.decode`, use `utf8.decode` if you want to support non-ASCII characters. Thanks to @bisquits in the comments for noticing this, since it's really important!
### The HomePage's build() method
The `HomePage` widget itself is going to be made of a `FutureBuilder` that waits for the GET request to the server to get the data, and then either displays it or some text informing the user an error has occurred, if that is the case:
@override
Widget build(BuildContext context) =>
Scaffold(
appBar: AppBar(title: Text("Secret Data Screen")),
body: Center(
child: FutureBuilder(
future: http.read('$SERVER_IP/data', headers: {"Authorization": jwt}),
builder: (context, snapshot) =>
snapshot.hasData ?
Column(
children: [
Text("${payload['username']}, here's the data:"),
Text(snapshot.data, style: Theme.of(context).textTheme.display1)
],
)
:
snapshot.hasError ? Text("An error occurred") : CircularProgressIndicator()
),
),
);
The {% raw %}`HomePage` class definition is, therefore, the following:
Creating the MyApp Class
The MyApp
class is the class that gets run when the app starts. It needs to check whether it already has a JWT and, if it has one, it should check whether it is valid, whether it has expired and, based on that, decide whether to ask the user to log in or whether to show them the home page. We are going to need to create a method called jwtOrEmpty()
Future get jwtOrEmpty async {
var jwt = await storage.read(key: "jwt");
if(jwt == null) return "";
return jwt;
}
That's because the {% raw %}`FutureBuilder`'s `snapshot.hasData` parameter that gets passed to the `builder` function would return `false` if it were to receive a `null` value, which would be the case if the JWT were non-existent. This is unwanted because we would have no way of distinguishing the case in which we are still waiting for the `Future` to return and the case in which there is no JWT. Having this second case return an empty string instead of `null` fixes the issue.
All that's left to do is to create a `build()` method, which should return a `MaterialApp` (that's the whole point of the `MyApp` class) that, after getting the JWT and checking it, either shows the user the login page or the home page based on the criteria I described earlier.
All of that ends up in the following class definition:
Implementing the Back-End with Node
You can find the code for this Node backend on GitHub by clicking here.
The Node back-end is where most of it happens: we need to create rules for login, logout and some sort of data to access. We are going to store users in an SQLite database to keep things simple for this example.
Choosing How to Sign the Token
The token can be signed using either a method based on public key cryptography (for example using RSA or ECDSA) or by relying on hashing the concatenation of the secret key and the message (called a payload in JWT terms) with any hashing algorithm (usually sha256). The latter concept has been expanded into a full standard for generation of a digital signature (called HMAC) that is protected against collisions and length extension attacks, about which you can find more information on Wikipedia.
For this example, that’s what we will use: HMAC with SHA256. That’s because it’s easier for a quick example for a blog post because we have greater freedom with our choice of private key. When building a real app, you should obviously consider the advantages and disadvantages of this approach based on your needs and the requirements your apps needs to meet. For this post we’ll use a short and simple string as a key, that’s not how it’s supposed to be done: it should ideally be a generated pseudorandom (and particularly long) key, just like any RSA or ECDSA key generated with OpenSSL, otherwise you’re making it very easy for attackers to crack your private key and generate their own JWT tokens your back-end will think are genuine, allowing them to pretend they’re logged in as any user, completely breaking your website’s security.
Safety First!
In other words, this is an example meant to be as easy to follow as possible and you must take the appropriate precautions when it comes to choosing or generating a private key. Since we’re talking about security precautions, you should obviously use TLS for communications between front-end and back-end in production. Also, salt the passwords before hashing them if you really want to play it safe.
Building the Back-End
These are the libraries we are going to use:
-
Express, which is what we are going to use as a Web framework;
-
jsonwebtoken, which simplifies greatly the process of creating JSON Web Tokens;
-
the sqlite3 SQLite Node driver to create and access the SQLite database where we are going to store our users' data;
- Node's built-in crypto library to hash the password to store in the SQLite database.
Using the jsonwebtoken Library
The jsonwebtoken
NPM package is very, very easy to use. It provides us with three functions, once imported with jwt = require("jsonwebtoken")
:
-
jwt.sign(payload, key, options)
, which returns a JWT containing the payload
and signed using the key
, optionally you could also add a callback at the end of the argument list and it would be ran asynchronously, but we're not going to use that feature for our simple example, the default algorithm used for generating the signature is RS256
(RSA signature with SHA256);
-
jwt.verify(token, key)
, which returns the decoded payload if the JWT is valid, and throws an error if it isn't, it also optionally takes some options and a callback just like jwt.sign()
, but we're going to provide neither of them;
-
jwt.decode(token)
, which decodes the payload without verifying the validity, we're not going to use this one at all.
For example, if your payload is {username: "myusername"}
and the private key is stored in the variable privKey
and you want to create a JWT using the provided RSA private key that is valid for two hours, you can run
let jwt = jwt.sign({username: "myusername"}, privKey, {expiresIn: "2h"} );
and then, if {% raw %}`pubKey` is the corresponding RSA public key, you can run
let jwt = jwt.verify({username: "myusername"}, pubKey);
In our example we are going to use just one secret key (stored in a variable called {% raw %}`KEY`), so we are going to generate the JWT as an HMAC with SHA256 (*HS256*) and have it expire after 15 days, which means we are going to generate the JWT with:
let jwt = jwt.sign({username: "myusername"}, KEY, {expiresIn: "15d", algorithm: "HS256"});
I recommend you look at the documentation for the package on the NPM page for a more complete list of options. I'm not writing an API reference here, I'm trying to make you understand how to implement this.
### A Quick Introduction to the sqlite3 Node SQLite Driver
The sqlite3 Node SQLite Driver doesn't come with too many bells and whistles. There are other packages that provide different interfaces, this is the most basic one and it's perfect for a very basic and simple example that really shouldn't require a huge {% raw %}`node_modules` directory or particularly complicated.
After importing the library with `sqlite = require("sqlite3")`, you can initialize a connection to a database stored in a file called *filename.db* with:
let db = new sqlite.Database("filename.db");
The functions we are going to use are:
* {% raw %}`db.get(query, function(err, row) {})`, which runs the `query` and passes to the provided callback the first row returned as the `row` argument and any errors as the `err` argument;
* `db.run(query)`, which runs the given query, without returning any results, this is used for commands such as `CREATE` and `INSERT` and you can pass a callback to be called in case of errors.
Two other functions exist called `db.all()` and `db.each()` which respectively pass all of the rows as an array to the callback and call the callback for each row returned, only passing one at a time. We would need to use such functions if we wanted, for example, to check whether a log-in attempt failed because the password was wrong, even though the given username exists in the database.
#### Query Parameters
The queries can contain parameters, which can optionally be passed as an array, so they can replace `?` characters found in the query string. This will sanitize the strings before substituting them, not making you vulnerable to SQL injection. For example, the following two calls to `db.each` are equivalent:
db.each("SELECT * FROM Users WHERE username='carmine'", function(err, row) {
console.log(row);
});
~~~
db.each("SELECT * FROM Users WHERE username=?", ['carmine'], function(err, row) {
console.log(row);
});
~~~
### Installing Dependencies
As always, create a Node project with
$ npm init
and install the packages I listed above with{% raw %}
$ npm install --save express jsonwebtoken sqlite3
### Initial Config
We are going to import all of the libraries into our code and set the secret key (a very unsafe one, but this is just an example) and initialize the database connection:
{% gist https://gist.github.com/carzacc/ab1f598a313806a02dc401d52a10df6b file=initialConfig.js %}
### Initializing the Database
You can do this either directly using the SQLite or any other tool of your choice or by creating this Node.js file and running it:
{% gist https://gist.github.com/carzacc/13453b285f265bfa3606f271d2601eb4 %}
### Implementing Sign-Up
Signing users up is simple and has nothing to do with how we manage authorization: we're not logging them in, which means we just need to check whether they have already signed up. If they haven't, we sign them up by adding them to our database. We are going to use the Express built-in urlencoded middleware to parse the body of the request and we're going to log everything to the server console:
{% gist https://gist.github.com/carzacc/ab1f598a313806a02dc401d52a10df6b file=signup.js %}
Logging in is all about looking in the database for the user who is trying to log in, generating a JWT and returning it if we find it, and returning an error if we don't:
{% gist https://gist.github.com/carzacc/ab1f598a313806a02dc401d52a10df6b file=login.js %}
Verifying the token is very easy with the Node library we are using, meaning the {% raw %}`/data` route is written very quickly and painlessly, remembering that a failure in verifying the JWT will result in an error being thrown:
As usual, we take the port to run the server on from the environment variable PORT
and, if that fails, we set it to 3000 and run the server:
Top comments (29)
I have a question: how do I use the user data in the payload on other pages of my app (other than the homepage)? Do I pass the payload for each class? Thanks a lot in advance! EDIT: I have seen that the solution could be a singleton object. What other ways can be?
The general approach to shared data across pages in Flutter is to use an
InheritedWidget
or simpler alternatives to it like the provider package. I hope this answered your question.On that note: how would you go about using the token itself for every api call in other parts of the app? Right now I’m getting the JWT out of secure storage for every api call I make but I wonder if that’s efficient.
You can fetch once when the app starts and then have an InheritedWidget or Provider above all of your widgets provide the JWT without having to fetch it from local storage every time.
Right, thought about doing that as well. Wasn’t sure because of security but I guess in memory (ie provider) is alright while secure storage at rest.
Thanks a lot! I was looking for this.
Also a tip: if you're using non-ascii characters in the username or other user data that you store in the payload, you will get a format exception. (I needed some time to figure out what had caused the problem, hahah!) The fix is to use utf8.decode() instead of ascii.decode().
You're completely right! I'm sorry I didn't read your comments earlier but I was really busy and didn't notice!
You didn't mention it, by the way thanks for the article
(See the Readme of plugin)
Note By default Android backups data on Google Drive. It can cause exception java.security.InvalidKeyException:Failed to unwrap key. You need to
disable autobackup, details
exclude sharedprefs FlutterSecureStorage used by the plugin, details
Awesome article! Thank you so much
Thanks a lot for this article and also for the advanced flutter networking series. The way you write is very understandable. I also like that, when you use a simplified example, you do mention that in order to not let the reader fall into bad habits. Looking forward to finishing the whole series and also reading more articles from you. Greetings from Mexico.
We are trying to write some code to help with coronavirus response internationally. Thanks for this clear explanation of JWT with Flutter and NodeJS. I appreciate the clean and simple github code.
We're working on an app for coronavirus volunteer coordination. This is the premise: tinyurl.com/Boots3CaretakersUserStory and tinyurl.com/Boots3Caretakers
I could use some code review. We could pay you or you could volunteer your time. (We're all volunteers)
I'm hocho on discord. Those docs give directions on how to find me.
Hi Carmine, Good tutorial.
I followed it, but I got problem in HomePage class, in the FutureBuilder.
I was not able to get it working in the original form, I had to modify the headers as follows (my data GET route is /api/auth/profile):
Using "Authorization": jwt, passed in the whole json and the server did not authenticate, so I was redirect to login ang got error 405 (because /api/auth/login requires POST).
On the server I have nginx+ Laravel 8 and followed the recomended instruction to install jwt-auth.
DId I miss somenthing in the cofiguration of the server?
Thx
Damiano
Thank you for this great tutorial. This was super helpful!
Ciao, avrei una domanda: con flutter effettuo il login con un json ma non funziona, nel senso che anche se metti le credenziali sbagliate da 200 (cioè ok). Ho solo inserito un file json (login.json). il server in cui risiede il json è un HOSTING L.A.M.P. (Linux / Apache / MySQL / PHP), devi creare qualcosa di speciale o sbaglio la struttura del json? Penso che lato Flutter sia tutto ok....
File Json
{
"email": "eve.holt@reqres.in",
"password": "cityslicka"
}
api_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../model/login_model.dart';
class APIService {
Future login(LoginRequestModel requestModel) async {
String url = "nextesa.it/api/login";
}
}
Chiedo scusa se rispondo tardi, ma non accedevo a DEV da tanto. In ogni caso, da questi snippet non vedo niente di anomalo, consiglio di mandarmi una mail a carmine@carminezacc.com in questi casi.
Thanks for sharing this. I have a question, for this implementation, does your app get the user logged in after the app completely closed or the jwt get lost after that? I am wondering if the flutter_secure_storage package is the best choice to use if I want to save the jwt so that when I reopen the app I don't have to log in. Thanks in advance!