ASP.NET Core Web API Course

Build RESTful services using ASP.NET Core 3.1

تعلم برمجة الـ RESTful services أو الـ Web APIs بإستخدام ASP.NET Core بطريقة مفصلة وتدريجية

الصفحة الرئيسية
خدمة RSS

للتواصل

12. Securing the API - Bearer (Token) Authentication - JWT

2020-06-21






في النوع السابق من المصادقة basic authentication كان يتوجب علينا إرسال إسم المستخدم وكلمة المرور عند تنفيذ أي عملية. وفي هذا الدرس سنتعلم نوع آخر من المصادقة مبني على إستخدام التوكن. وهو عبارة عن نص يحتوي على معلومات خاصة بالمستخدم الذي يحاول تنفيذ العملية من دون إحتواءه على كلمة المرور الخاصة بالمستخدم. هذا التوكن يتم إنشاؤه بواسطة private key موجود على الـ server ولذلك وطالما أخذنا بالإعتبار الإحتياطات الأمنية اللازمة فإنه يصعب تزوير هذا التوكن.

آلية إنشاء التوكن تكون بالشكل التالي:

1) يستدعي المستخدم العملية auth / login ممرراً إسم المستخدم وكلمة المرور الخاصة به

2) يتم التحقق من صحة معلومات المستخدم وبعد ذلك سنعيد اليه توكن يمكن إستخدامه في تنفيذ باقي العمليات

3) في نفس الوقت سيعطى المستخدم توكن للتحديث refresh token بحيث يمرره الى العملية auth / refresh لتحديث الـ jwt token من دون الحاجة الى إرسال إسم المستخدم وكلمة المرور مرة أخرى

سيتم تمرير التوكن في الـ Authorization header بالشكل التالي:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1laWQiOiIxIiwidW5pcXVlX25hOcGaL6YuL7nk

وهنا بشكل مختصر شرح للتوكن وللعملية بشكل عام:

العمل في branch مستقل

ننشئ branch جديد ونتحول اليه:

git branch jwt
git checkout jwt

إضافة المكتبات البرمجية

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt

إنشاء الـ JWT Token

إدارة المستفيدين من الخدمة

ننشئ ملف جديد بإسم User.cs يمثل المستفيد من الخدمة التي نقدمها داخل المجلد Entities:

using System;
using System.ComponentModel.DataAnnotations;

namespace aspnetcorewebapiproject.Entities
{
    public class User : IHasCreatedAndLastModifiedDates
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [StringLength(100)]
        public string Username { get; set; }

        [Required]
        [StringLength(100)]
        public string Password { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }

        public DateTime LastModified { get; set; }
        public DateTime CreatedDate { get; set; }
    }
}

إضافته الى الـ db context

نقوم بإضافة الـ entity الجديد User على MainDbContext كما يلي:

public class MainDbContext : DbContext
{
	public DbSet<Entities.Employee> Employees { get; set; }

	public DbSet<Entities.User> Users { get; set; }

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.Entity<Entities.User>().HasData(
			new Entities.User { Id = 1, Username = "ahmad", Password = "abc@123", Email = "ahmad@email.com" },
			new Entities.User { Id = 2, Username = "ali", Password = "def@456", Email = "ali@email.com" },
			new Entities.User { Id = 3, Username = "yousef", Password = "ghi@789", Email = "yousef@email.com" }
		);
	}

	...

في OnModelCreating طلبنا إضافة بعض البيانات في قاعدة البيانات.

نضيف أوامر التعديل على قاعدة البيانات:

dotnet ef migrations add "Adds_Users_and_Sample_Data"

نقوم الآن بتنفيذ الأمر:

dotnet ef database update

نستخدم الآن Azure Data Studio لرؤية التعديلات التي تم تنفيذها:

إضاقة إعدادات في appsettings.json

نقوم بإضافة الـ key المستخدم للتشفير ومدة صلاحية الـ jwt token وهي 5 دقائق:

  ... ,
  "JwtSettings": {
    "SecretKey": "SecretSecurityKey",
    "TokenLifeTime": "00:05:00"
  }    
}

إضافة DTO

في المجلد Models / v1 / Auth نضيف الملف LoginDto.cs والذي يحتوي على معلومات المستخدم المطلوبة عند تسجيل الدخول:

