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

This article covers the Service-side part of a token based communication. Please refer to Part1 for the introduction to this topic.

Deliberately, I opted for an implementation in .NET, since it is not obvious to find posts using this technology.

This article is only meant to give an implementation of a token-based communication between a Mobile App and a Server.

Summary of the previous article

Token

A token is a piece of information which is shared between the Mobile App and the Server to securize the communications. It contains the following data:

{device_id}~{application_id}~{user_id}~{expiration_timestamp}~{token_type}~{sand}
  • device_id: unique identity of the device. This information was sent by the Mobile App
  • application_id: unique identity of the application. This information was sent by the Mobile App and also known by the server
  • user_id: identity of the user (once authentified). This information is added by the Server
  • expiration_timestamp: timestamp beyond which the token is no longer valid. This information is generated by the Server
  • token_type: type of token. Generated by the Server
  • sand: some random data, also generated by the Server

A token is encrypted using a one-way encryption algorithm (the encryption key is only known at the Server side).

Handshake

In the previous article, we saw that in order to securize the communication, an initial handshake happens between the Mobile App and the Server.

During this initial handshake, the Mobile App submits the following pieces of information to the server, via the Header:

  • application identification
  • device identity
  • any previous token

When the server receives such request, it needs to:

  • fetch this information from the request header
  • validate the information
  • generate a new token based on the validation results or reject the request

Other communications

Any other communication has to follow this strict pattern:

The Mobile App sends:

  • In the request header:

    • application identification
    • device identity
    • token
  • In the request body:

    • the payload

When the server receives a request:

  • It retrieves the data, from the request header
  • Validate this data
  • If the validation fails, rejects the request
  • If the validation succeeds, process the request and sends the answer back.

Implementation

This section shows a basic implementation of this theory.

Token generation and validation

The following code is a helper class that deals with the whole notion of token:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Security;

namespace MyApplication.Helpers.Mobile
{
    /// <summary>
    /// Handles the MobileToken
    /// 
    /// Resulting token is a base64 string.
    /// The content is the following:
    ///     {deviceId}~{applicationId}~{userId}~{expirationTimestamp}~{status}~{sand}
    /// To obtain the base64 string, we encrypt the content by the deviceId + ENCRYPTION_KEY
    /// </summary>
    public class MobileToken
    {
        public string Token { get; set; }
        public MobileTokenStatus Status { get; set; }
        public DateTime ExpirationDateTime { get; set; }
        public string ApplicationId { get; set; }
        public string UserId { get; set; }
        public string Sand { get; set; }
        public string DeviceId { get; set; }


        private const string MOBILE_DEVICE_ID = "X-DEVICE-ID";
        private const string MOBILE_APP_ID = "X-APP-ID";
        private const string MOBILE_TOKEN = "X-TOKEN";
        private const string REFERENCE_APP_ID = "MY_APPLICATION_ID";
        private const string ENCRYPTION_KEY = "MyEncrypTionKey";
		
        public MobileToken()
        {
            // Empty
        }

        /// <summary>
        /// Retrieve the token from a Http Request
        /// We need to validate this token
        /// </summary>
        /// <param name="request"></param>
        public MobileToken(HttpRequestMessage request)
        {
            IEnumerable<string> headerValues;

            //
            // Retrieve the device ID
            //
            string _deviceId = "";
            if (request.Headers.TryGetValues(MOBILE_DEVICE_ID, out headerValues))
            {
                _deviceId = headerValues.FirstOrDefault();
            }

            //
            // If there is no device ID, this is a fatal ERROR
            // This should normally NEVER happens since we have a filter to make sure all requests issued by a mobile device contain this intel
            //
            if (string.IsNullOrEmpty(_deviceId))
            {
                Status = MobileTokenStatus.INVALID;
                return;
            }

            //
            // Retrieve the application ID
            //
            string _appId = "";
            if (request.Headers.TryGetValues(MOBILE_APP_ID, out headerValues))
            {
                _appId = headerValues.FirstOrDefault();
            }

            //
            // If there is no application ID or if the latter does not correspond to the reference one, this is a fatal ERROR
            //
            if (string.IsNullOrEmpty(_appId) || _appId != REFERENCE_APP_ID)
            {
                Status = MobileTokenStatus.INVALID;
                return;
            }
						
            //
            // Retrieve the token from the headers
            //
            string _token = "";
            if (request.Headers.TryGetValues(MOBILE_TOKEN, out headerValues))
            {
                _token = headerValues.FirstOrDefault();
            }

            if (string.IsNullOrEmpty(_token))
            {
                Status = MobileTokenStatus.NO_TOKEN;
                DeviceId = _deviceId;       // Save the device id for later re-use
                return;
            }

            //
            // Let's open the token to validate it
            //
            _validate(_token, _deviceId);
        }

