This article gives an introduction to the notion of token-based, secured communication between the Flutter application and Web Server. It describes a generic protocol and flow based on Web API but without focusing on any standard such as OAuth2 protocol.

Difficulty: Intermediate

Foreword

When I first had to work with the notion of secured communication between a Client and a Server, I had to develop a solution that worked with old protocols like X25 or RS-232 in C-Language without using any library.

One of the requirements was to make sure any intercepted packets could not be re-used or replayed at a later stage. I then used the Kerberos protocol which involves the notion of tokens and encryption using DES.

Later on, when I designed Websites that also dealt with secured communication, I used JWT.

When I started up writing my first Phone App, I needed to find a solution to let my Server know the identity of the issuer of the requests. So I gave it a try and came to the following solution.

Rationale

Except for stand-alone Phone Apps, most applications need to communicate with a Server to interact. Also, in most cases, the Server needs to know and validate the identity of the application and/or the user before processing the requests. Unlike with Websites, the notion of Web Session is not to be considered. Therefore, how can we do to let the Server know if a request it receives is valid or not as well as the identity of the requestor?

For those who might be interested in industry-standard protocol for authorization, please head to OAuth 2.0 for further explanation.

This article is not about OAuth 2.0 but describes a private protocol based on the notion of token to explain the generic principle of secured, token-based communication.

What is a token?

In simple words and among others, a token is something that:

  • is exchanged between the client (=requestor) and the server at each request;
  • contains pieces of information that:
    • ensure its integrity;
    • allow the Server to know and validate the identity of the requestor;
    • allow the Server to know and control when the token expires;
    • allow the Server to control the flow of operations through a control of the validity of the request;
  • must be securized against attempts of modification or re-use in an inappropriate schema

Identity of the requestor

As the identity of the requestor is a very sensitive data, which piece of information could we use?

The user’s identity is something that may be forged quite easily. Therefore we need to find something stronger and fortunately, each Mobile Devices has a unique identifier. This information will be used to identify the requestor in terms of device (from which device is the request issued?).

Two additional pieces of information could also be used to strengthen/complement the identity of the requestor:

  • the application name or id;
  • the user (login, id, email…).

Securization of the communication

Communication between the Phone App and the Server makes use of HTTP(s) Requests (GET, POST, PUT or DELETE).

In order to make the communication between the Phone App and the Server more secured, good practice is to:

  • use SSL to encrypt the communication and therefore use HTTPS Requests;
  • pass the token in the header of the HTTPS Requests;
  • also systematically pass both Application ID and the Device Unique ID, also in the header of each HTTPS Request.

Who generates the token?

By definition, the Server is the only one authorized to generate the tokens.

Types of tokens

In most cases, it is enough to only consider 2 types of tokens:

Handshake token

During the communication from the Phone App to the Server, as no token exists, the very first request to send to the Server is a handshake request to ask the Server to generate an initial token, with a very short lifetime, to be used during the very next communication between the Phone App and the Server. This token is then returned to the Phone App.

As this token does not contain any sensitive information and has a very short lifetime (meaning it will expire in a couple of milliseconds or seconds, maximum), we do not actually need to make its transmission from the Server to the Phone App too much secured. Therefore a normal HTTPS GET request is acceptable.

handshake token

Such request could look like the following:

var response = await http.get('https://www.myserver.com/api/mobile/handshake',
				headers: {
				  'X-DEVICE-ID': 'my_device_id',
				  'X-TOKEN': '',
				  'X-APP-ID': 'my_application_id'
				});
				
if (response.statusCode == 200) {
  String token = response.body;
}

Another case might also exist: the application persists the token from one instance/session to another (case when you want the user to automatically be (re-)authenticated when the application is launched another time).

In this circumstance, rather than submitting an empty token during the handshaking request, we send the last known token. The Server validates the token, potentially proceeds with the authentication if the token appears to be valid, but systematically generates a new token, as an answer.

Communication token

This is the second type of token to be used for any kind of request.

It is generated by the Server when the following cases happen:

  • after an authentication request;
  • when the token expired or is going to expire very soon.

How to generate a token?

As said previously, a token may contain many different pieces of information, be securized, have an expiration date and allow the Server to proceed with validation of any request.

Notion of token state

In the communication protocol, we could also include the notion of token state. This information could be used at the Server side, after the token validation process, as a means of second-level flow control.

To illustrate this notion, we could imagine the following usual sequence of requests, with aside, the corresponding token state, returned by the Server:

  • handshaking -> “requires_authentication
  • authentication -> “token
  • other types of requests -> no token is returned (however if the token lifetime is short, we could consider having to generate new tokens in the middle of “usual” requests. In this case, a “token” could be returned)

Therefore, when the Server receives a request that requires an authentication, if the token is not of the state “token”, the request could be rejected. By extension, if the Server receives an “authentication” request but the token is not of the state “requires_authentication”, it could reject the request.

The way to build a token

The easiest way of building a token is to concatenate a series of textual information, use a separator between each of them and then to encrypt it, using a one-direction encryption algorithm, and to transform the result to a Base64 string.

Example:

{device_id}~{application_id}~{user_id}~{expirationTimestamp}~{type}~{sand}

could be the concatenated string, where:

  • device_id is the unique identity of the Mobile Device, as sent by the Phone App;
  • application_id is the unique identity of the application;
  • user_id after authentication, is the identity of the user, empty before the authentication;
  • expirationTimestamp is the date and time beyond which the token is no longer valid;
  • state is the token state
  • sand is some random characters

Then, this string is encyphered using a one-way encryption algorithm (the encryption key is only known at the Service side) and the resulting buffer is converted to a Base64 string.

The “sand” is used to ensure that 2 tokens, generated with the very same information, will not be the same.

HTTP helper class

Here is an example of client-side source code that implements the topics covered in this article. It requires the following 3 packages to be installed, and referenced in your pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter

  device_info: ^0.1.2
  shared_preferences: ^0.4.0
  http: ^0.11.3+16	

Also, this code assumes that the Server always returns a JSON object:

jsonObject = {
	status: 'result status',
	data:   'data, returned by the Server'
};
/**
 * Module that handles all communications with the server
 */
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:device_info/device_info.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;

// the unique ID of the application
const String _applicationId = "my_application_id";

// the storage key for the token
const String _storageKeyMobileToken = "token";

// the URL of the Web Server
const String _urlBase = "https://www.myserver.com";

// the URI to the Web Server Web API
const String _serverApi = "/api/mobile/";

// the mobile device unique identity
String _deviceIdentity = "";

/// ----------------------------------------------------------
/// Method which is only run once to fetch the device identity
/// ----------------------------------------------------------
final DeviceInfoPlugin _deviceInfoPlugin = new DeviceInfoPlugin();

Future<String> _getDeviceIdentity() async {
  if (_deviceIdentity == '') {
    try {
      if (Platform.isAndroid) {
        AndroidDeviceInfo info = await _deviceInfoPlugin.androidInfo;
        _deviceIdentity = "${info.device}-${info.id}";
      } else if (Platform.isIOS) {
        IosDeviceInfo info = await _deviceInfoPlugin.iosInfo;
        _deviceIdentity = "${info.model}-${info.identifierForVendor}";
      }
    } on PlatformException {
      _deviceIdentity = "unknown";
    }
  }

  return _deviceIdentity;
}

/// ----------------------------------------------------------
/// Method that returns the token from Shared Preferences
/// ----------------------------------------------------------

Future<SharedPreferences> _prefs = SharedPreferences.getInstance();

Future<String> _getMobileToken() async {
  final SharedPreferences prefs = await _prefs;

  return prefs.getString(_storageKeyMobileToken) ?? '';
}

/// ----------------------------------------------------------
/// Method that saves the token in Shared Preferences
/// ----------------------------------------------------------
Future<bool> _setMobileToken(String token) async {
  final SharedPreferences prefs = await _prefs;

  return prefs.setString(_storageKeyMobileToken, token);
}

/// ----------------------------------------------------------
/// Http Handshake
///
/// At application start up, the application needs to synchronize
/// with the server.
/// How does this work?
///   - A. If a previous token exists, the latter is sent to
///   -   the server to be validated.  If the validation is Ok,
///   -   the user is re-authenticated and a new token is returned
///   -   to the application.  The application then stores it.
///
///   - B. If no token exists, the application sends a request
///   -   for a new token to the server, which returns the
///   -   the requested token.  This token will be saved.
/// ----------------------------------------------------------
Future<String> handShake() async {
  String _status = "ERROR";

  return ajaxGet("handshake").then((String responseBody) async {
    Map response = json.decode(responseBody);
    _status = response["status"];
    switch (_status) {
      case "REQUIRES_AUTHENTICATION":
        // We received a new token, so let's save it.
        await _setMobileToken(response["data"]);
        break;

      case "INVALID":
        // The token we passed in invalid ??  why ?? somebody played with the local storage?
        // Anyways, we need to remove the previous one from the local storage,
        // and proceed with another handshake
        await _setMobileToken("");
        break;
		
      //TODO: add other cases
    }

    return _status;
  }).catchError(() {
    return "ERROR";
  });
}

/// ----------------------------------------------------------
/// Http "GET" request
/// ----------------------------------------------------------
Future<String> ajaxGet(String serviceName) async {
  var responseBody = '{"data": "", "status": "NOK"}';
  try {
    var response = await http.get(_urlBase + '/$_serverApi$serviceName',
        headers: {
          'X-DEVICE-ID': await _getDeviceIdentity(),
          'X-TOKEN': await _getMobileToken(),
          'X-APP-ID': _applicationId
        });

    if (response.statusCode == 200) {
      responseBody = response.body;
    }
  } catch (e) {
    // An error was received
    throw new Exception("AJAX ERROR");
  }
  return responseBody;
}

/// ----------------------------------------------------------
/// Http "POST" request
/// ----------------------------------------------------------
Future<Map> ajaxPost(String serviceName, Map data) async {
  var responseBody = json.decode('{"data": "", "status": "NOK"}');
  try {
    var response = await http.post(_urlBase + '/$_serverApi$serviceName',
        body: json.encode(data),
        headers: {
          'X-DEVICE-ID': await _getDeviceIdentity(),
          'X-TOKEN': await _getMobileToken(),
          'X-APP-ID': _applicationId,
          'Content-Type': 'application/json; charset=utf-8'
        });
    if (response.statusCode == 200) {
      responseBody = json.decode(response.body);

      //
      // If we receive a new token, let's save it
      //
      if (responseBody["status"] == "TOKEN") {
        await mid.setMobileToken(responseBody["data"]);

        // TODO: rerun the Post request
      }
    }
  } catch (e) {
    // An error was received
    throw new Exception("AJAX ERROR");
  }
  return responseBody;
}

Conclusion

This completes the introduction to the notion of token-based communication.

In a next article, I will explain the Server part of the communication.

Meanwhile, stay tuned and happy coding.