Click here to Skip to main content
15,867,453 members
Articles / Web Development / ASP.NET

Securing ASP.NET Web API using Custom Token Based Authentication

Rate me:
Please Sign up or sign in to vote.
4.92/5 (63 votes)
20 Apr 2017CPOL18 min read 199.3K   5.3K   99   28
In this article, we are going to learn how to secure asp.net web API using custom token based authentication.

Introduction

In the modern era of development, we use web API for various purposes for sharing data, or for binding grid, drop-down list, and other controls, but if we do not secure this API, then other people who are going to access your web application or service can misuse it in some or the other way and also we are into the era of client-side framework (JavaScript, Angular js, react js, express js, common js, etc.) if you are using one of these client-side frameworks, then you are using web service or web API. It is true for getting or posting data to server and being on client side is less secure, you need to add extra efforts to secure it.

In this article, we are going to learn that extra part, the process of securing Web API begins with registering process. In this part, we are first going to register a user, after user registration, next user who is registered is going to login into application, after login into application, User needs to register a company which is going to use this service, after company registration, the next step we are going to get ClientID and ClientSecert keys.

After getting keys, next we are going to use these keys for authentication. The first request to access API must come with valid ClientID and ClientSecert. Next, it will validate keys and then it is going to provide Token in response, this token you need to use in every request to authenticate that you are a valid user and this Token expires in 30 minutes, but if you want to provide custom time according to your need, you can do it.

Also, this token is secured using AES 256 encryption algorithm.

Process

  1. Register User
  2. Login
  3. Register Company
  4. Get ClientID and ClientSecert
  5. Authenticate to get Token
  6. Use Token for Authenticating and accessing Data

Database Parts

In this part, we have created the database with name "MusicDB" and it has five tables which we are going to use in the application.

Image 1

Image 2

  1. Register User: Stores user information
  2. Register Company: Stores Companies information
  3. Client Keys: Stores ClientID and ClientSecert
  4. TokensManager: Stores all token information
  5. Music Store: Stores all Music information which is accessed by Clients

Image 3

Creating WEB API Application

In this part, we are going to create simple Web API application for creating that I have chosen "ASP.NET Web Application (.NET Framework)" template and named the project as "MusicAPIStore" and next, we are going to choose a template as "Web API" to create a project.

Image 4

Image 5

After creating project, below is the complete View of the project.

Image 6

Using Entity Framework Code First Approach to Connecting Application to Database

The first step we are going to add connection string of database in Web.config.

Image 7

The second step we are going to add Models according to tables in "MusicDB" database.

In the below snapshot, we have created Model of all Tables with the same name as Table name.

Image 8

The third step creating a class with name DatabaseContext which is going to inherit DbContext class than inside this class we are going to add a DbSet object.

Image 9

Code Snippet of Databasecontext Class

C#
using MusicAPIStore.Models;
using System.Data.Entity;

namespace MusicAPIStore.Context
{
    public class DatabaseContext : DbContext
    {
        public DatabaseContext() : base("DefaultConnection")
        {

        }
        
        public DbSet<RegisterUser> RegisterUser { get; set; }
        public DbSet<RegisterCompany> RegisterCompany { get; set; }
        public DbSet<TokensManager> TokensManager { get; set; }
        public DbSet<ClientKeys> ClientKeys { get; set; }
        public DbSet<MusicStore> MusicStore { get; set; }
    }
}

After completing with adding DatabaseContext class, next we are going to add folder with name Repository to Project.

Adding Repository Folder to Application

In this application, we are going to use repository pattern.

For storing interfaces and concrete class, we have to add Repository folder to the application.

Image 10

We have completed adding Repository folder to the application. Next, we are going to add RegisterUser, Model, Interface, Concrete class, Controller, and Views.

Register User

Image 11

  1. RegisterUser Model
  2. Adding IRegisterUser Interface
  3. Adding RegisterUserConcrete
  4. RegisterUserConcrete will inherit IRegisterUser
  5. Adding Controller
  6. Adding View

Let’s see RegisterUser Model.

1. RegisterUser Model

C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MusicAPIStore.Models
{
    [Table("RegisterUser")]
    public class RegisterUser
    {
        [Key]
        public int UserID { get; set; }

        [Required(ErrorMessage = "Required Username")]
        [StringLength(30, MinimumLength = 2, 
         ErrorMessage = "Username Must be Minimum 2 Charaters")]
        public string Username { get; set; }

        [DataType(DataType.Password)]
        [Required(ErrorMessage = "Required Password")]
        [MaxLength(30,ErrorMessage = "Password cannot be Greater than 30 Charaters")]
        [StringLength(31, MinimumLength = 7 , 
         ErrorMessage ="Password Must be Minimum 7 Charaters")]
        public string Password { get; set; }
        public DateTime CreateOn { get; set; }

    [Required(ErrorMessage = "Required EmailID")]
    [RegularExpression(@"[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}",
     ErrorMessage = "Please enter Valid Email ID")]
        public string EmailID { get; set; }
    }
}

After adding RegisterUser Model, next we are going to add Interface IRegisterUser in which we are going to declare methods which we require.

2. IRegisterUser Interface

We are going to add IRegisterUser interface in Repository folder which we have created.

This interface contains four methods:

  1. Add (Inserting User Data into database)
  2. ValidateRegisteredUser

    (Validating User already exists in database or not returns Boolean value)

  3. ValidateUsername

    (Validating Username already existed in database or not returns Boolean value)

  4. GetLoggedUserID (Gets UserID by Username and Password)

Code Snippet

C#
using MusicAPIStore.Models;

namespace MusicAPIStore.Repository
{
    public interface IRegisterUser
    {
        void Add(RegisterUser registeruser);
        bool ValidateRegisteredUser(RegisterUser registeruser);
        bool ValidateUsername(RegisterUser registeruser);
        int GetLoggedUserID(RegisterUser registeruser);
    }
}

After adding IRegisterUser Interface, next we are going to add RegisterUserConcrete Class in Repository folder.

3. RegisterUserConcrete

We are going to add RegisterUserConcrete Class in Repository folder which we have created.