        /// <summary>
        /// Opens an existing token
        /// </summary>
        /// <param name="token"></param>
        /// <param name="deviceId"></param>
        private void _validate(string token, string deviceId)
        {
            try
            {
                var bytes = Convert.FromBase64String(token);
                var decrypted = Encoding.UTF8.GetString(MachineKey.Unprotect(bytes, deviceId.Replace('~', '|') + ENCRYPTION_KEY));

                //{deviceId}~{applicationId}~{userId}~{expirationTimestamp}~{status}~{sand}
                string[] parts = decrypted.Split('~');

                if (deviceId != parts[0])
                {
                    // Decrypted token is not ok
                    Status = MobileTokenStatus.INVALID;
                    return;
                }

                // Get the other values
                DeviceId = deviceId;
                ApplicationId = Convert.ToInt64(parts[1]);
                UserId = parts[2];
                ExpirationDateTime = Convert.ToDateTime(parts[3]);

                Status = (MobileTokenStatus)Convert.ToInt32(parts[4]);

                Sand = parts[5];

                // Validate the application Id
                if (ApplicationId != REFERENCE_APP_ID)
                {
                    // Application_id is not ok
                    Status = MobileTokenStatus.INVALID;
                    return;
                }
				
                // Let's now validate the expiration timestamp
                TimeSpan span = (DateTime.Now - ExpirationDateTime);
                if (span.Seconds > 0)
                {
                    // The token has expired
                    Status = MobileTokenStatus.EXPIRED;
                    DeviceId = deviceId;       // Save the device id for later re-use
                }
                else if (Status != MobileTokenStatus.REQUIRES_AUTHENTICATION)
                {
                    // The token is valid
                    Status = MobileTokenStatus.OK;
                }
            }
            catch (Exception ex)
            {
                // Decrypted token is not ok
                Status = MobileTokenStatus.INVALID;
            }
        }

        /// <summary>
        /// Generates a token that does not contain any user related information
        /// This token is only used to provide authentication mechanism
        /// </summary>
        /// <param name="deviceId"></param>
        public void Generate(string deviceId)
        {
            Status = MobileTokenStatus.REQUIRES_AUTHENTICATION;
            UserId = "";
            ExpirationDateTime = DateTime.Now.AddHours(1);
            DeviceId = deviceId;

            _generateToken();
        }

        public void Generate(string deviceId, string userId)
        {
            Status = MobileTokenStatus.OK;
            UserId = userId;
            ExpirationDateTime = DateTime.Now.AddMonths(12);
            DeviceId = deviceId;

            _generateToken();
        }

        private void _generateToken()
        {
            _getSand();
            //{deviceId}~{applicationId}~{userId}~{expirationTimestamp}~{status}~{sand}
            string tokenInClear = $"{DeviceId}~{REFERENCE_APP_ID}~{UserId}~{ExpirationDateTime}~{(int)Status}~{Sand}";

            var originalStringBytes = Encoding.UTF8.GetBytes(tokenInClear);
            var encrypted = MachineKey.Protect(originalStringBytes, DeviceId + ENCRYPTION_KEY);

            Token =  Convert.ToBase64String(encrypted);
        }

