Barış Kısır

Senior Software Developer

Navigation
 » Home
 » RSS

JWT Authentication in .NET Core Web API with MySQL

14 Aug 2019 » csharp, security, sql

jwt-logo

What is JWT?

A JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret or a public/private key pair.

A JWT is simply a string but it contains three distinct parts separated with dots

jwt-sample


Header - Algorithm and Token type

Header part contains algorithm and token type informations as json object and it is base64 encoded. Both server and client can read value by using base64 decode.

jwt-header

base64("{"alg":"HS256","typ":"JWT"}") -> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"


Payload

Payload can contain any data you want to include into JWT. We can use both registered and non registered claims.

Registered claim names

Both server and client can read value by using base64 decode. We shouldn’t put any sensitive information into JWT.

jwt-payload

base64("{"sub":"1234567890","name":"John Doe","iat":1516239022}") -> "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"


Signature

Signature is an encrypted string for validating header and payload parts by using secret key. Anyone who has secret key can generate jwt token and validate so we should store the key at back-end. We should use different secret keys for each environment. I generally use guid for that.

jwt-signature


Generating JWT

[AllowAnonymous]
[HttpPost]
[Route("login")]
public ActionResult<ResponseLogin> Login(RequestLogin requestLogin)
{
    var responseLogin = new ResponseLogin();
    using (var db = new NetCoreAuthJwtMySqlContext())
    {
        var existingUser = db.User.SingleOrDefault(x => x.Email == requestLogin.Email);
        if (existingUser != null)
        {
            var isPasswordVerified = CryptoUtil.VerifyPassword(requestLogin.Password, existingUser.Salt, existingUser.Password);
            if (isPasswordVerified)
            {
                var claimList = new List<Claim>();
                claimList.Add(new Claim(ClaimTypes.Name, existingUser.Email));
                claimList.Add(new Claim(ClaimTypes.Role, existingUser.Role));
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["SecretKey"]));
                var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                var expireDate = DateTime.UtcNow.AddDays(1);
                var timeStamp = DateUtil.ConvertToTimeStamp(expireDate);
                var token = new JwtSecurityToken(
                    claims: claimList,
                    notBefore: DateTime.UtcNow,
                    expires: expireDate,
                    signingCredentials: creds);
                responseLogin.Success = true;
                responseLogin.Token = new JwtSecurityTokenHandler().WriteToken(token);
                responseLogin.ExpireDate = timeStamp;
            }
            else
            {
                responseLogin.MessageList.Add("Password is wrong");
            }
        }
        else
        {
            responseLogin.MessageList.Add("Email is wrong");
        }
    }
    return responseLogin;
}


Get user claims at back-end

We can use claims to get user information and restrict users according to roles if we add role claim into JWT.

Code block at below restricts users without admin role.

[Authorize(Roles = "ADMIN")]
[HttpGet]
[Route("AuthorizeAdmin")]
public ActionResult<string> AuthorizeAdmin()
{
    var userEmail = User.Identity.Name;
    var userRole = User.Identity.Role;
    return string.Format("Hello {0}", userEmail);
}


Configure JWT authentication

In Startup.cs, we need to configure token settings.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    public IConfiguration Configuration { get; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecretKey"]))
                };
                options.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                        {
                            context.Response.Headers.Add("Token-Expired", "true");
                        }
                        return Task.CompletedTask;
                    }
                };
            });
        services.AddCors(options => options.AddPolicy("ApiCorsPolicy", builder =>
        {
            builder.WithOrigins("https://www.bariskisir.com").AllowAnyMethod().AllowAnyHeader().AllowCredentials();
        }));
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseAuthentication();
        app.UseCors("ApiCorsPolicy");
        app.UseMvc();
    }
}


Hash and Salt

A hash is not ‘encryption’ – it cannot be decrypted back to the original text (it is a ‘one-way’ cryptographic function, and is a fixed size for any size of source text). This makes it suitable when it is appropriate to compare ‘hashed’ versions of texts, as opposed to decrypting the text to obtain the original version.

Hash cannot be decrypted back to the original text but attackers may find the original text by hashing list of common passwords and compare with hashed value.

Lets say user’s password is “test123” and stored as sha512 hashed which is

"daef4953b9783365cad6615223720506cc46c5167cd16ab500fa597aa08ff964eb24fb19687f34d7665f778fcb6c5358fc0a5b81e1662cf90f73a2671c53f991"

There are wordlists on web that store common passwords with hash values.

Sha512-Decrypter

“test123” is a common password and websites already have hashed value of the password. If we search the hashed value, we will get the original text. Thats why we need to use salt.

Salt is a random string that we append to password before we hash it. Salt could be any string, I generally use guid for each user.

When user registers, we will hash “test123_b4e657f4-0c52-47a5-a177-a0fa1fa30c06” and result will be

"BED1E86E586994BE4524DAAE037F6E0AC464044A6EBFB2F3DDD127DDF4DEC5C7DBCF4B7D8A2C1BBF036F49B07ED08CB79A275728593B4FFF8675026916D31B4A"

We will save salt and hashed value to user table. We can hash password+salt multiple times for more protection.

jwt-mysql-user

When user logins, we will do the same thing and compare the hashed values. If match, we will generate token.

public static class CryptoUtil
{
    public static string GenerateSalt()
    {
        var salt = Guid.NewGuid().ToString();
        return salt;
    }
    private static string Hash(string text)
    {
        string result = null;
        if (!string.IsNullOrWhiteSpace(text))
        {
            var encoding = new UTF8Encoding();
            var sha512Provider = new SHA512CryptoServiceProvider();
            var data = sha512Provider.ComputeHash(encoding.GetBytes(text));
            result = ByteToHex(data);
        }
        return result;
    }
    public static string HashMultiple(string password, string salt)
    {
        var hashedString = password + salt;
        for (int i = 0; i < 7; i++)
        {
            hashedString = Hash(hashedString);
        }
        return hashedString;
    }
    private static string ByteToHex(byte[] input)
    {
        var stringBuilder = new StringBuilder(string.Empty);
        for (var i = 0; i < input.Length; i++)
        {
            stringBuilder.Append(input[i].ToString("X2"));
        }
        return stringBuilder.ToString();
    }
    public static bool VerifyPassword(string password, string salt, string hashedPassword)
    {
        var result = false;
        var hashed = HashMultiple(password, salt);
        if (hashedPassword == hashed)
        {
            result = true;
        }
        return result;
    }
}


MySql Configuration

User table

CREATE DATABASE IF NOT EXISTS `netcoreauthjwtmysql` /*!40100 DEFAULT CHARACTER SET utf8 */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `netcoreauthjwtmysql`;

-- Dumping structure for table netcoreauthjwtmysql.user
CREATE TABLE IF NOT EXISTS `user` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `EMAIL` varchar(50) NOT NULL DEFAULT '0',
  `SALT` varchar(36) NOT NULL DEFAULT '0',
  `PASSWORD` varchar(128) NOT NULL DEFAULT '0',
  `ROLE` varchar(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  KEY `IX_EMAIL` (`EMAIL`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


NuGet Package for MySQL

Pomelo.EntityFrameworkCore.MySql

In .NET Core, we dont have gui to generate DbContext, we can run the code below in source folder to generate or update DbContext.

dotnet ef dbcontext scaffold "server=localhost;port=3306;user=root;password=root;database=netcoreauthjwtmysql" Pomelo.EntityFrameworkCore.MySql -o Models/Db -f


Postman requests

jwt-postman-register-1
jwt-postman-register-2

jwt-postman-login-1
jwt-postman-login-2

jwt-postman-auth


You can download source code, db script and postman requests from here –> Download