After adding RegisterUserConcrete class, next we are going to inherit it from IRegisterUser Interface and implement all methods inside of IRegisterUser Interface.

Image 12

Code Snippet

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MusicAPIStore.Models;
using MusicAPIStore.Context;

namespace MusicAPIStore.Repository
{
    public class RegisterUserConcrete : IRegisterUser
    {
        DatabaseContext _context;
        public RegisterUserConcrete()
        {
            _context = new DatabaseContext();
        }

        public void Add(RegisterUser registeruser)
        {
            _context.RegisterUser.Add(registeruser);
            _context.SaveChanges();
        }

        public int GetLoggedUserID(RegisterUser registeruser)
        {
            var usercount = (from User in _context.RegisterUser
                             where User.Username == registeruser.Username && 
                                   User.Password == registeruser.Password
                             select User.UserID).FirstOrDefault();

            return usercount;
        }

        public bool ValidateRegisteredUser(RegisterUser registeruser)
        {
            var usercount = (from User in _context.RegisterUser
                              where User.Username == registeruser.Username && 
                              User.Password == registeruser.Password
                        select User).Count();
            if (usercount > 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public bool ValidateUsername(RegisterUser registeruser)
        {
            var usercount = (from User in _context.RegisterUser
                             where User.Username == registeruser.Username
                             select User).Count();
            if (usercount > 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

After adding RegisterUserConcrete class, next we are going to Add RegisterUserController.

4. Adding RegisterUser Controller

For adding controller, just right on Controllers folder, inside that select Add a Inside that select Controller after selecting Controller a new dialog with name "Add Scaffold" will Pop up for choosing type of controller to add in that we are just going to select "MVC5Controller - Empty" and click on Add button. After that, a new dialog with name "Add Controller" will pop up asking for Controller name. Here, we are going to name controller as RegisterUserController and click on Add button.

Note: Next, we are going to initialize an object in Constructor of RegisterUser Controller.

Image 13

Code Snippet

C#
using MusicAPIStore.AES256Encryption;
using MusicAPIStore.Models;
using MusicAPIStore.Repository;
using System;
using System.Web.Mvc;

namespace MusicAPIStore.Controllers
{
    public class RegisterUserController : Controller
    {
        IRegisterUser repository;
        public RegisterUserController()
        {
            repository = new RegisterUserConcrete();
        }
        // GET: RegisterUser/Create
        public ActionResult Create()
        {
            return View(new RegisterUser());
        }

        // POST: RegisterUser/Create
        [HttpPost]
        public ActionResult Create(RegisterUser RegisterUser)
        {
            try
            {
                if (!ModelState.IsValid)
                {
                    return View("Create", RegisterUser);
                }

                // Validating Username 
                if (repository.ValidateUsername(RegisterUser))
                {
                    ModelState.AddModelError("", "User is Already Registered");
                    return View("Create", RegisterUser);
                }
                RegisterUser.CreateOn = DateTime.Now;

                // Encrypting Password with AES 256 Algorithm
                RegisterUser.Password = EncryptionLibrary.EncryptText(RegisterUser.Password);

                // Saving User Details in Database
                repository.Add(RegisterUser);
                TempData["UserMessage"] = "User Registered Successfully";
                ModelState.Clear();
                return View("Create", new RegisterUser());
            }
            catch
            {
                return View();
            }
        }
    }
}

In this Controller, we have added two Action methods of Create one for handling [HttpGet] request and other for handling [HttpPost] request. In [HttpPost] request, we are going to first Validate Is Username already exists or not. If not, then we are going to Create User, next we are also taking Password as input which we cannot store in database as clear text. We need to store it in encrypted format. For doing that, we are going to use AES 256 algorithm.

5. RegisterUser View

Image 14

After Registering a User, below are details which get stored in RegisterUser Table.

6. RegisterUser Table

Image 15

After completing with Registration part, next we are going to create a Login page.

2. Login

Image 16

In this part, we are going to add Login Controller with Login and Logout Action method in it.

Here, we do not need to add a new interface or concrete class because we have already created a RegisterUserConcrete method which has a method which is required by Login Controller.

Code Snippet

C#
using MusicAPIStore.AES256Encryption;
using MusicAPIStore.Models;
using MusicAPIStore.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MusicAPIStore.Controllers
{
    public class LoginController : Controller
    {
        IRegisterUser _IRegisterUser;
        public LoginController()
        {
            _IRegisterUser = new RegisterUserConcrete();
        }

        public ActionResult Login()
        {
            return View(new RegisterUser());
        }

        [HttpPost]
        public ActionResult Login(RegisterUser RegisterUser)
        {
            try
            {
                if (string.IsNullOrEmpty(RegisterUser.Username) && 
                   (string.IsNullOrEmpty(RegisterUser.Password)))
                {
                    ModelState.AddModelError("", "Enter Username and Password");
                }
                else if (string.IsNullOrEmpty(RegisterUser.Username))
                {
                    ModelState.AddModelError("", "Enter Username");
                }
                else if (string.IsNullOrEmpty(RegisterUser.Password))
                {
                    ModelState.AddModelError("", "Enter Password");
                }
                else
                {
                    RegisterUser.Password = 
                    EncryptionLibrary.EncryptText(RegisterUser.Password);

                    if (_IRegisterUser.ValidateRegisteredUser(RegisterUser))
                    {
                        var UserID = _IRegisterUser.GetLoggedUserID(RegisterUser);
                        Session["UserID"] = UserID;
                        return RedirectToAction("Create", "RegisterCompany");
                    }
                    else
                    {
                        ModelState.AddModelError("", "User is Already Registered");
                        return View("Create", RegisterUser);
                    }
                }

                return View("Login", RegisterUser);
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Logout()
        {
            Session.Abandon();
            return RedirectToAction("Login", "Login");
        }
    }
}

Image 17

After completing with login, next we going to create Register Company Interface and Concrete in Repository folder and after that, we are going add RegisterCompanyController and Action method along with View.

Note: We are using AES 256 algorithm with salt for encryption and decryption.

3. Register Company

Image 18

In this part, we are going to register a company and to this company, we are going to generate ClientID and ClientSecret.

Let’s start with adding an interface with name IRegisterCompany which will contain all methods which need to be implemented by Concrete class.

Code Snippet of IRegisterCompany

C#
using MusicAPIStore.Models;
using System.Collections.Generic;

namespace MusicAPIStore.Repository
{
    public interface IRegisterCompany
    {
        IEnumerable<RegisterCompany> ListofCompanies(int UserID);
        void Add(RegisterCompany entity);
        void Delete(RegisterCompany entity);
        void Update(RegisterCompany entity);
        RegisterCompany FindCompanyByUserId(int UserID);
        bool ValidateCompanyName(RegisterCompany registercompany);
        bool CheckIsCompanyRegistered(int UserID);
    }
}

We have declared all methods which are required by Register Company Controller. Next, we are going to add Concrete class with name RegisterCompanyConcrete which is going to implement the IRegisterCompany interface.

Image 19

Code Snippet

C#
using System;
using System.Collections.Generic;
using System.Linq;
using MusicAPIStore.Models;
using MusicAPIStore.Context;

namespace MusicAPIStore.Repository
{
    public class RegisterCompanyConcrete : IRegisterCompany
    {
        DatabaseContext _context;
        public RegisterCompanyConcrete()
        {
            _context = new DatabaseContext();
        }

        public IEnumerable<RegisterCompany> ListofCompanies(int UserID)
        {
            try
            {
                var CompanyList = (from companies in _context.RegisterCompany
                               where companies.UserID == UserID
                               select companies).ToList();
                return CompanyList;
            }
            catch (Exception)
            {
                throw;
            }
        }

        public void Add(RegisterCompany entity)
        {
            try
            {
                _context.RegisterCompany.Add(entity);
                _context.SaveChanges();
            }
            catch (Exception)
            {

                throw;
            }
        }

        public void Delete(RegisterCompany entity)
        {
            try
            {
                var itemToRemove = _context.RegisterCompany.SingleOrDefault
                                   (x => x.CompanyID == entity.CompanyID);
                _context.RegisterCompany.Remove(itemToRemove);
                _context.SaveChanges();
            }
            catch (Exception)
            {
                throw;
            }
        }

        public RegisterCompany FindCompanyByUserId(int UserID)
        {
            try
            {
                var Company = 
                _context.RegisterCompany.SingleOrDefault(x => x.UserID == UserID);
                return Company;
            }
            catch (Exception)
            {

                throw;
            }
        }
     
        public bool ValidateCompanyName(RegisterCompany registercompany)
        {
            try
            {
                var result = (from company in _context.RegisterCompany
                              where company.Name == registercompany.Name && 
                                    company.EmailID == registercompany.EmailID
                              select company).Count();
                if (result > 0)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception)
            {

                throw;
            }
        }

        public bool CheckIsCompanyRegistered(int UserID)
        {
            try
            {
                var companyExists = _context.RegisterCompany.Any(x => x.UserID == UserID);

                if (companyExists)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

After we are done with implementing all methods of interface, next we are going to Add RegisterCompany Controller.

Adding RegisterCompany Controller

In this part, we are going to add RegisterCompany Controller with Create and index Action method in it.

In index Action method, we are going to get list of Companies and in creating [HttpGet] Action method, we are going to get a list of companies on the basis of UserID, and in creating [HttpPost] Action method, we are going to save data of company in database and before that, we are going to check whether the company name already exists or not. If company name exists, then we are going to show error message "Company is Already Registered".

For adding Controller, follow the same step which we have used for adding RegisterUser Controller. After adding controller, we have just manually added a constructor and three Action methods in it as shown below.

Code Snippet

C#
using MusicAPIStore.Context;
using MusicAPIStore.Models;
using MusicAPIStore.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MusicAPIStore.Filters;
namespace MusicAPIStore.Controllers
{
    [ValidateSessionAttribute]
    public class RegisterCompanyController : Controller
    {
        IRegisterCompany _IRegister;
        public RegisterCompanyController()
        {
            _IRegister = new RegisterCompanyConcrete();
        }

        // GET: Register
       public ActionResult Index()
        {
            var RegisterList = _IRegister.ListofCompanies(Convert.ToInt32(Session["UserID"]));
            return View(RegisterList);
        }

        // GET: Register/Create
        public ActionResult Create()
        {
            var Company = _IRegister.CheckIsCompanyRegistered
                          (Convert.ToInt32(Session["UserID"]));
            if (Company)
            {
                return RedirectToAction("Index");
            }
            return View();
        }

        // POST: Register/Create
        [HttpPost]
        public ActionResult Create(RegisterCompany RegisterCompany)
        {
            try
            {
                if (!ModelState.IsValid)
                {
                    return View("Create", RegisterCompany);
                }

                if (_IRegister.ValidateCompanyName(RegisterCompany))
                {
                    ModelState.AddModelError("", "Company is Already Registered");
                    return View("Create", RegisterCompany);
                }
                RegisterCompany.UserID = Convert.ToInt32(Session["UserID"]);
                RegisterCompany.CreateOn = DateTime.Now;
                _IRegister.Add(RegisterCompany);

                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }     
    }
}

After completing with adding Controller and its Action method, next we are going to add View to Create and index Action method.

Adding Index and Company View

In this part, we are going to add Views. For adding View, just right click inside Action method, then choose Add View from Menu list a new dialog will pop up with name "Add View". Next, we do not require to provide name to view it is set to default which is Action method name in Template choose template to depend on which view you want to create (Create, Index) and long with that choose Model (RegisterCompany). Click on Add Button.

Image 20

After adding View, just save the application and run, then the first step is to login into the application as you log in, you will see RegisterCompany View, just register your Company.

Image 21

After creating a company, you will able to see Index View as shown in the below snapshot with company details which you have filled.

Image 22

Now we have Registered Company next step is to Get Application ClientID and Client Secret.

Generate ClientID and Client Secret Keys

Image 23

In this part, we are going to Generate Unique ClientID and Client Secret keys for each company which is registered.

We generate these keys using RNGCryptoServiceProvider algorithm.

Code Snippet of Key Generator Class

C#
public static class KeyGenerator
{
    public static string GetUniqueKey(int maxSize = 15)
    {
        char[] chars = new char[62];
        chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
        byte[] data = new byte[1];
        using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
        {
            crypto.GetNonZeroBytes(data);
            Data = new byte[maxSize];
            crypto.GetNonZeroBytes(data);
        }
        StringBuilder result = new StringBuilder(maxSize);
        foreach (byte b in data)
        {
            result.Append(chars[b % (chars.Length)]);
        }
        return result.ToString();
    }
}

After generating ClientID and Client Secret keys, we are going to insert these Keys with UserID in ClientKey table and display to User for using it.

And we have also provided ReGenerate Keys button on GenerateKeys View such that if the user wants, he can generate a new pair of Keys he can do by clicking on this button again, these values are updated to the database according to UserID.

Let’s begin with having a look at Model ClientKey first.

Image 24

After adding ClientKey Model in Model folder, next we are going to move forward by adding Interface with name IClientKeys and in this interface, we are going to declare five methods. The methods names inside interface are self-explanatory.

Adding Interface IClientKeys

C#
using MusicAPIStore.Models;

namespace MusicAPIStore.Repository
{
    public interface IClientKeys
    {
        bool IsUniqueKeyAlreadyGenerate(int UserID);
        void GenerateUniqueKey(out string ClientID, out string ClientSecert);
        int SaveClientIDandClientSecert(ClientKey ClientKeys);
        int UpdateClientIDandClientSecert(ClientKey ClientKeys);
        ClientKey GetGenerateUniqueKeyByUserID(int UserID);
    }
}

After adding Interface and declaring methods inside it, next we are going to add Concrete class which is going to inherit this IClientKeys interface.

Adding Class ClientKeysConcrete

In this part, we are going to add Concrete class with name ClientKeysConcrete which is going to inherit IClientKeys interface and implement all methods in it.

C#
using System.Linq;
using MusicAPIStore.Models;
using MusicAPIStore.Context;
using MusicAPIStore.AES256Encryption;
using System.Data.Entity;

namespace MusicAPIStore.Repository
{
    public class ClientKeysConcrete : IClientKeys
    {
        DatabaseContext _context;
        public ClientKeysConcrete()
        {
            _context = new DatabaseContext();
        }

        public void GenerateUniqueKey(out string ClientID, out string ClientSecert)
        {
            ClientID = EncryptionLibrary.KeyGenerator.GetUniqueKey();
            ClientSecert = EncryptionLibrary.KeyGenerator.GetUniqueKey();
        }

        public bool IsUniqueKeyAlreadyGenerate(int UserID)
        {
            bool keyExists = _context.ClientKeys.Any
                             (clientkeys => clientkeys.UserID.Equals(UserID));

            if (keyExists)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public int SaveClientIDandClientSecert(ClientKey ClientKeys)
        {
            _context.ClientKeys.Add(ClientKeys);
            return _context.SaveChanges();
        }

        public ClientKey GetGenerateUniqueKeyByUserID(int UserID)
        {
            var clientkey = (from ckey in _context.ClientKeys
                            where ckey.UserID  == UserID
                            select ckey).FirstOrDefault();
            return clientkey;
        }


        public int UpdateClientIDandClientSecert(ClientKey ClientKeys)
        {
            _context.Entry(ClientKeys).State = EntityState.Modified;
            _context.SaveChanges();
            return _context.SaveChanges();
        }
    }
}

If you had view on ClientKeysConcrete class, there is "GenerateUniqueKey" method which generates unique ClientID and Client Secret keys and returns, next we are going to have look at the method "IsUniqueKeyAlreadyGenerate". This method checks whether ClientID and Client Secret keys is already generated for this user or not, next we are going to have look on method "SaveClientIDandClientSecert" as this method name says it is going to save ClientID and Client Secret keys details in database.

If a user logs out from the portal and if he visits the portal again to see his ClientID and Client Secret keys for getting those keys from the database, we have created "GetGenerateUniqueKeyByUserID" method.

The last method which we are going to see is "UpdateClientIDandClientSecert". This method is used to update ClientID and Client Secret keys when User clicks on ReGenerate Keys button on View which we are going to add in the meanwhile.

Image 25

Adding ApplicationKeys Controller

In this part, we are going to add ApplicationKeys Controller with two GenerateKeys Action methods in it.

One Action method handles [HttpGet] part and another is going to handle [HttpPost] part.

In [HttpGet] GenerateKeys Action method, we are going the first check is Keys Already Generate or not. If not, then we are going to Generate new Keys and save that keys in Database and Display to Users.

The [HttpPost] GenerateKeys Action method is called when User clicks on "ReGenerate Keys" button and this we are going to Generate new Keys and save those keys in database and display to users.

For adding Controller, follow the same step which we have used for adding RegisterUser Controller. After adding controller, we have just manually added a constructor and two Action methods in it as shown below.

Code Snippet of ApplicationKeys Controller

C#
using MusicAPIStore.Filters;
using MusicAPIStore.Models;
using MusicAPIStore.Repository;
using System;
using System.Web.Mvc;

namespace MusicAPIStore.Controllers
{
    [ValidateSessionAttribute]
    public class ApplicationKeysController : Controller
    {
        IClientKeys _IClientKeys;
        IRegisterCompany _IRegisterCompany;
        public ApplicationKeysController()
        {
            _IClientKeys = new ClientKeysConcrete();
            _IRegisterCompany = new RegisterCompanyConcrete();
        }

        // GET: ApplicationKeys/GenerateKeys
        [HttpGet]
        public ActionResult GenerateKeys()
        {
            try
            {
                ClientKey clientkeys = new ClientKey();

                // Validating ClientID and ClientSecert already Exists
                var keyExists = _IClientKeys.IsUniqueKeyAlreadyGenerate
                                (Convert.ToInt32(Session["UserID"]));

                if (keyExists)
                {
                    // Getting Generate ClientID and ClientSecert Key By UserID
                    clientkeys = _IClientKeys.GetGenerateUniqueKeyByUserID
                                 (Convert.ToInt32(Session["UserID"]));
                }
                else
                {
                    string clientID=string.Empty;
                    string clientSecert = string.Empty;
                    int companyId = 0;

                    var company = _IRegisterCompany.FindCompanyByUserId
                                  (Convert.ToInt32(Session["UserID"]));
                    companyId = company.CompanyID;

                    //Generate Keys
                    _IClientKeys.GenerateUniqueKey(out clientID, out clientSecert);

                    //Saving Keys Details in Database
                    clientkeys.ClientKeyID = 0;
                    clientkeys.CompanyID = companyId;
                    clientkeys.CreateOn = DateTime.Now;
                    clientkeys.ClientID = clientID;
                    clientkeys.ClientSecret = clientSecert;
                    clientkeys.UserID = Convert.ToInt32(Session["UserID"]);
                    _IClientKeys.SaveClientIDandClientSecert(clientkeys);
                }

                return View(clientkeys);
            }
            catch (Exception)
            {
                throw;
            }
        }

        // POST: ApplicationKeys/GenerateKeys
        [HttpPost]
        public ActionResult GenerateKeys(ClientKey clientkeys)
        {
            try
            {
                string clientID = string.Empty;
                string clientSecert = string.Empty;

                //Generate Keys
                _IClientKeys.GenerateUniqueKey(out clientID, out clientSecert);

                //Updating ClientID and ClientSecert 
                var company = _IRegisterCompany.FindCompanyByUserId
                              (Convert.ToInt32(Session["UserID"]));
                clientkeys.CompanyID = company.CompanyID;
                clientkeys.CreateOn = DateTime.Now;
                clientkeys.ClientID = clientID;
                clientkeys.ClientSecret = clientSecert;
                clientkeys.UserID = Convert.ToInt32(Session["UserID"]);
                _IClientKeys.UpdateClientIDandClientSecert(clientkeys);

                return RedirectToAction("GenerateKeys");
            }
            catch (Exception ex)
            {
                return View();
            }
        }
    }
}

After completing with adding Controller and its Action method, next we are going to add View to GenerateKeys Action method.

Adding GenerateKeys View

In this part, we are going to add Views for adding View. Just right click inside Action method, then choose Add View from Menu list, a new dialog will pop up with name "Add View". Next, we do not require to provide name to view. It is set to default which is Action method name. In Template, choose template to depend on upon which view you want to create (Create) and along with that, choose Model (ClientKey). Click on Add Button.

Image 26

After adding View, just save the application and run, then the first step is to login to the application. As you log in, you will see RegisterCompany Index View.

On the Master page, we have added a link to show GenerateKeys View.

Image 27

Now on clicking on Application secret link, it will show GenerateKeys View.

Image 28

Now we have completed generating ClientID and Client Secret. Next, we are going to add Authenticate Controller for authenticating ClientID and Client Secret and return Token key in Response.

Authentication Mechanism

Image 29

In this process, we have provided ClientID and Client Secret to Client and now we need to develop authenticating mechanism where user will send this ClientID and Client Secret to Server, then we are going to validate these keys with database and after that, we are going to return token to User in response if keys are valid then only, else we are going to return Error Message.

Let’s start with adding interface with name IAuthenticate and declaring methods in it.

Adding IAuthenticate Interface

C#
using MusicAPIStore.Models;
using System;

namespace MusicAPIStore.Repository
{
    public interface IAuthenticate
    {
        ClientKey GetClientKeysDetailsbyCLientIDandClientSecert
                  (string clientID , string clientSecert);
        bool ValidateKeys(ClientKey ClientKeys);
        bool IsTokenAlreadyExists(int CompanyID);
        int DeleteGenerateToken(int CompanyID);
        int InsertToken(TokensManager token);
        string GenerateToken(ClientKey ClientKeys, DateTime IssuedOn);
    }
}

We have declared five methods in the IAuthenticate interface:

  1. GetClientKeysDetailsbyCLientIDandClientSecert

    This method takes ClientID and Client Secret as input and gets data from the database on the basis of it.

  2. ValidateKeys

    This method takes ClientKey Model as input in which it checks ClientID and Client Secret passed by users is valid or not.

  3. IsTokenAlreadyExists

    This method takes CompanyID as input and checks whether Token is already generated for it.

  4. DeleteGenerateToken

    This method takes CompanyID as input and deletes token which is already generated on the basis of the CompanyID parameter.

  5. InsertToken

    This method takes TokensManager model as input parameter for saving Token values in database.

  6. GenerateToken

    This method generates and returns Token.

Adding Authenticate Concrete Class

In this part, we are going to add Concrete class with name AuthenticateConcrete which is going to inherit IAuthenticate interface and implement all methods in it.

C#
using System;
using System.Linq;
using MusicAPIStore.Models;
using MusicAPIStore.Context;
using MusicAPIStore.AES256Encryption;
using static MusicAPIStore.AES256Encryption.EncryptionLibrary;

namespace MusicAPIStore.Repository
{
    public class AuthenticateConcrete : IAuthenticate
    {
        DatabaseContext _context;

        public AuthenticateConcrete()
        {
            _context = new DatabaseContext();
        }

        public ClientKey GetClientKeysDetailsbyCLientIDandClientSecert
               (string clientID, string clientSecert)
        {
            try
            {
                var result = (from clientkeys in _context.ClientKeys
                              where clientkeys.ClientID == clientID && 
                                    clientkeys.ClientSecret == clientSecert
                              select clientkeys).FirstOrDefault();
                return result;
            }
            catch (Exception)
            {
                throw;
            }
        }

        public bool ValidateKeys(ClientKey ClientKeys)
        {
            try
            {
                var result = (from clientkeys in _context.ClientKeys
                              where clientkeys.ClientID == ClientKeys.ClientID && 
                                    clientkeys.ClientSecret == ClientKeys.ClientSecret
                              select clientkeys).Count();
                if (result > 0)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception)
            {

                throw;
            }
        }

        public bool IsTokenAlreadyExists(int CompanyID)
        {
            try
            {
                var result = (from token in _context.TokensManager
                              where token.CompanyID == CompanyID
                              select token).Count();
                if (result > 0)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception)
            {

                throw;
            }
        }

        public int DeleteGenerateToken(int CompanyID)
        {
            try
            {
                var token = _context.TokensManager.SingleOrDefault
                            (x => x.CompanyID == CompanyID);
                _context.TokensManager.Remove(token);
                return _context.SaveChanges();
            }
            catch (Exception)
            {
                throw;
            }
        }

        public string GenerateToken(ClientKey ClientKeys, DateTime IssuedOn)
        {
            try
            {
                string randomnumber =
                   string.Join(":", new string[]
                   {   Convert.ToString(ClientKeys.UserID),
                KeyGenerator.GetUniqueKey(),
                Convert.ToString(ClientKeys.CompanyID),
                Convert.ToString(IssuedOn.Ticks),
                ClientKeys.ClientID
                   });

                return EncryptionLibrary.EncryptText(randomnumber);
            }
            catch (Exception)
            {
                throw;
            }
        }

        public int InsertToken(TokensManager token)
        {
            try
            {
                _context.TokensManager.Add(token);
                return _context.SaveChanges();
            }
            catch (Exception)
            {

                throw;
            }
        }
    }
}

Snapshot after Adding Interface IAuthenticate and AuthenticateConcrete

Image 30

After completing with adding Interface IAuthenticate and AuthenticateConcrete, next, we are going to add ApiController with name Authenticate.

Adding Authenticate Controller

In this part, we are going to add ApiController with name Authenticate and it will have a single Action method in it with name Authenticate which takes ClientKey Model as input from [FromBody].

C#
using MusicAPIStore.Models;
using MusicAPIStore.Repository;
using System;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace MusicAPIStore.Controllers
{
    public class AuthenticateController : ApiController
    {
        IAuthenticate _IAuthenticate;
        public AuthenticateController()
        {
            _IAuthenticate = new AuthenticateConcrete();
        }

        // POST: api/Authenticate
        public HttpResponseMessage Authenticate([FromBody]ClientKey ClientKeys)
        {
            if (string.IsNullOrEmpty(ClientKeys.ClientID) && 
                string.IsNullOrEmpty(ClientKeys.ClientSecret))
            {
                var message = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
                message.Content = new StringContent("Not Valid Request");
                return message;
            }
            else
            {
                if (_IAuthenticate.ValidateKeys(ClientKeys))
                {
                    var clientkeys = 
                    _IAuthenticate.GetClientKeysDetailsbyCLientIDandClientSecert
                    (ClientKeys.ClientID, ClientKeys.ClientSecret);

                    if (clientkeys == null)
                    {
                        var message = new HttpResponseMessage(HttpStatusCode.NotFound);
                        message.Content = new StringContent("InValid Keys");
                        return message;
                    }
                    else
                    {
                        if (_IAuthenticate.IsTokenAlreadyExists(clientkeys.CompanyID))
                        {
                            _IAuthenticate.DeleteGenerateToken(clientkeys.CompanyID);

                            return GenerateandSaveToken(clientkeys);
                        }
                        else
                        {
                            return GenerateandSaveToken(clientkeys);
                        }
                    }
                }
                else
                {
                    var message = new HttpResponseMessage(HttpStatusCode.NotFound);
                    message.Content = new StringContent("InValid Keys");
                    return new HttpResponseMessage 
                           { StatusCode = HttpStatusCode.NotAcceptable };
                }
            }
        }
      
        [NonAction]
        private HttpResponseMessage GenerateandSaveToken(ClientKey clientkeys)
        {
            var IssuedOn = DateTime.Now;
            var newToken = _IAuthenticate.GenerateToken(clientkeys, IssuedOn);
            TokensManager token = new TokensManager();
            token.TokenID = 0;
            token.TokenKey = newToken;
            token.CompanyID = clientkeys.CompanyID;
            token.IssuedOn = IssuedOn;
            token.ExpiresOn = DateTime.Now.AddMinutes(Convert.ToInt32
                              (ConfigurationManager.AppSettings["TokenExpiry"]));
            token.CreatedOn = DateTime.Now;
            var result = _IAuthenticate.InsertToken(token);

            if (result == 1)
            {
                HttpResponseMessage response = new HttpResponseMessage();
                response = Request.CreateResponse(HttpStatusCode.OK, "Authorized");
                response.Headers.Add("Token", newToken);
                response.Headers.Add("TokenExpiry", 
                         ConfigurationManager.AppSettings["TokenExpiry"]);
                response.Headers.Add("Access-Control-Expose-Headers", "Token,TokenExpiry");
                return response;
            }
            else
            {
                var message = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
                message.Content = new StringContent("Error in Creating Token");
                return message;
            }
        }
    }
}

Let’s understand the code:

C#
public HttpResponseMessage Authenticate([FromBody]ClientKey ClientKeys)
        {
            if (string.IsNullOrEmpty(ClientKeys.ClientID) && 
                string.IsNullOrEmpty(ClientKeys.ClientSecret))
            {
                var message = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
                message.Content = new StringContent("Not Valid Request");
                return message;
            }
            else
            {
                if (_IAuthenticate.ValidateKeys(ClientKeys))
                {
                    var clientkeys = 
                        _IAuthenticate.GetClientKeysDetailsbyCLientIDandClientSecert
                                     (ClientKeys.ClientID, ClientKeys.ClientSecret);

                    if (clientkeys == null)
                    {
                        var message = new HttpResponseMessage(HttpStatusCode.NotFound);
                        message.Content = new StringContent("InValid Keys");
                        return message;
                    }
                    else
                    {
                        if (_IAuthenticate.IsTokenAlreadyExists(clientkeys.CompanyID))
                        {
                            _IAuthenticate.DeleteGenerateToken(clientkeys.CompanyID);

                            return GenerateandSaveToken(clientkeys);
                        }
                        else
                        {
                            return GenerateandSaveToken(clientkeys);
                        }
                    }
                }
                else
                {
                    var message = new HttpResponseMessage(HttpStatusCode.NotFound);
                    message.Content = new StringContent("InValid Keys");
                    return new HttpResponseMessage 
                           { StatusCode = HttpStatusCode.NotAcceptable };
                }
            }
        }

The first step in this part is that the Authenticate method takes ClientKey model as input and from this model, we are going to use only two parameters, ClientID and Client Secret. Next, we are first going to check this is parameters null or not. If it is Null, we are going to send HttpResponseMessage as "Not Valid Request" in response.

If ClientID and Client Secret are not null, then we are going to send ClientID and Client Secret to ValidateKeys method to check whether these Values passed already exist in the database or not.

If ClientID and Client Secret value exist in database, then we pass both values to "GetClientKeysDetailsbyCLientIDandClientSecert" method to get all ClientKey details. Here again, we check whether Keys are Valid or not, if not, then we are going to send HttpResponseMessage as "InValid Keys" in response.

If ClientID and Client Secret are Valid, then we get ClientKey Details from the database and from this Model, we pass CompanyID to "IsTokenAlreadyExists" method to check whether Token already exists in the database or not.

If Token already exists in database, then we are going to delete the old token and generate New token and insert new token in the database and we are also going to send Token in response to Client who has sent the request.

If Token does not exist in database, then we are going to generate token and insert new token in the database and we are also going to send Token in response to Client who has sent the request.

Now we have understood how to process work. Let’s try a real time example.

First to call Web API api/Authenticate method, we are going to use Postman web debugger.

For Downloading Postman Chrome App

Installing the Postman Chrome App

https://www.getpostman.com/docs/introduction

Image 31

After installing Postman Chrome App, now you can open Postman Chrome App. Below is a snapshot of Postman Chrome App.

Image 32

In the next step, we are going have a look at ClientID and ClientSecret because we need to send these Keys to Get Token from Server.

Getting Keys

Image 33

Next, we are going to set keys (ClientID and ClientSecret) in a FromBody request to Post.

Setting Values for Post Request in POSTMAN

Image 34

  1. Choose Post Request From Dropdownlist
  2. Set URL: http://localhost:4247/api/Authenticate
  3. We are going to send this Keys FromBody of Request.
  4. It will be raw data with Content Type as application/json
  5. Set 'ClientID' and 'ClientSecret'
    C#
    {
     'ClientID':'pGU6RJ8ELcVRZmN',
     'ClientSecret':'tiIfdZ3vh5IwGVm'
    }
  6. Click on Send Button to Send Request.

    Image 35

Response of Request

Image 36

In Response, we get Token and TokenExpiry.

Token: XbCsogSJXKLSq2TBUs0QZrbClRpiuXZFrfjKy0WRtEdPQYpA87Pav9KozmmKoNMd1W3Q8Hg8hoaGYKDtyTH2Rg==

TokenExpiry: 30

Token is only valid for 30 Minutes.

Image 37

After getting a response, let’s have a view of TokenManager table to see what fields got inserted in it.

Image 38

In the next step, we are going to add AuthorizeAttribute with name APIAuthorizeAttribute.

Adding AuthorizeAttribute (APIAuthorizeAttribute)

Image 39

In this part, we are going to create an AuthorizeAttribute with name "APIAuthorizeAttribute" in Filter folder.

For adding this AuthorizeAttribute, first we are going to a class with name "APIAuthorizeAttribute" and this class is going to inherit a class "AuthorizeAttribute" and implement all methods inside it.

Image 40

In this "APIAuthorizeAttribute", we are going to validate Token which is sent from the client.

  1. The first step we are going to receive Token from Client Header.
  2. After that, we are going to check whether this token is Null or not.
  3. Next, we are going to decrypt this Token.
  4. After decrypting this Token, we get string values as output.
  5. Next, we are going to split (‘:’) string values which we have received.
  6. After splitting, we get (UserID, Random Key, CompanyID, Ticks, ClientID) values.
  7. Next, we are going to pass (UserID, CompanyID, ClientID) to database to check whether this parameter which we have received is valid.
  8. After that, we are going to check Token Expiration.
  9. If something is throwing error in this step, we are going to return a false value.
  10. And if we have valid values and token is not expired, then it will return true.

Authorize Attribute Snapshot with Explanation

Image 41

Image 42

After having a view on Snapshot, you will get a detailed idea about how it works. Next, we are going to see the complete code snippet of APIAuthorizeAttribute.

APIAuthorizeAttribute Code Snippet

C#
using MusicAPIStore.AES256Encryption;
using MusicAPIStore.Context;
using System;
using System.Linq;
using System.Web.Http;
using System.Web.Http.Controllers;

namespace MusicAPIStore.Filters
{
    public class APIAuthorizeAttribute : AuthorizeAttribute
    {
        private DatabaseContext db = new DatabaseContext();
        public override void OnAuthorization(HttpActionContext filterContext)
        {
            if (Authorize(filterContext))
            {
                return;
            }
            HandleUnauthorizedRequest(filterContext);
        }
        protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
        {
            base.HandleUnauthorizedRequest(filterContext);
        }

        private bool Authorize(HttpActionContext actionContext)
        {
            try
            {
                var encodedString = actionContext.Request.Headers.GetValues("Token").First();

                bool validFlag = false;

                if (!string.IsNullOrEmpty(encodedString))
                {
                    var key = EncryptionLibrary.DecryptText(encodedString);

                    string[] parts = key.Split(new char[] { ':' });

                    var UserID = Convert.ToInt32(parts[0]);       // UserID
                    var RandomKey = parts[1];                     // Random Key
                    var CompanyID = Convert.ToInt32(parts[2]);    // CompanyID
                    long ticks = long.Parse(parts[3]);            // Ticks
                    DateTime IssuedOn = new DateTime(ticks);
                    var ClientID = parts[4];                      // ClientID 

                    // By passing this parameter 
                    var registerModel = (from register in db.ClientKeys
                                         where register.CompanyID == CompanyID
                                         && register.UserID == UserID
                                         && register.ClientID == ClientID
                                         select register).FirstOrDefault();

                    if (registerModel != null)
                    {
                        // Validating Time
                        var ExpiresOn = (from token in db.TokensManager
                                         where token.CompanyID == CompanyID
                                         select token.ExpiresOn).FirstOrDefault();

                        if ((DateTime.Now > ExpiresOn))
                        {
                            validFlag = false;
                        }
                        else
                        {
                            validFlag = true;
                        }
                    }
                    else
                    {
                        validFlag = false;
                    }
                }
                return validFlag;
            }
            catch (Exception)
            {
                return false;
            }
        }
    }
}

After completing with adding APIAuthorizeAttribute, next we need to apply this attribute to the controller.

But we have not added the controller (ApiController) on which we need to apply this attribute, let’s add ApiController with name "LatestMusic".

Adding ApiController LatestMusic

Image 43

In this part, we are going to add ApiController with name "LatestMusic" and it will have a single Action method in it with name GetMusicStore which will return a list of latest music from the database.

Image 44

LatestMusic Controller Code Snippet

C#
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using MusicAPIStore.Context;
using MusicAPIStore.Models;
using MusicAPIStore.Filters;

namespace MusicAPIStore.Controllers
{
    [APIAuthorizeAttribute]
    public class LatestMusicController : ApiController
    {
        private DatabaseContext db = new DatabaseContext();

        // GET: api/LatestMusic
        public List<MusicStore> GetMusicStore()
        {
            try
            {
                var listofSongs = db.MusicStore.ToList();
                return listofSongs;
            }
            catch (System.Exception)
            {
                throw;
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

In the above code snippet, if you have a close look, then you can see that we have applied Attribute at the controller level.

Image 45

Now, whenever anyone is going to access this controller, he needs to have a valid token. Only after that, he can access LatestMusic API.

Accessing LatestMusic API Controller

For accessing LatestMusic API, we need to send a valid token in the header.

Let’s start from authenticating process first after authenticating, we are going to receive a valid token in response and this token again we are going to send to access LatestMusic API.

  1. Authenticating process to get token:

    Image 46

  2. Sending token to access LatestMusic API and getting Top 10 Music hit list in response.

    Image 47

Response in Details

Image 48

Passing Invalid Token to Test Response

Image 49

Validating Token after Session Expiration

After sending request when the token is expired, it will show an error message as shown below:

Image 50

Table where token expiration date and time is stored:

Image 51

Conclusion

In this article, we have learned how to secure WEB API using token based authentication in a step by step manner and in detailed manner such that junior developer can also understand it very easily. Now, you can secure your most client based application using this process, and also server based application.

Thank you for reading. I hope you liked my article.

Image 52

History

  • 20th April, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
India India
Microsoft Most Valuable Professional
Code Project Most Valuable Author
C# Corner Most Valuable Professional

I am Senior Technical lead Working on.Net Web Technology
ASP.NET MVC,.Net Core,ASP.NET CORE, C#, SQL Server, MYSQL, MongoDB, Windows

Comments and Discussions

 
GeneralMy vote of 5 Pin
Sunasara Imdadhusen7-Jan-20 0:39
professionalSunasara Imdadhusen7-Jan-20 0:39 
Questionnice article. thank you Pin
djs8329-Jan-19 1:34
djs8329-Jan-19 1:34 
Praisesuperb excellent article Pin
naren 81986145-Dec-18 13:33
naren 81986145-Dec-18 13:33 
QuestionAmazing article thanks! One issue... Pin
Member 1406572624-Nov-18 9:45
Member 1406572624-Nov-18 9:45 
QuestionStrange response Pin
apiegoku11-Sep-18 2:26
apiegoku11-Sep-18 2:26 
GeneralMy vote of 5 Pin
Member 410934429-Aug-18 0:44
Member 410934429-Aug-18 0:44 
QuestionGood code n Explained. Pin
Rahul Kadam Pune1-Aug-18 21:33
Rahul Kadam Pune1-Aug-18 21:33 
BugRegarding issue Pin
Member 1330056714-Jun-18 6:48
Member 1330056714-Jun-18 6:48 
GeneralRe: Regarding issue Pin
Member 1291771519-Jun-18 3:16
Member 1291771519-Jun-18 3:16 
PraiseYou rocked Pin
Prafulla Sahu6-Apr-18 0:42
professionalPrafulla Sahu6-Apr-18 0:42 
QuestionLogin form issue Pin
Member 1125509124-Mar-18 2:18
Member 1125509124-Mar-18 2:18 
QuestionVery nice article. Pin
Member 136146389-Jan-18 7:46
Member 136146389-Jan-18 7:46 
AnswerRe: Very nice article. Pin
Saineshwar Bageri9-Jan-18 16:50
Saineshwar Bageri9-Jan-18 16:50 
QuestionSingle sign on Pin
Meh :D B@y@t17-Nov-17 20:45
professionalMeh :D B@y@t17-Nov-17 20:45 
QuestionLooking for Role based example like token example you have given. Pin
Member 1045878716-Nov-17 6:04
Member 1045878716-Nov-17 6:04 
This is such a very nice example i ever found for custom token based Authentication. Please provide a Role based Authorization example without using any framework like OWIN or IdentityFramwork.
Looking for your response.
Neeraj

QuestionThe relevance of clientkeys table Pin
Member 1275241214-Nov-17 14:00
Member 1275241214-Nov-17 14:00 
QuestionHow to get a new token when the current token is expired or about to expire Pin
Mohammed Abdul Mateen9-Nov-17 19:50
Mohammed Abdul Mateen9-Nov-17 19:50 
AnswerRe: How to get a new token when the current token is expired or about to expire Pin
dan ross2-Aug-18 15:57
dan ross2-Aug-18 15:57 
QuestionThanks Pin
huu loi huynh8-Nov-17 13:37
huu loi huynh8-Nov-17 13:37 
AnswerRe: Thanks Pin
Saineshwar Bageri9-Nov-17 5:26
Saineshwar Bageri9-Nov-17 5:26 
QuestionUnable to download code Pin
mvs narayana26-Oct-17 5:21
mvs narayana26-Oct-17 5:21 
AnswerRe: Unable to download code Pin
Saineshwar Bageri26-Oct-17 7:49
Saineshwar Bageri26-Oct-17 7:49 
GeneralRe: Unable to download code Pin
mvs narayana26-Oct-17 21:20
mvs narayana26-Oct-17 21:20 
GeneralRe: Unable to download code Pin
Saineshwar Bageri26-Oct-17 21:41
Saineshwar Bageri26-Oct-17 21:41 
PraiseGreat Pin
MaartenKumpen24-Oct-17 20:58
MaartenKumpen24-Oct-17 20:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.