Authentication is the process of verifying the identity of a user by obtaining some sort of credentials and using those credentials to verify the user’s identity.
Authorization is the process of allowing an authenticated users to access the resources by checking whether the user has access rights to the system. Authorization helps you to control access rights by granting or denying specific permissions to an authenticated user.
There are a few authentication mechanisms:
Basic authentication is a simple authentication scheme built into the HTTP protocol. The client sends HTTP requests with the Authorization header that contains the word Basic word followed by a space and a base64-encoded string username:password. Flow:
1. Create a new empty API controller and call it e.g. BasicAuthController, and make a new action method Login:
2. public class BasicAuthController :
ControllerBase { [HttpGet] public IActionResult Login([FromHeader] string authorization) { Response.Headers.Add("WWW-Authenticate", "basic"); return Unauthorized(); }
The method creates a new Key-Value pair in the Header of the response and sends an 401 unauthorized message back to the client.
Extra reading:
The most important item to understand is the scheme below:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
If the server sends back the WWW-Authenticate key with Basic as value it will use the built-in authentication of the HTTP protocol and the browser will automatically popup a login screen:
If you fill in a username and password you will see a request will send an authorization key in the headers: (will be done in every request). This unreadable string is decoded with base64:
So we can catch this at server-side:
[HttpGet] public IActionResult Login([FromHeader] string authorization) { Response.Headers.Add("WWW-Authenticate", "basic"); var result = ""; if (authorization != null) { result = base64Decode(authorization.Substring(6)); if(GetUsername(result).Equals("tom") && GetPassword(result).Equals("tom")) { return Ok(); } } return Unauthorized(); }
public string base64Decode(string data) { try { System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding(); System.Text.Decoder utf8Decode = encoder.GetDecoder(); byte[] todecode_byte = Convert.FromBase64String(data); int charCount = utf8Decode.GetCharCount(todecode_byte, 0, todecode_byte.Length); char[] decoded_char = new char[charCount]; utf8Decode.GetChars(todecode_byte, 0, todecode_byte.Length, decoded_char, 0); string result = new String(decoded_char); return result; } catch (Exception e) { throw new Exception("Error in base64Decode" + e.Message); } }
public string GetUsername(string data) { var result = data.Substring(0, data.IndexOf(":")); return result; }
public string GetPassword(string data) { var result = data.Substring( data.IndexOf(":")+1, data.Length-1- data.IndexOf(":")); return result; }
I implemented a base64 decoder, and based on the string “username:password” I created simple helper methods the get the username and password. If the username and password are right, I send an 200 OK status code.
More info on status codes:
You also can logout, by writing a logout action method:
[HttpGet("Logout")] public IActionResult LogOut() { return Unauthorized(); }
Off course when you implement a basic authentication mechanism , for every request you always need to check your authentication credentials. To automate this process we can implement middleware.
Middleware is code that’s collected into a pipeline to handle requests and responses. You can write your own middleware, because it’s a class with some requirements:
https://medium.com/@hidayatarg/asp-net-core-jwt-authentication-middleware-reading-a-jwt-14cdb32e39ca
Let’s write my own middleware class so I don’t have to take care of the authentication mechanism in my controllers:
class AuthMiddleWare { private RequestDelegate next; public AuthMiddleWare(RequestDelegate next) { this.next = next; } public async Task Invoke(HttpContext context) { var auth = context.Request.Headers.FirstOrDefault(header => header.Key.ToLower() == "authorization"); if (string.IsNullOrEmpty(auth.Value)) { context.Response.Headers.Add("WWW-Authenticate", "Basic"); context.Response.StatusCode = 401; //401 error in response, so next middleware pipeline item not invoked } else { //TODO check username & password and correct, invoke next middleware await next.Invoke(context); } } }
You can see the constructor with the RequestDelegate as an argument, and in the Invoke method, I will execute my code. I cannot send an Ok() message or Unauthorized() message as in my action methods, but I can set some Key Values in my header by the HttpContext argument.
The HttpContext encapsulates all the HTTP-information about an individual HTTP request:
What is a cookie? A cookie is a header field created by the server into the response. The browser will always read the cookie value and stores it per domain and sends the cookie with every request to the server. (you don’t need to write any javascript code for this).
A cookie has a limited amount of data: 4096 bytes, and has an expiration DateTime. (you need to set up at server-side).
How it works:
1. Clients send a request but get an Unauthorized response.
2. Clients send username and password
3. The server verifies credentials and sets auth cookie
4. The client always sends a cookie 5. The server needs to validate the cookie Set cookie at server-side I create a new controller and a new Action method:
[Route("api/[controller]")] [ApiController] public class CookieController : ControllerBase { [HttpGet] public IActionResult CreateCookie() { //in the response I get a list of cookies Response.Cookies.Append("CookieTom", "test", new CookieOptions() { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(1)) }); return Ok(); } } }
In the response, I can add a new cookie and give it a key-value row with extra parameters, like an expire field. ( as a test it set 1 minute). Start your API application and check the header fields in the development tools of the browser:
You can switch to the Application tab where you can see all cookies per domain:
If you do a request to another action method (another controller), you can see the cookie is added into the request headers:
After the expire-time, you need to request a new cookie.
In the middleware, you can write code to read your cookie and decide the user can have access to your requested action method.
The trend nowadays is to delegate authentication to 3rd party identity providers. The providers are using a claim-based mechanism, meaning the user logs in with an identity provider like Google or Facebook. The provider checks the credentials but will not release the identity, but will provide you with claims which can be used to authorize the user in your application. Claims or ‘things’ about the logged-in user, e.g. first name, last name, date of birth, email, … What kind of claims you get depends on the identity provider.
This claim-based mechanism is baked into ASP.NET Core, and we can use this with middleware to login with cookie-based authentication.
In practice:
Create a new ClaimsController, and add a new Login ActionMethod
namespace Auth_Demo.Controllers { [Route("api/[controller]")] [ApiController] public class ClaimsController : ControllerBase { [HttpPost] public IActionResult Login(string username, string password) { //Check your username & password //now I will use claims of the ASP.NET CORE frameword var claims = new List(); claims.Add(new Claim("username", username)); //with the claims we can create an Identity: var identity = new ClaimsIdentity(claims, "claims"); //with the identity I need to crete a principal, and with the principal i can login in ASP.NET CORE var principal = new ClaimsPrincipal(identity); //login: HttpContext.SignInAsync(principal); return Ok(); } } }
In the action method, I check the username and password, and if this is ok, I will create a claim. You can do this with the build-in functionality of the ASP.NET CORE framework. Next, you need to create an identity based on the claims, and a new ClaimsPrincipal based on your identity. Finally you log in with the HttpContext.SignInAsync.
With signInAsync there will be an encrypted cookie created and added to the current response.
namespace Auth_Demo.Controllers { [Route("api/[controller]")] [ApiController] public class ClaimsController : ControllerBase { [HttpPost] public IActionResult Login(string username, string password) { if (username.Equals("tom") && password.Equals("tom")) { //Check your username & password //now I will use claims of the ASP.NET CORE frameword var claims = new List(); claims.Add(new Claim("username", username)); //with the claims we can create an Identity: var identity = new ClaimsIdentity(claims, "claims"); //with the identity I need to crete a principal, and with the principal i can login in ASP.NET CORE var principal = new ClaimsPrincipal(identity); //login: HttpContext.SignInAsync(principal); return Ok(); } else return Unauthorized(); } } }
Extra reading:
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1 https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimsidentity?view=netframework-4.8 https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal?view=netframework-4.8
Next, I will make use of existing middleware, but you need to activate the middleware in the startup class. So you have to say you will use cookie authentication, and in the pipeline (the configure method), you have to set up you will use authentication and authorization.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("cookieauthentication").AddCookie("cookieauthentication", settings =>
{
settings.Cookie.Name = "AuthCookie";
});
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// app.UseMiddleware<AuthMiddleWare>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
If you start the application and go to …/api/claims?username=tom&password=tom you will see that middleware created a cookie with the claims information:
The browser will send the cookie with every request. Via the HttpContext object (it’s a controller object) you have access to the claims of the user. You can test this by going to your claims controller first and log in with the right username and password, next you can go to another action method of a controller and check the HttpContext.User object. E.g. I created a projectscontroller with a GetProject() Action method:
[Route("api/[controller]")] [ApiController] public class ProjectsController : ControllerBase { [HttpGet] public IActionResult GetProject() { var debug = HttpContext.User; return Ok("project1"); } }
In my debug I check the claims list.
Now you can see that you can check your access in every action method by reading the claims list and check for the right claim, but you also can automate the process by adding an attribute per action method of for all action methods by adding the attribute at the controller:
[Authorize] [Route("api/[controller]")] [ApiController] public class ProjectsController : ControllerBase { [HttpGet] public IActionResult GetProject() { var debug = HttpContext.User; return Ok("project1"); } }
If you try to reach the GetProject() Action method, you will automatically be redirected to: https://localhost:44344/Account/Login?ReturnUrl=%2Fapi%2Fprojects
Before you reach this action method, the framework checks if the user is authenticated before you get access. If not authorized you get a 302 message, which is a redirect message. So if the user is not logged in, the framework says that you need to be redirected to the login page (/account/login ).
If you go to the claims controller first https://localhost:44344/api/claims?username=tom&password=tom And next, you will go back to your projects controller: https://localhost:44344/api/projects
You will see it works now!
You can customize the redirectlogin by adding extra parameters in the middleware. You need to customize this in the startup, so you return e.g. a 401 message (unauthorized); which is better for an API: public void ConfigureServices(IServiceCollection services) { services.AddAuthentication("cookieauthentication").AddCookie("cookieauthentication", settings => { settings.Cookie.Name = "AuthCookie"; settings.Events.OnRedirectToLogin = (context) => { context.Response.StatusCode = 401; //unauthorized return Task.CompletedTask; }; }); services.AddControllers(); } To remove the cookie and logout I can implement a LogOut method in my claimscontroller: public IActionResult Logout() { //cookie will be deleted HttpContext.SignOutAsync(); return Ok(); }
You don’t need to keep credentials at your own server, but you will use 3rd party credentials like Microsoft, Google, Facebook. The protocol you use is OAuth of OpenId. This protocol gives you the possibility to work with a 3rd party Identity Framework. You as a developer can choose which provider you work with. OAuth protocol With the OAuth protocol, you delegate the authentication functionality to an authorization server. The disadvantage is the complexity of the login flow (see scheme).
1. Client request access to the client application
2. The authorization server will send login form because you will be redirected by our own server
3. Between server and client applications, there will be a messaging with tokens and code.
https://docs.oracle.com/cd/E50612_01/doc.11122/oauth_guide/content/oauth_intro.html
See next topic and: