Authenticating Front-End Apps Using Cookies In .NET Core Web API

Introduction

 
In the old days, web applications were treated as one. There was no separation between front-end and back-end apps, at least not like today. The reason is because back then, computers were not as powerful as they are today. Browsers relied on servers to render the front end and return it as a simple HTML. This technique is called server-side rendering. This mindset is what gave rise to frameworks like ASP Webforms.
 
However, as innovations in computing increased rapidly, modern computers are much more powerful. Allowing it to render more client-side code. Thus, modern apps tend to benefit from this by implementing client-side rendering. I’m sure you have heard of Javascript frameworks like React and Vue. The complexity of these apps usually makes the separation of frontend and backend applications more reasonable.
 
Separation of concerns makes synchronization between apps more difficult especially because the apps are completely separated. Security concerns also arise because of the nature of Javascript frameworks to run on the client's machine.
 
In this article, I am going to give a walkthrough on how we can implement authentication for your frontend apps with a .NET Core backend using cookies.
 

Configuring the frontend

 
Before we configure our backend we need to configure our frontend. Why? Separate frontend and backend apps will have some sort of cross-origin resource sharing (CORS) policy.
 
Configuring Frontend The HTTP Request
 
So, by configuring our frontend first, we then only allow the parameters that we need. 
  1. fetch('https://apiendpoint.com/login', {  
  2.     method: 'GET',  
  3.     credentials: 'include',  
  4.     headers: {  
  5.       'Accept''application/json',  
  6.       'Content-Type''application/json',  
  7.       'Authorization''Basic ' + btoa(inst.user.email + ':' + inst.user.password)  
  8.     }  
  9.   })  
  10.   .then(  
  11.     function(response) {  
  12.       if (response.status !== 200) {  
  13.         console.log('Looks like there was a problem. Status Code: ' +  
  14.           response.status);  
  15.         return;  
  16.       }  
  17.     }  
  18.   )  
  19.   .catch(function(err) {  
  20.     console.log('Fetch Error :-S', err);  
  21.   });  
Let’s take this code snippet as an example. If we use this request we will need to allow all three Accept, Content-Type and Authorization headers. Also if we use HTTP methods like GET, POST, PATCHand DELETE we will need to allow those methods as well. Additionally, if we want to use one of the aforementioned HTTP methods we will also need to allow OPTIONS too. Because before each request the browser sends an OPTIONS request.
 
Receiving Cookies In Frontend
 
Oh, and notice this line within the code snippet:
  1. credentials: 'include'  
This line is crucial when we want to allow set-cookies in our frontend apps. For apps using the new fetch API, add credentials: 'include' in the request to enable cookies. For Axios users, use axios.defaults.withCredentials = true; before you initiate a new Axios client. For a more in-depth explanation of CORS for the frontend, you can check out the article here.
 
Configuring the Backend
 
The backend is where the magic happens — well, sort of…
 
You see, when handling CORS you will always need to allow it from the backend. Requests from the frontend only define what we need from the backend whilst the security is determined by our backend policies.
 
You can either configure the CORS policies from the backend app, or you configure it in your server configs. Either way, you only need to configure one. Because it seems that configurations on both sides can cause issues.
 

Defining CORS Policies

 
As mentioned in the previous section, you would need to define allowed methods, headers, and origins.
  1. services.AddCors(options =>  
  2.   options.AddPolicy("Dev", builder =>  
  3.   {  
  4.     // Allow multiple methods  
  5.     builder.WithMethods("GET""POST""PATCH""DELETE""OPTIONS")  
  6.       .WithHeaders(  
  7.         HeaderNames.Accept,  
  8.         HeaderNames.ContentType,  
  9.         HeaderNames.Authorization)  
  10.       .AllowCredentials()  
  11.       .SetIsOriginAllowed(origin =>  
  12.       {  
  13.         if (string.IsNullOrWhiteSpace(origin)) return false;  
  14.         // Only add this to allow testing with localhost, remove this line in production!  
  15.         if (origin.ToLower().StartsWith("http://localhost")) return true;  
  16.         // Insert your production domain here.  
  17.         if (origin.ToLower().StartsWith("https://dev.mydomain.com")) return true;  
  18.         return false;  
  19.       });  
  20.   })  
  21. );   
