Authentication is the process that helps identify who is the users. On the other hand, authorization is the process of determining what a user can do. For authorization to work, the user will be authenticated first. We need the user’s identity to identify the role of a user and act accordingly.
Authentication middleware is responsible for authentication in ASP.Net Core applications. The authentication middleware uses the registered authentication handlers to authenticate a user. The registered handlers and their associated configurations are called schemes
.
In ASP.Net Core, the authentication middleware is added in the Startup
class, inside the Configure
method. It is done by calling UseAuthentication
method on the IApplicationBuilder
instance passed to the method.
Authentication schemes are registered in the Startup
class inside of the ConfigureServices
method. It is done by calling AddAuthentication
method on the IServiceCollection
instance passed to the method. We can register multiple authentication schemes, whereas only one of them will be a default scheme.
Authentication Scheme and Handler
As I mentioned above, the scheme is nothing but the index of a handler and its configuration. A scheme is a mechanism for referring an authentication, the challenge (how to challenge a request if authentication fails) and forbid behavior (how to react when authorization of a user fails).
The configuration method for AddAuthentication
provides a way to configure the default scheme. We will show it in detail later.
An authentication handler is a class, where we will define how to react to a specific scheme. To implement a handler, we will either have to implement the interface IAuthenticationHandler
or derive from class AuthenticationHandler<TOptions>
.
Inside the handler, we can use our own logic for authenticating a user.
Creating a Web API Application
To demonstrate the feature, I will create a ASP.Net Core Web API application. The application will have a simple name API, which will return names of few states. But to access the API the caller will first authenticate using a /name/authenticate
API endpoint.
To create a new ASP.Net Core Web API application, I will use Visual Studio 2019. After opening Visual Studio 2019, I will click on “Create a new project” option.
From the list of project templates, I will select “ASP.NET Core Web Application”. And create a new project named “Auth.Demo”. After that, I will select “API” as the type of project and will keep Docker enable. And, I will use ASP.NET Core 3.0.
Once the project is created, I will create a new API to authenticate. But firstly, I will delete the auto-generated classes WeatherForecast
and WeatherForecastController
.
Creating Name API
Firstly, I will create a new API, by right-clicking the “Controllers” folder, then selecting “Add -> Controller” menu option.
Secondly, when the Add New item popup appears, I will select the “API Controller with read/write actions” option.
Finally, I will name the controller as “NameController”. From the auto-generated controller class, I will delete the code for Post
, Put
and Delete
methods, as they are not relevant for this example.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace Auth.Demo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class NameController : ControllerBase
{
// GET: api/Name
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET: api/Name/5
[HttpGet("{id}", Name = "Get")]
public string Get(int id)
{
return "value";
}
}
}
Create the Authenticate method
Inside of the NameController
class, I will create a new HTTP POST method named Authenticate
. This method will accept the UserCred
object parameter from the body of the POST request.
The UserCred
class contain two properties Username
and Password
.
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody] UserCred userCred)
{
return Ok();
}
public class UserCred
{
public string Username { get; set; }
public string Password { get; set; }
}
But before we proceed further to implement the Authenticate
method, let us first configure Startup
class to wire up the authentication middleware.
Wiring up Startup for authentication middleware
Firstly, inside the Startup
classes Configure
method, I will call UseAuthentication
extension method on the IApplicationBuilder
instance. I will call this method just above the call of UseAuthorization
.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
At this point, I have asked the .Net Core to use authentication, but have not provided any authentication schemes yet. But before we do that, we have to consider how everything will play out.
Once a user is authenticated, we will let the authorizer determine if the Identity created is allowed to access a particular resource or not. For this example, we will not have any authorization, we will let all authenticated users have access to all resources.
Post authentication, we will send a token back to the caller, using which the caller will make subsequent resource calls. This token generation and lifetime management process can be custom. Or we can use something like JWT.
What is JWT
JWT stands for JSON Web Token. JWT is JSON based access token created for claims. It is a self-contained and compact standard for an access token to securely transfer claims.
For our project, we will use JWT. For creating a JWT, we can use different hash algorithms. We will use HS256 algorithm for this project.
Adding Authentication
Now, I will update the Startup class to call the extension method AddAuthentication
of IServiceCollection
instance inside of the ConfigureServices
method. I will add NuGet package “Microsoft.AspNetCore.Authentication” to enable JWT.
var tokenKey = Configuration.GetValue<string>("TokenKey");
var key = Encoding.ASCII.GetBytes(tokenKey);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
Configuration file below:
{
"TokenKey": "This is my test private key",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
In the above code, first, inside of the AddAuthentication
method, I am setting the default authentication and challenge scheme as JwtBearerDefaults.AuthenticationScheme
.
Secondly, I am calling the AddJwtBearer
extension method. And inside of the code I am setting the IssuerSigningKey
using the string key “This is my test private key” from the configuration file. The AddJwtBearer
will handle all requests and will check for a valid JWT Token in the header. If it is not passed, or the token is expired, it will generate a 401 Unauthorized
HTTP response.
Authentication Manager
Next, I am going to implement the authentication manager which will handle authentication of users.
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
namespace Auth.Demo
{
public interface IJWTAuthenticationManager
{
string Authenticate(string username, string password);
}
public class JWTAuthenticationManager : IJWTAuthenticationManager
{
IDictionary<string, string> users = new Dictionary<string, string>
{
{ "test1", "password1" },
{ "test2", "password2" }
};
private readonly string tokenKey;
public JWTAuthenticationManager(string tokenKey)
{
this.tokenKey = tokenKey;
}
public string Authenticate(string username, string password)
{
if (!users.Any(u => u.Key == username && u.Value == password))
{
return null;
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(tokenKey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, username)
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}
In JWTAuthenticationManager
class, I am keeping a constant dictionary of username and password for demo purposes. In a real-life scenario, this information will be saved encrypted in data storage.
Inside the Authenticate
method, I am checking against the dictionary if the username and password are available. If they are, then I will create a JWT token using the JWT API which will expire in an hour. And I am using HS256 algorithm for encryption of the token. Also, I am using the same key from the configuration, as I used in the Startup
class for configuring JWT handler.
Updating NameController
Finally, I will update NameController
class’s Authenticate
method to call JWTAuthenticationManager
and authenticate the incoming username and password. And send back the generated JWT token.
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Auth.Demo.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class NameController : ControllerBase
{
private readonly IJWTAuthenticationManager jWTAuthenticationManager;
public NameController(IJWTAuthenticationManager jWTAuthenticationManager)
{
this.jWTAuthenticationManager = jWTAuthenticationManager;
}
// GET: api/Name
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "New York", "New Jersey" };
}
// GET: api/Name/5
[HttpGet("{id}", Name = "Get")]
public string Get(int id)
{
return "New Jersey";
}
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody] UserCred userCred)
{
var token = jWTAuthenticationManager.Authenticate(userCred.Username, userCred.Password);
if (token == null)
return Unauthorized();
return Ok(token);
}
}
}
In the above code if the token returned is null, I am sending an Unauthorized
response back. Else sending a Ok
response with the JWT token generated.
Next, I will update the Startup
class to register JWTAuthenticationManager
.
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace Auth.Demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var tokenKey = Configuration.GetValue<string>("TokenKey");
var key = Encoding.ASCII.GetBytes(tokenKey);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSingleton<IJWTAuthenticationManager>(new JWTAuthenticationManager(tokenKey));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Finally, I will make an HTTP POST to create a JWT token and use this token to call api/name
.
Custom Authentication Handler
So far I was working with JWT token and the out of box API provided by nuget packages to manage all these. Now, let us consider, we will be using our own token generator. And hence our own custom authentication handler.
Creating custom authentication manager
Firstly, I will create a custom authentication manager. This new class CustomAuthenticationManager
, will also have a static list of username and password combo from which it will validate the user. And once validated it will create a GUID as a token, which will never expire (for simplicity of the solution). And it will expose the list of token created for the authentication handler to validate against.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Auth.Demo
{
public interface ICustomAuthenticationManager
{
string Authenticate(string username, string password);
IDictionary<string, string> Tokens { get; }
}
public class CustomAuthenticationManager : ICustomAuthenticationManager
{
private readonly IDictionary<string, string> users = new Dictionary<string, string>
{
{ "test1", "password1" },
{ "test2", "password2" }
};
private readonly IDictionary<string, string> tokens = new Dictionary<string, string>();
public IDictionary<string, string> Tokens => tokens;
public string Authenticate(string username, string password)
{
if (!users.Any(u => u.Key == username && u.Value == password))
{
return null;
}
var token = Guid.NewGuid().ToString();
tokens.Add(token, username);
return token;
}
}
}
Creating CustomAuthenticationHandler
Secondly, I will create a new class CustomAuthenticationHandler
, which will derive from AuthenticationHandler<T>
. Where T
is derived from AuthenticationSchemeOptions
. Hence, I will create a class BasicAuthenticationOptions
deriving from AuthenticationSchemeOptions
.
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Auth.Demo
{
public class BasicAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class CustomAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly ICustomAuthenticationManager customAuthenticationManager;
public CustomAuthenticationHandler(
IOptionsMonitor<BasicAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
ICustomAuthenticationManager customAuthenticationManager)
: base(options, logger, encoder, clock)
{
this.customAuthenticationManager = customAuthenticationManager;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Unauthorized");
string authorizationHeader = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(authorizationHeader))
{
return AuthenticateResult.NoResult();
}
if (!authorizationHeader.StartsWith("bearer", StringComparison.OrdinalIgnoreCase))
{
return AuthenticateResult.Fail("Unauthorized");
}
string token = authorizationHeader.Substring("bearer".Length).Trim();
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.Fail("Unauthorized");
}
try
{
return validateToken(token);
}
catch (Exception ex)
{
return AuthenticateResult.Fail(ex.Message);
}
}
private AuthenticateResult validateToken(string token)
{
var validatedToken = customAuthenticationManager.Tokens.FirstOrDefault(t => t.Key == token);
if (validatedToken.Key == null)
{
return AuthenticateResult.Fail("Unauthorized");
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, validatedToken.Value),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new System.Security.Principal.GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
Update in Startup
Thirdly, I will update Startup
class ConfigureServices
method to change the implementation of AddAuthentication
. Instead of using JWT token provider, I will use a custom token provider. And I will also register the CustomAuthenticationManager
to the dependency injection container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var tokenKey = Configuration.GetValue<string>("TokenKey");
var key = Encoding.ASCII.GetBytes(tokenKey);
services.AddAuthentication("Basic")
.AddScheme<BasicAuthenticationOptions, CustomAuthenticationHandler>("Basic", null);
services.AddSingleton<ICustomAuthenticationManager, CustomAuthenticationManager>();
}
Change to Controller class
Finally, I will change the NameController class to use ICustomAuthenticationManager for creating an authentication token.
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Auth.Demo.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class NameController : ControllerBase
{
private readonly ICustomAuthenticationManager customAuthenticationManager;
public NameController(ICustomAuthenticationManager customAuthenticationManager)
{
this.customAuthenticationManager = customAuthenticationManager;
}
// GET: api/Name
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "New York", "New Jersey" };
}
// GET: api/Name/5
[HttpGet("{id}", Name = "Get")]
public string Get(int id)
{
return "New Jersey";
}
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody] UserCred userCred)
{
var token = customAuthenticationManager.Authenticate(userCred.Username, userCred.Password);
if (token == null)
return Unauthorized();
return Ok(token);
}
}
}
Now, I will run the Postman to create a new GUID based token.
Next, I will call api/name
passing the newly created token.
Conclusion
In this blog, I covered two ways of managing authentication. One through JWT token. The other based on custom GUID based token. For the custom implementation, we can use anything as a solution. As it will be triggered every time a resource endpoint is called; as long as the class is annotated with Authorize
attribute.
If we want to escape a resource from the authentication check, the way we did for the Authenticate
method, we will annotate that method with AllowAnonymous
attribute.
Most importantly, the authentication handler and the middleware keeps the actual code clean.
Github URL for the source code: https://github.com/choudhurynirjhar/auth-demo
YouTube video: https://youtu.be/vWkPdurauaA