namespace aspnetcorewebapiproject.Models.Auth.v1
{
    public class LoginDto
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

إضافة AuthController

نقوم الآن بإضافة الـ controller المسؤول عن إنشاء التوكن token وذلك في المجلد Controllers / v1 كما يلي:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Cors;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using aspnetcorewebapiproject.Models.Auth.v1;
using aspnetcorewebapiproject.DbContexts;
using System.Linq;

namespace aspnetcorewebapiproject.Controllers.v1
{
    /// <summary>
    /// Authenticates users and generates a jwt token
    /// </summary> 
    [ApiVersion("1.0")]
    [ApiVersion("1.1")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [EnableCors("CorsPolicy")]
    [ApiController] 
    public class AuthController : ControllerBase
    {
        private readonly IConfiguration _config;
        private readonly MainDbContext _dbContext;

        public AuthController(IConfiguration config, MainDbContext dbContext)
        {
            _config = config;
            _dbContext = dbContext;
        }

        [HttpPost("login")]
        public async Task<IActionResult> GenerateToken([FromBody] LoginDto data)
        {
            if( string.IsNullOrWhiteSpace(data.Username) || string.IsNullOrWhiteSpace(data.Password) )
                return BadRequest();

            var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.Username == data.Username && u.Password == data.Password);
          
            if( user != null )
            {
                TimeSpan tokenLifeTime = _config.GetValue<TimeSpan>("JwtSettings:TokenLifeTime");

                // User found. Create ticket for user
                var claims = new List<Claim> {
                    new Claim("id", user.Id.ToString()),
                    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                    new Claim(ClaimTypes.Name, user.Username),
                    new Claim(ClaimTypes.Email, user.Email),

                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),

                    // not before, this token is not valid before a certain date and time
                    // in our case we want it to be valid right away
                    new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString()),

                    // when will it expire
                    new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.UtcNow).Add(tokenLifeTime).ToUnixTimeSeconds().ToString())
                };

                var creds = new SigningCredentials(
                        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.GetValue<string>("JwtSettings:SecretKey"))), 
                        SecurityAlgorithms.HmacSha256);

                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Subject = new ClaimsIdentity(claims),
                    Expires = DateTime.UtcNow.Add(tokenLifeTime),
                    SigningCredentials = creds
                };

                var tokenHandler = new JwtSecurityTokenHandler();
                SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);

                return Ok( new { 
                    token = tokenHandler.WriteToken(token),
                });                
            }

            return BadRequest();
        }
    }
}

التعديل على Startup.cs

لتفعيل ما قمنا به نقوم بالتالي:

التعديل على الدالة ()Configure

علينا إضافة ()app.UseAuthentication قبل ()app.UseAuthorization:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger, IApiVersionDescriptionProvider provider)
{
	...

	app.UseAuthentication();

	app.UseAuthorization();

	...     
}

التعديل على ()ConfigureServices

نقوم بإضافة ما يلي في آخر الدالة:

public void ConfigureServices(IServiceCollection services)
{
	...
	
	services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
	.AddJwtBearer(options => 
	{
		options.TokenValidationParameters = new TokenValidationParameters()
		{
			IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("JwtSettings:SecretKey"))),
			ValidateIssuer = false,
			ValidateAudience = false,
			ValidateLifetime = true,
			LifetimeValidator = LifetimeValidator
		};
	});
}

ثم نضيف الدالة الجديدة التالية:

private bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters @params)
{
	if (expires != null)
		return expires > DateTime.UtcNow;
	
	return false;
}

والآن نخبر Swagger بأن الـ API تعتمد على JWT:

public void ConfigureServices(IServiceCollection services)
{
	...
	            
	services.AddSwaggerGen(o =>
	{
		// add a custom operation filter which sets default values
		o.OperationFilter<SwaggerDefaultValues>();

		o.ResolveConflictingActions( apiDescriptions => apiDescriptions.First() );

		// Set the comments path for the Swagger JSON and UI.
		var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
		var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
		o.IncludeXmlComments(xmlPath);  
		
		// Swagger needs to know about JWT
		o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
		{
			Description = "JWT Authorization header using the Bearer scheme.",
			Name = "Authorization",
			In = ParameterLocation.Header,
			Type = SecuritySchemeType.ApiKey,
			Scheme = "Bearer",
			BearerFormat = "JWT"
		});

		o.AddSecurityRequirement(new OpenApiSecurityRequirement(){
			{
				new OpenApiSecurityScheme
				{
					Reference = new OpenApiReference
					{
						Type = ReferenceType.SecurityScheme,
						Id = "Bearer"
					}
				},
				new List<string>()
			}
		});

	});            
}

التعديل على EmployeesController

لجعل EmployeesController تتطلب التحقق من المستخدم قبل إمكانية الإستفادة منها فإنه يتوجب علينا إضافة الـ [Authorize] attribute:

[Authorize]
...
public class EmployeesController : ControllerBase
{
	...

التجربة في Postman

سنقوم بطلب الخدمة هذه المرة بدون تمرير الـ Authorization header:

نلاحظ أن الخدمة رفضت هذه العملية وأعادت الـ HTTP Status Code التالي: Unauthorized 401.

نطلب الآن العنوان التالي ونقوم بتمرير إسم المستخدم وكلمة المرور لأحد المستخدمين المسجلين في قاعدة البيانات في الـ request body:

https://localhost:5001/api/v1.0/auth/login

والآن إنسخ التوكن ولو فتحت الـ console في المتصفح وكتبت التالي لأتضحت لك محتويات التوكن:

JSON.stringify(JSON.parse(atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJhaG1hZCIsIm5iZiI6MTU5MjA4MDcxMSwiZXhwIjoxNTkyMTY3MTExLCJpYXQiOjE1OTIwODA3MTF9.luIluXkuSnd8YrKzsnyiboOopi37Eh5iixvCiPokmgM".split('.')[1])), null, 2);

نعود الآن الى Postman لتنفيذ GetEmployee حيث نختار التبويب Authorization، ثم في القائمة المنسدلة TYPE نختار Bearer Token وفي الصندوق النصي Token في اليمين نضع التوكن الذي نسخناه بدون علامتي التنصيص ثم نضغط على Send ,سترى أنه بإمكاننا الآن الوصول الى الخدمة:

ولترى الـ header الذي سيتم إرساله، إختر التبويب Headers ثم إضغط على hidden:

لمعرفة المستخدم الذي قام بالإستدعاء

بإمكانك الآن إستخدام User.Identity للحصول على معلومات المستخدم الذي قام بالإستدعاء. وعلى سبيل المثال:

public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> GetEmployee(int id)
{
	_logger.LogInformation($"GetEmployee requested by: {User.Identity.Name}");
	...

لعدم إشتراط التحقق

هناك حالات لا تريد فيها من المستخدم أن يرسل بيانات التحقق كإسم المستخدم وكلمة المرور أو التوكن في حالتنا هذه، كأن يسجل في الخدمة لأول مرة، ففي هذه الحالة يمكنك إضافة الـ attribute التالي: [AllowAnonymous] على العملية التي لا تتطلب التحقق من المستخدم.

تحديث الـ JWT Token

مشكلة التوكن أن صلاحيته تنتهي expires بعد مرور فترة من الزمن تقوم أنت بتحديدها. وما يمكننا فعله في هذه الحالية هو توفير خاصية لتحديث التوكن بدلاً من تسجيل الدخول بإستخدام الإسم وكلمة المرور من جديد.

الفرق بين توكن jwt وتوكن التحديث هو أن الأول فترة صلاحيته أقل بكثير من الثاني حيث من الممكن أن تستمر صلاحية الثاني الى عدة أشهر. بالإضافة الى أن توكن التحديث لا يحتوي على معلومات المستخدم وإنما يعتبر كمعرف للأول لا أكثر.

سوف نستخدم توكن jwt المنتهي صلاحيته وتوكن التحديث refresh token لتوليد توكن jwt جديد.

تحديد مدة صلاحية الـ refresh token

يكون ذلك في ملف appsettings.json حيث سنقوم بإضافة RefreshTokenLifeTimeInMonths وهي تحدد صلاحية الـ refresh token بالأشهر:

  "JwtSettings": {
    "SecretKey": "SecretSecurityKey",
    "TokenLifeTime": "00:05:00",
    "RefreshTokenLifeTimeInMonths": 6
  }

إنشاء entity جديدة

ننشئ entity بإسم UserRefreshToken في مجلد الـ Entities مسؤولة عن حفظ الـ refresh token الخاصة بالمستخدم والـ jwt id التي ترتبط بها:

using System;
using System.ComponentModel.DataAnnotations;

namespace aspnetcorewebapiproject.Entities
{
    public class UserRefreshToken
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string JwtTokenId { get; set; }

        [Required]
        public string RefreshToken { get; set; }

        [Required]
        public DateTime RefreshTokenExpirationDate { get; set; }

        [Required]
        public User User { get; set; }

        [Required]
        public bool IsValid { get; set; } = true;

        public DateTime LastModified { get; set; }

        public DateTime CreatedDate { get; set; }
    }
}

إضافته الى الـ db context

نقوم بإضافة الـ entity الجديد UserRefreshToken على MainDbContext كما يلي:

public class MainDbContext : DbContext
{
	public DbSet<Entities.Employee> Employees { get; set; }

	public DbSet<Entities.User> Users { get; set; }

	public DbSet<Entities.UserRefreshToken> UserRefreshTokens { get; set; }
	
	...

نضيف أوامر التعديل على قاعدة البيانات:

dotnet ef migrations add "Adds_UserRefreshTokens"

نقوم الآن بتنفيذ الأمر:

dotnet ef database update

إضافة DTO

في المجلد Models / v1 / Auth نضيف الملف RefreshTokenDto.cs والذي يحتوي على معلومات المستخدم المطلوبة عند تسجيل الدخول:

namespace aspnetcorewebapiproject.Models.Auth.v1
{
    public class RefreshTokenDto
    {
        public string JwtToken { get; set; }
        public string RefreshToken { get; set; }
    }
}

التعديل على AuthController

هنالك عدة تعديلات تمت على AuthController وذلك لأننا أضفنا دالة خاصة بتحديث التوكن فأصبح لدينا كود مشترك بين دالة التحديث ودالة الإنشاء مما توجب علينا إستخلاصها في دوال مشتركة:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Cors;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using aspnetcorewebapiproject.Models.Auth.v1;
using aspnetcorewebapiproject.DbContexts;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;

namespace aspnetcorewebapiproject.Controllers.v1
{
    /// <summary>
    /// Authenticates users and generates a jwt token
    /// </summary> 
    [AllowAnonymous]
    [ApiVersion("1.0")]
    [ApiVersion("1.1")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [EnableCors("CorsPolicy")]
    [ApiController] 
    public class AuthController : ControllerBase
    {
        //
        // Fields
        //

        private readonly IConfiguration _config;
        private readonly MainDbContext _dbContext;
        private readonly string _jwtSecretKey;
        private readonly TimeSpan _jwtTokenLifeTime;        
        private readonly int _refreshTokenLifeTimeInMonths;
        private readonly ILogger<AuthController> _logger;

        //
        // Constructor
        //
        
        public AuthController(IConfiguration config, MainDbContext dbContext, ILogger<AuthController> logger)
        {
            _config = config;            
            _dbContext = dbContext;
            _logger = logger;
            
            _jwtSecretKey = _config.GetValue<string>("JwtSettings:SecretKey");
            _jwtTokenLifeTime = _config.GetValue<TimeSpan>("JwtSettings:TokenLifeTime");            
            _refreshTokenLifeTimeInMonths = _config.GetValue<int>("JwtSettings:RefreshTokenLifeTimeInMonths");           
        }

        //
        // Action Methods
        //        

        [HttpPost("login")]        
        public async Task<IActionResult> GenerateToken([FromBody] LoginDto data)
        {
            if( string.IsNullOrWhiteSpace(data.Username) || string.IsNullOrWhiteSpace(data.Password) )
                return BadRequest();

            var user = await _dbContext.Users.SingleOrDefaultAsync(
                                    u => u.Username == data.Username && u.Password == data.Password);
          
            if( user != null )
            {
                string jwtToken = _GenerateJwtToken(user, out string jwtId);
                string refreshToken = _GenerateRefreshToken();

                var userRefreshToken = new Entities.UserRefreshToken {
                    JwtTokenId = jwtId,
                    RefreshToken = refreshToken,
                    RefreshTokenExpirationDate = DateTime.UtcNow.AddMonths(_refreshTokenLifeTimeInMonths),
                    User = user
                };

                // Save to DB
                _dbContext.UserRefreshTokens.Add(userRefreshToken);
                await _dbContext.SaveChangesAsync();

                // Return to user
                return Ok( new { 
                    jwtToken = jwtToken,
                    refreshToken = refreshToken
                });                
            }

            return BadRequest();
        }

        [HttpPost("refresh")]
        public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenDto data)
        {
            // 1. Get user info
            ClaimsPrincipal principal = _GetPrincipalFromToken(data.JwtToken);
            string userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            // 2. Check if jwt token actually expired
            var expirationDateUnixTimestamp = 
                long.Parse( principal.Claims.Single(c => c.Type == JwtRegisteredClaimNames.Exp).Value );

            var expirationDateUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
                        .AddSeconds(expirationDateUnixTimestamp);

            if( expirationDateUtc > DateTime.UtcNow )
                return BadRequest("Token has not expired yet");

            // 3. Get jwt token Id
            var jti = principal.Claims.Single(c => c.Type == JwtRegisteredClaimNames.Jti).Value;

            // 4. Check if refresh token actually exists in db
            var userRefreshTokenDb = await _dbContext.UserRefreshTokens.Include("User").SingleOrDefaultAsync(
                        ut => ut.JwtTokenId == jti && ut.RefreshToken == data.RefreshToken
                                && ut.User.Id == Convert.ToInt32(userId));

            if( userRefreshTokenDb == null )
                return BadRequest("Refresh token does not exist");

            // 5. Check if refresh token is valid
            if( !userRefreshTokenDb.IsValid )
                return BadRequest("Refresh token is invalid");                

            // 6. Check if refresh token has expired
            if( DateTime.UtcNow > userRefreshTokenDb.RefreshTokenExpirationDate )
                return BadRequest("Refresh token expired. Please login again");

            string jwtToken = _GenerateJwtToken(userRefreshTokenDb.User, out string jwtId);
            string refreshToken = _GenerateRefreshToken();

            var userRefreshToken = new Entities.UserRefreshToken {
                JwtTokenId = jti,
                RefreshToken = refreshToken,
                RefreshTokenExpirationDate = DateTime.UtcNow.AddMonths(_refreshTokenLifeTimeInMonths),
                User = userRefreshTokenDb.User
            };

            // Save to DB
            _dbContext.UserRefreshTokens.Add(userRefreshToken);
            await _dbContext.SaveChangesAsync();

            // Return to user
            return Ok( new { 
                jwtToken = jwtToken,
                refreshToken = refreshToken
            });              
        }

        //
        // Helper Methods
        //

        private string _GenerateJwtToken(Entities.User user, out string jwtId)
        {    
            string userId = user.Id.ToString();
            string username = user.Username;
            string userEmail = user.Email;
            jwtId = Guid.NewGuid().ToString();
            string notBefore = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString();
            string expiration = new DateTimeOffset(DateTime.UtcNow).Add(_jwtTokenLifeTime).ToUnixTimeSeconds().ToString();

            // User found. Create ticket for user
            var claims = new List<Claim> {
                new Claim("id", userId),
                new Claim(ClaimTypes.NameIdentifier, userId),
                new Claim(ClaimTypes.Name, username),
                new Claim(JwtRegisteredClaimNames.Email, userEmail),
                new Claim(JwtRegisteredClaimNames.Jti, jwtId),
                new Claim(JwtRegisteredClaimNames.Sub, username),

                // not before, this token is not valid before a certain date and time
                // in our case we want it to be valid right away
                new Claim(JwtRegisteredClaimNames.Nbf, notBefore),

                // when will it expire
                new Claim(JwtRegisteredClaimNames.Exp, expiration)
            };

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.UtcNow.Add(_jwtTokenLifeTime),
                SigningCredentials = new SigningCredentials(
                            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey)), 
                            SecurityAlgorithms.HmacSha256) 
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);

            return tokenHandler.WriteToken(token);
        }
        
        private string _GenerateRefreshToken()
        {            
            using(var randNumGen = RandomNumberGenerator.Create())
            {
                var randNum = new byte[32];

                randNumGen.GetBytes(randNum);

                return Convert.ToBase64String(randNum);
            }
        }

        // Gets user from access jwt token
        private ClaimsPrincipal _GetPrincipalFromToken(string jwtToken)
        {
            // The same as the one in Startup.ConfigureServices().AddJwtBearer() except ValidateLifetime = false
            var tokenValidationParameters = new TokenValidationParameters()
            {
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey)),
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = false
            };

            var tokenHandler = new JwtSecurityTokenHandler();

            try
            {
                var principal = tokenHandler.ValidateToken(jwtToken, tokenValidationParameters, out SecurityToken validatedSecurityToken);

                // Is JWT with valid security algorithm
                if( (validatedSecurityToken is JwtSecurityToken jwtSecurityToken) &&
                    (jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)))
                    return principal;

                throw new SecurityTokenException("Invalid token");
            }
            catch
            {
                throw new SecurityTokenException("Invalid token");
            }
        }
    }
}

فيما يلي شرح لأهم النقاط المذكورة في الكود:

التجربة في Postman

1) إطلب auth / login :

2) إنسخ الـ jwt token وإستخدمه في إستدعاء GetEmployee:

3) نفذ الإستدعاء السابق بعد مرور أكثر من 5 دقائق:

4) إنشئ request جديد في Postman يستدعي العملية auth / refresh ويمرر اليها الـ jwt & refresh tokens التي حصلنا عليها في الخطوة رقم 1:

5) إستخدم الـ jwt token الجديد وسترى بأن الإستدعاء سيتم بشكل صحيح

حفظ التعديلات

نضيف الآن هذه التعديلات الى git:

git add .
git commit -m "adds bearer token authentication support"

إعتماد خاصية الـ JWT tokens

سنعتمد الآن ما قمنا به في الـ branch الذي أنشأناه وسنقوم بإضافته على الـ master.

نعود الآن الى الـ master branch:

git checkout master

ثم ندمج التعديلات التي تمت في الـ jwt branch:

git merge jwt