The policy builder allows us to fluently add methods that will be allowed through CORS. In the example above, we allowed GET, POST, PATCH, DELETE, and OPTIONS methods for HTTP requests. Additionally, we allow all three Accept, Content-Type and Authorization headers.
 
To successfully pass cookies through the APIs we need to allow credentials with AllowCredentials. If we allow credentials, we need to define the allowed origins in the backend as well. Thus we can either use WithOrigin or SetIsOriginAllowed. Both methods would work because per the documentation both methods seem to have the same goal.
 
Most CORS implementations would use withOrigin and end up setting a wildcard (i.e *) as the origin. This would work for some cases that do not implement credentials, but the best practice is to define which origins to allow. To achieve this, I chose to use the SetIsOriginAllowed method instead.
 
After implementing a dependency injection, you can implement the CORS policy by calling the policy name from the IApplicationBuilder.
  1. // Use our CORS policy we defined earlier.  
  2. app.UseCors("Dev");  
Adding Cookie Authentication through Dependency Injection
 
First, you need to determine your cookie configurations in the Startup.cs file.
  1. services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)  
  2.   .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>  
  3.   {  
  4.       options.Cookie.Name = "UserLoginCookie";  
  5.       options.SlidingExpiration = true;  
  6.       options.ExpireTimeSpan = new TimeSpan(1, 0, 0); // Expires in 1 hour  
  7.       options.Events.OnRedirectToLogin = (context) =>  
  8.       {  
  9.           context.Response.StatusCode = StatusCodes.Status401Unauthorized;  
  10.           return Task.CompletedTask;  
  11.       };  
  12.   
  13.       options.Cookie.HttpOnly = true;  
  14.       // Only use this when the sites are on different domains  
  15.       options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;  
  16.   });  
AddAuthentication extends the IServiceCollection configuration. It’s how we inject the cookie authentication scheme. After we add our authentication scheme using cookies, we will need to define the cookie. The AddCookie method does this.
 
We define our cookie name by using the Cookie.Name parameter and we define when our Cookie will expire by defining the ExpireTimeSpan (in our case the cookie will expire in an hour). The SlidingExpiration will make your cookies last longer by refreshing the expiration time when it comes close.
 

Securing The Cookie Configurations

 
To use secure cookies, using the HttpOnly cookie option is key. Essentially it protects our cookies from being retrieved by malicious XSS scripts. You should definitely implement it.
 
The next thing you should implement is the SameSite option. If your app has the same origin, you will most likely set SameSite to the Strict option. Otherwise, If you are using a cross-site cookie you might want to set the SameSite option to None. You might think, “How does this make my app secure?”. Well, it doesn’t. It will only allow your .NET Core app to authenticate cookies from other domains. But to protect from unwanted uses of your cookie, modern browsers will require you to add a Secure policy on your cookies.
 
You can implement this by extending the IApplicationBuilder:
  1. // Tells the app to transmit the cookie through HTTPS only.  
  2. app.UseCookiePolicy(  
  3.     new CookiePolicyOptions  
  4.     {  
  5.         Secure = CookieSecurePolicy.Always  
  6.     });  
But what does, Secure mean? It will check if the cookie is transmitted through HTTPS, and will only accept cookies from HTTPS. This is only forced when you want to set SameSite to None. That’s why in local environments I usually comment out both the SameSite and Secure policy.
 

Using The Cookie Authentication Middleware

 
To make the development process more seamless, implement UseAuthentication and UseAuthorization middleware in IApplicationBuilder.
 
Remember to implement this before the UseEndpoints method!
  1. // You need both of these for authorization using cookies.  
  2. app.UseAuthentication();  
  3. app.UseAuthorization();  
Implementing Cookie Auth on Methods
 