        /// <summary>
        /// Generates some Sand (random padding value) so that a token, even with the very same pieces of information
        /// won't be the same at next time
        /// </summary>
        private void _getSand()
        {
            using (RandomNumberGenerator rng = new RNGCryptoServiceProvider())
            {
                byte[] tokenData = new byte[32];
                rng.GetBytes(tokenData);

                Sand = Convert.ToBase64String(tokenData);
            }
        }
    }

    public enum MobileTokenStatus: int
    {
        INVALID = -2,
        NO_TOKEN = -1,
        OK = 0,
        EXPIRED = 1,
        REQUIRES_AUTHENTICATION = 2
    }
}

The Controller

Here is the code of the controller:

namespace MyApplication.Controllers.api
{
    [WebApiMobileDeviceId]
    [AllowAnonymous]
    [RoutePrefix("api/Mobile")]
    public class MobileController : ApiController
    {
        [HttpGet]
        [Route("handshake")]
        public WebServiceResponseDto handshake()
        {
            WebServiceResponseDto response = new WebServiceResponseDto();
            MobileToken token = new MobileToken(Request);
            switch (token.Status)
            {
                case MobileTokenStatus.NO_TOKEN:
                case MobileTokenStatus.EXPIRED:
                    // There is no token or it is expired, so we need to generate one
                    token.Generate(token.DeviceId);

                    // And pass it to the application
                    response.data = token.Token;
                    break;

                case MobileTokenStatus.INVALID:
                    // The token is invalid
                    break;

                case MobileTokenStatus.OK:
                    // The token is valid
                    break;
            }

            response.status = token.Status.ToString();
            return response;
        }

        [HttpPost]
        [Route("login")]
        public async Task<WebServiceResponseDto> login([FromBody] MobileLoginDto model)
        {
            WebServiceResponseDto response = new WebServiceResponseDto(){status = WebServiceResponseDto.NOK};
            MobileToken token = new MobileToken(Request);
            if (token.Status == MobileTokenStatus.REQUIRES_AUTHENTICATION || token.Status == MobileTokenStatus.NO_TOKEN)
            {
                //
                // Let's proceed with the login
                // (insert your authentication code here)
                //
                string userId = _doLogin(model.Email, model.Password);

                if (userId != "")
                {
                    //
                    // We now need to update the token
                    //
                    token.Generate(token.DeviceId, webUser.UserId);

                    //
                    // As everything went ok, let's return the token
                    //
                    response.data = token.Token;
                    response.status = WebServiceResponseDto.TOKEN;
                }
            }
            return response;
        }

        [HttpGet]
        [Route("MethodABC/{data}")]
        public WebServiceResponseDto MethodABC(string data)
        {
            MobileToken token = new MobileToken(Request);

            var service = new MethodServices();
            return service.DoSomething(token.UserId, data);
        }
	}
}

Filters

In order to increase the security, you may also implement some filters which ensure that some pieces of information are always present in the header.

The following code gives an example of such filter, which rejects any request where the header would not contain any device identity.

namespace MyApplication.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class WebApiMobileDeviceIdAttribute : System.Web.Http.Filters.ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            //
            // Try to retrieve the mobile device UUID from the header
            //
            IEnumerable<string> headerValues;
            string mobileUuid = "";
            if (actionContext.Request.Headers.TryGetValues("X-DEVICE-ID", out headerValues))
            {
                mobileUuid = headerValues.First();
            }

            //
            // If we could not find it, then no need to go any further
            //
            if (String.IsNullOrEmpty(mobileUuid))
            {
                actionContext.Response = new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.Forbidden
                };
            }
            else
            {
                base.OnActionExecuting(actionContext);
            }
        }
    }
}

Conclusion

As you can see, thanks to the notion of token, it is very difficult to break or hack the communication.

I hope that the code is self-explanatory and easy to understand.

Stay tuned for other articles and, as usual, I wish you a happy coding.