What is API Gateway in Asp.net Core Micro Service?


In ASP.NET Core microservices architecture, an API Gateway acts as a central entry point for client applications to access various microservices. It provides a single unified interface for clients to interact with multiple services.

The API Gateway handles requests from clients, performs necessary routing and protocol translation, and dispatches those requests to the appropriate microservices.

Here are some key features and benefits of using API Gateways in ASP.NET Core microservices:

Request aggregation: An API Gateway can aggregate multiple requests from clients into a single request to reduce chattiness between clients and microservices. This helps improve performance and reduce network overhead.

Routing and load balancing: The API Gateway can handle routing requests to different microservices based on the requested resource or operation. It can also perform load balancing to distribute the incoming traffic across multiple instances of microservices.

Protocol translation: Clients may use different protocols or data formats than what the microservices support. The API Gateway can handle the translation of protocols (e.g., HTTP to gRPC) and data formats (e.g., JSON to Protobuf), ensuring seamless communication between clients and microservices.

Authentication and authorization: The API Gateway can handle authentication and authorization for incoming requests. It can enforce security policies, authenticate client requests, and authorize access to specific microservices based on the client’s identity and permissions.

Caching and rate limiting: API Gateways can implement caching mechanisms to improve performance by serving cached responses for repeated requests. They can also enforce rate limits to prevent abuse and protect microservices from excessive traffic.

Logging and monitoring: API Gateways can log incoming requests and responses, providing valuable insights into the system’s behavior. They can also integrate with monitoring tools to collect metrics and perform health checks on microservices.

There are so many api gateways which can we use in asp.net core microservice. like Ocelot, Azure API Management, AWS API Gateway, Kong etc

In this demo we will see basic demo of Ocelot.

Step 1: Create the 3 project layers using Asp.net Core for StudentService, TeacherService and Gateways layer

Step 2: Go to Ocelot gateway layer and installed the ocelot NuGet package.

Step 3: Create Ocelot.json file and configure the routing using Upstream and Downstream like this

{
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5003"
  },
  "Routes": [
    {
      "UpstreamPathTemplate": "/gateway/students",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/student",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/students/{Id}",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/student/{Id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/teachers",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/teacher",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/teacher/{Id}",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/teachers/{Id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ]
    }
  ]
}

This is main setting configuration file, which will read the input request and route to specific project api Countroller URL.

If you will give

http://localhost:5003/gateway/students

Then It will Call http://localhost:5001/api/student

Again If you type http://localhost:5003/gateway/teachers

It will route to http://localhost:5002/api/teacher

In the above setting we are seeing with use of single api endpoint http://localhost:5003/

we are calling multiple microservice api endpoints.

Step 4: Go to the program file of OcelotGateway project and configure the middleware pipeline like this

using Ocelot.DependencyInjection;
using Ocelot.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();


builder.Configuration.AddJsonFile("ocelot.json",optional:false,reloadOnChange:true);
builder.Services.AddOcelot(builder.Configuration);
var app = builder.Build();


app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

await app.UseOcelot();

app.Run();

Step 5: Give some dummy data in Student and Teacher We API Controller

Step 6: Make all 3-project layer as Starup project layer.

Step 7: Run the application and test it.

Source code to Download: https://github.com/Chandradev819/Ocelot_API_Gateway_Demo

How to create Token Based Authentication with Refresh Token in Asp.net Core 7.0?


Token-based authentication in ASP.NET Web API is a security mechanism that involves the use of tokens to authenticate and authorize users when accessing resources or performing actions within a web API. It is commonly used in modern web applications and APIs to provide secure and stateless authentication.

Here’s how token-based authentication typically works in ASP.NET Web API:

  1. User Authentication: When a user logs in or provides valid credentials, the server generates a token for that user. The token contains information such as the user’s identity and any relevant user roles or permissions.
  2. Token Issuance: The server issues the token to the client (usually a web browser or a mobile app) in the response after successful authentication. The token is typically sent in the HTTP response header, such as the “Authorization” header.
  3. Token Storage: The client (web browser or app) stores the received token securely. Commonly, the token is stored in a client-side storage mechanism, such as local storage or a cookie.
  4. Token Usage: Subsequent requests from the client to the server include the token in the request header, typically in the “Authorization” header. This informs the server that the client has been authenticated and authorized to access protected resources.
  5. Token Validation: The server receives the token in the request header and verifies its authenticity and integrity. It checks if the token is valid, has not expired, and is issued by a trusted authority. If the token is valid, the server extracts the user identity and proceeds with the requested operation.
  6. Authorization:  After successful token validation, the server performs authorization checks to determine if the user associated with the token has sufficient privileges to access the requested resource or perform the requested action.
  7. Response: The server processes the request, generates the appropriate response, and sends it back to the client.

Advantages

Token-based authentication offers several advantages, including statelessness, scalability, and the ability to support distributed systems. It enables secure API access without the need for session management on the server-side, which simplifies the overall architecture.

Images: Bearer Token workflow diagram

What is Bearer Token ?

Bearer token is a type of access token used in token-based authentication schemes, including OAuth 2.0 and JSON Web Tokens (JWT). It is commonly used in web APIs and provides a means of authorizing and authenticating requests made by clients.

What is OAuth 2.0 ?

OAuth 2.0 is an open standard framework that allows users to grant limited access to their resources on one website (known as the “resource server”) to another website or application (known as the “client”) without sharing their credentials, such as passwords. It is commonly used for authentication and authorization purposes in web applications, mobile apps, and APIs.

OAuth 2.0 defines a set of roles, grant types, and protocols to facilitate the secure delegation of access rights

What is OpenId Connect?

OpenID Connect (OIDC) is an authentication layer built on top of the OAuth 2.0 framework. It provides a standardized and secure way for clients (such as web applications or mobile apps) to authenticate users using an identity provider (IdP) and obtain identity information about the authenticated users. OIDC adds identity-related features on top of OAuth 2.0, making it a complete identity protocol.

Now let’s create Bearer Token based Authentication demo

Step 1: Create the Web API Core application like this

Step 2: Install the given package on your project as given below

  • Microsoft.EntityFrameworkCore.SqlServer 
  • Microsoft.EntityFrameworkCore.Tools 
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore 
  • Microsoft.AspNetCore.Authentication.JwtBearer 

Step 3:   Change the connection string and JWT details in appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ConnStr": "Data Source=.;Initial Catalog=JWTRefreshTokenDB;Integrated Security=SSPI;Encrypt=False"
  },
  "JWT": {
    "ValidAudience": "https://localhost:7262",
    "ValidIssuer": "https://localhost:7262/",
    "Secret": "PleaseHoldthisValue_on_AWS_OR_Azure",
    "TokenValidityInMinutes": 1,
    "RefreshTokenValidityInDays": 7
  }
}

Step 4: Create the Auth Folder and inside Auth Folder create the ApplicationDbContext.cs class

We are using Identity Framework for creating all required tables for us for managing security on our application, we need to configure ApplicationDbContext like this.

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace BearerTokenDemo.Auth
{
    public class ApplicationDbContext: IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }
}

Step 5: Create Application User class like this

using Microsoft.AspNetCore.Identity;

namespace BearerTokenDemo.Auth
{
    public class ApplicationUser: IdentityUser
    {
        public string? RefreshToken { get; set; }
        public DateTime RefreshTokenExpiryTime { get; set; }
    }
}




Step 6: Create User Role Class like this

namespace BearerTokenDemo.Auth
{
    public static class UserRoles
    {
        public const string Admin = "Admin";
        public const string User = "User";
    }
}

Step 7: Go to the Model folder and Create Login Model like this

using System.ComponentModel.DataAnnotations;

namespace BearerTokenDemo.Model
{
    public class LoginModel
    {
        [Required(ErrorMessage = "User Name is required")]
        public string? Username { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string? Password { get; set; }
    }
}




RegisterModel

using System.ComponentModel.DataAnnotations;

namespace BearerTokenDemo.Model
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "User Name is required")]
        public string? Username { get; set; }

        [EmailAddress]
        [Required(ErrorMessage = "Email is required")]
        public string? Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string? Password { get; set; }
    }
}






Response Model:

namespace BearerTokenDemo.Model
{
    public class Response
    {
        public string? Status { get; set; }
        public string? Message { get; set; }
    }
}





Token Model

namespace BearerTokenDemo.Model
{
    public class TokenModel
    {
        public string? AccessToken { get; set; }
        public string? RefreshToken { get; set; }
    }
}

Step 7: Go to the Controller Folder and Create AuthenticateController like this

using BearerTokenDemo.Auth;
using BearerTokenDemo.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;

namespace BearerTokenDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticateController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IConfiguration _configuration;

        public AuthenticateController(
            UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IConfiguration configuration)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            var user = await _userManager.FindByNameAsync(model.Username);
            if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
            {
                var userRoles = await _userManager.GetRolesAsync(user);

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, userRole));
                }

                var token = CreateToken(authClaims);
                var refreshToken = GenerateRefreshToken();

                _ = int.TryParse(_configuration["JWT:RefreshTokenValidityInDays"], out int refreshTokenValidityInDays);

                user.RefreshToken = refreshToken;
                user.RefreshTokenExpiryTime = DateTime.Now.AddDays(refreshTokenValidityInDays);

                await _userManager.UpdateAsync(user);

                return Ok(new
                {
                    Token = new JwtSecurityTokenHandler().WriteToken(token),
                    RefreshToken = refreshToken,
                    Expiration = token.ValidTo
                });
            }
            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });

            ApplicationUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });

            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }

        [HttpPost]
        [Route("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });

            ApplicationUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });

            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
            if (!await _roleManager.RoleExistsAsync(UserRoles.User))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));

            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.Admin);
            }
            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.User);
            }
            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }

        [HttpPost]
        [Route("refresh-token")]
        public async Task<IActionResult> RefreshToken(TokenModel tokenModel)
        {
            if (tokenModel is null)
            {
                return BadRequest("Invalid client request");
            }

            string? accessToken = tokenModel.AccessToken;
            string? refreshToken = tokenModel.RefreshToken;

            var principal = GetPrincipalFromExpiredToken(accessToken);
            if (principal == null)
            {
                return BadRequest("Invalid access token or refresh token");
            }

            string username = principal.Identity.Name;
            var user = await _userManager.FindByNameAsync(username);

            if (user == null || user.RefreshToken != refreshToken || user.RefreshTokenExpiryTime <= DateTime.Now)
            {
                return BadRequest("Invalid access token or refresh token");
            }

            var newAccessToken = CreateToken(principal.Claims.ToList());
            var newRefreshToken = GenerateRefreshToken();

            user.RefreshToken = newRefreshToken;
            await _userManager.UpdateAsync(user);

            return new ObjectResult(new
            {
                accessToken = new JwtSecurityTokenHandler().WriteToken(newAccessToken),
                refreshToken = newRefreshToken
            });
        }

        [Authorize]
        [HttpPost]
        [Route("revoke/{username}")]
        public async Task<IActionResult> Revoke(string username)
        {
            var user = await _userManager.FindByNameAsync(username);
            if (user == null) return BadRequest("Invalid user name");

            user.RefreshToken = null;
            await _userManager.UpdateAsync(user);

            return NoContent();
        }

        [Authorize]
        [HttpPost]
        [Route("revoke-all")]
        public async Task<IActionResult> RevokeAll()
        {
            var users = _userManager.Users.ToList();
            foreach (var user in users)
            {
                user.RefreshToken = null;
                await _userManager.UpdateAsync(user);
            }

            return NoContent();
        }

        private JwtSecurityToken CreateToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
            _ = int.TryParse(_configuration["JWT:TokenValidityInMinutes"], out int tokenValidityInMinutes);

            var token = new JwtSecurityToken(
                issuer: _configuration["JWT:ValidIssuer"],
                audience: _configuration["JWT:ValidAudience"],
                expires: DateTime.Now.AddMinutes(tokenValidityInMinutes),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                );

            return token;
        }

        private static string GenerateRefreshToken()
        {
            var randomNumber = new byte[64];
            using var rng = RandomNumberGenerator.Create();
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }

        private ClaimsPrincipal? GetPrincipalFromExpiredToken(string? token)
        {
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"])),
                ValidateLifetime = false
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
            if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
                throw new SecurityTokenException("Invalid token");

            return principal;

        }
    }
}






Next Keep Authorize attributes like this.

Step 8: Go to the Program.cs file and configure the middleware like this

using BearerTokenDemo.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;

// For Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));

// For Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Adding Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.Zero,

        ValidAudience = configuration["JWT:ValidAudience"],
        ValidIssuer = configuration["JWT:ValidIssuer"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
    };
});

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

//Adding Bearer token option on swagger
builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "BearerToken Demo API", Version = "v1" });
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Step 9: You can run this two command to update schema in database using package manager console.

add-migration Initial

update-database

Step 10: Run the application and verify it.

Now test the Login functionalities like this

You can download the source code from GitHub and play with it.

https://github.com/Chandradev819/WebAPI_BearerTokenDemo

How to downgrade the .Net Core SDK Version?


So many times, while upgrading the .Net Core SDK to higher version, our working project will start to break due to some bugs on the latest sdk.

Step 1: Go to the project folder location and run the command

Step 2:  Type this command to know all installed .Net Core SDK

dotnet –info

Step 3: Run this command to change the sdk like this

dotnet new globaljson –sdk-version 7.0.203 –force

Now in your application you will be able to see the global.json file like this.