I will give you an example of how I wrote an implementation for login and logout methods.
  1. using Microsoft.AspNetCore.Authentication;  
  2. using Microsoft.AspNetCore.Authentication.Cookies;  
  3. using Microsoft.AspNetCore.Authorization;  
  4. using Microsoft.AspNetCore.Http;  
  5. using Microsoft.AspNetCore.Mvc;  
  6. using System;  
  7. using System.Linq;  
  8. using System.Text;  
  9.   
  10. namespace Example.Controller  
  11. {  
  12.   [ApiController]  
  13.   [Route("auth")]  
  14.   public class UserService : ControllerBase  
  15.   {  
  16.     [HttpGet]  
  17.     [Route("login")]  
  18.     [Produces("application/json")]  
  19.     public IActionResult Login([FromHeader(Name = "Authorization")] string authHeader)  
  20.     {  
  21.       try  
  22.       {  
  23.         // Get credentials  
  24.         string encodedEmailPassword = authHeader.Substring("Basic ".Length).Trim();  
  25.         string emailPassword = Encoding  
  26.         .GetEncoding("iso-8859-1")  
  27.         .GetString(Convert.FromBase64String(encodedEmailPassword));  
  28.   
  29.         // Get email and password  
  30.         int seperatorIndex = emailPassword.IndexOf(':');  
  31.         string email = emailPassword.Substring(0, seperatorIndex);  
  32.         string password = emailPassword.Substring(seperatorIndex + 1);  
  33.   
  34.         var context = new UserContext();  
  35.         var users = context.User.Where(x => x.Email == email).ToList();  
  36.         if ((users.Count > 0) && (BCryptPasswordHasher.VerifyHashedPassword(users.FirstOrDefault().Password, password)))  
  37.         {  
  38.           var claims = new Claim[]  
  39.           {  
  40.             new Claim("ID", users.FirstOrDefault().UserID.ToString()),  
  41.             new Claim("Name", users.FirstOrDefault().Name.ToString())  
  42.           };  
  43.   
  44.           var claimsIdentity = new ClaimsIdentity(  
  45.           claims, CookieAuthenticationDefaults.AuthenticationScheme);  
  46.   
  47.           HttpContext.SignInAsync(  
  48.           CookieAuthenticationDefaults.AuthenticationScheme,  
  49.           new ClaimsPrincipal(claimsIdentity)).Wait();  
  50.   
  51.           return Ok();  
  52.         }  
  53.   
  54.         return new UnauthorizedResult();  
  55.       }  
  56.       catch (Exception ex)  
  57.       {  
  58.         return StatusCode(500);  
  59.       }  
  60.     }  
  61.   
  62.     [HttpGet]  
  63.     [Route("logout")]  
  64.     [Produces("application/json")]  
  65.     [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]  
  66.     public IActionResult Logout()  
  67.     {  
  68.       try  
  69.       {  
  70.         HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme).Wait();  
  71.         return Ok();  
  72.       }  
  73.       catch (Exception ex)  
  74.       {  
  75.         return StatusCode(500);  
  76.       }  
  77.     }  
  78.   }  
  79. }  
You get the authentication cookie by calling SignInAsync on the HttpContext. This will set the cookie on the HTTP request when it is returned.
 
To use authentication methods on the cookies, we need to use the Authorize attribute on the method. This will activate the UseAuthentication and UseAuthorization middleware.
 
To destroy the cookie, simply call SignOutAsync on the HttpContext. The cookie will then expire and cease to be usable in all sessions.
 
Real-Life Example
 
I have implemented my solution and to show you how it works, I have attached a few screenshots. Hope this could help you understand what happens when all is implemented correctly.
 
Localhost cookie example
 
Here, is an example of what happens when the cookie setup works. The cookie will be saved and it can be checked inside the Application tab inside Chrome Dev Tools.
 
Example of the HTTP request, Image by Author
 
Notice that the SameSite attribute inside the cookie is lax. It is because I tested this in a localhost environment, thus I cannot set SameSite to None because it's required to be transported through HTTPS.
 
Conversely, if I were to implement HTTPS. The expected result will be as follows.
 
Example of the HTTPS request, Image by Author
 
This is an example of how my production environment cookies are. Notice the Secure attribute and the SameSite attribute are set to None. The logout request should have a response as follows:
 
Example of log out request, Image by Author 
 
Expect the set-cookie header to be empty, only returning a plain response.
 
That is how you use cookies for your front-end apps. Keep in mind that the session is not distributed and if you have multiple servers you need to implement distributed sessions. The most common one being Redis Session. An implementation of the Redis session in .NET Core can be seen here. CORS is complicated so, but there’s much to learn from it. I hope this article helps you in setting up CORS for your .NET Core app.


Similar Articles