Asp.Net Core Authentication and Authrization

Spread the love

Authentication And Authorization In ASP.NET Core Web API With JSON Web Tokens

Introduction

Authentication is the process of validating user credentials and authorization is the process of checking privileges for a user to access specific modules in an application. In this article, we will see how to protect an ASP.NET Core Web API application by implementing JWT authentication. We will also see how to use authorization in ASP.NET Core to provide access to various functionality of the application. We will store user credentials in an SQL server database and we will use Entity framework and Identity framework for database operations.

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are:

  • Header
  • Payload
  • Signature

Therefore, a JWT typically looks like the following.

xxxx.yyyy.zzzz

Please refer to below link for more details about JSON Web Tokens.

https://jwt.io/introduction/

Create ASP.NET Core Web API using Visual Studio 2019

We can create an API application with ASP.NET Core Web API template.

We must install below libraries using NuGet package manager.

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

We can modify the appsettings.json with below values.

appsettings.json

  1. {  
  2.   “Logging”: {  
  3.     “LogLevel”: {  
  4.       “Default”: “Information”,  
  5.       “Microsoft”: “Warning”,  
  6.       “Microsoft.Hosting.Lifetime”: “Information”  
  7.     }  
  8.   },  
  9.   “AllowedHosts”: “*”,  
  10.   “ConnectionStrings”: {  
  11.     “ConnStr”: “Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SarathlalDB;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False”  
  12.   },  
  13.   “JWT”: {  
  14.     “ValidAudience”: “http://localhost:4200”,  
  15.     “ValidIssuer”: “http://localhost:61955”,  
  16.     “Secret”: “ByYM000OLlMQG6VVVp1OH7Xzyr7gHuw1qvUC5dcGt3SNM”  
  17.   }  
  18. }  

We have added a database connection string and also added valid audience, valid issuer and secret key for JWT authentication in above settings file.

Create an “ApplicationUser” class inside a new folder “Authentication” which will inherit the IdentityUser class. IdentityUser class is a part of Microsoft Identity framework. We will create all the authentication related files inside the “Authentication” folder.

ApplicationUser.cs

  1. using Microsoft.AspNetCore.Identity;  
  2.   
  3. namespace JWTAuthentication.Authentication  
  4. {  
  5.     public class ApplicationUser: IdentityUser  
  6.     {  
  7.     }  
  8. }  

We can create the “ApplicationDbContext” class and add below code.

ApplicationDbContext.cs

  1. using Microsoft.AspNetCore.Identity.EntityFrameworkCore;  
  2. using Microsoft.EntityFrameworkCore;  
  3.   
  4. namespace JWTAuthentication.Authentication  
  5. {  
  6.     public class ApplicationDbContext : IdentityDbContext<ApplicationUser>  
  7.     {  
  8.         public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)  
  9.         {  
  10.   
  11.         }  
  12.         protected override void OnModelCreating(ModelBuilder builder)  
  13.         {  
  14.             base.OnModelCreating(builder);  
  15.         }  
  16.     }  
  17. }  

Create a static class “UserRoles” and add below values.

UserRoles.cs

  1. namespace JWTAuthentication.Authentication  
  2. {  
  3.     public static class UserRoles  
  4.     {  
  5.         public const string Admin = “Admin”;  
  6.         public const string User = “User”;  
  7.     }  
  8. }  

We have added two constant values “Admin” and “User” as roles. You can add many roles as you wish.

Create class “RegisterModel” for new user registration.

RegisterModel.cs

  1. using System.ComponentModel.DataAnnotations;  
  2.   
  3. namespace JWTAuthentication.Authentication  
  4. {  
  5.     public class RegisterModel  
  6.     {  
  7.         [Required(ErrorMessage = “User Name is required”)]  
  8.         public string Username { getset; }  
  9.   
  10.         [EmailAddress]  
  11.         [Required(ErrorMessage = “Email is required”)]  
  12.         public string Email { getset; }  
  13.   
  14.         [Required(ErrorMessage = “Password is required”)]  
  15.         public string Password { getset; }  
  16.   
  17.     }  
  18. }  

Create class “LoginModel” for user login.

LoginModel.cs

  1. using System.ComponentModel.DataAnnotations;  
  2.   
  3. namespace JWTAuthentication.Authentication  
  4. {  
  5.     public class LoginModel  
  6.     {  
  7.         [Required(ErrorMessage = “User Name is required”)]  
  8.         public string Username { getset; }  
  9.   
  10.         [Required(ErrorMessage = “Password is required”)]  
  11.         public string Password { getset; }  
  12.     }  
  13. }  

We can create a class “Response” for returning the response value after user registration and user login. It will also return error messages, if the request fails.

Response.cs

  1. namespace JWTAuthentication.Authentication  
  2. {  
  3.     public class Response  
  4.     {  
  5.         public string Status { getset; }  
  6.         public string Message { getset; }  
  7.     }  
  8. }  

We can create an API controller “AuthenticateController” inside the “Controllers” folder and add below code.

AuthenticateController.cs

  1. using JWTAuthentication.Authentication;  
  2. using Microsoft.AspNetCore.Http;  
  3. using Microsoft.AspNetCore.Identity;  
  4. using Microsoft.AspNetCore.Mvc;  
  5. using Microsoft.Extensions.Configuration;  
  6. using Microsoft.IdentityModel.Tokens;  
  7. using System;  
  8. using System.Collections.Generic;  
  9. using System.IdentityModel.Tokens.Jwt;  
  10. using System.Security.Claims;  
  11. using System.Text;  
  12. using System.Threading.Tasks;  
  13.   
  14. namespace JWTAuthentication.Controllers  
  15. {  
  16.     [Route(“api/[controller]”)]  
  17.     [ApiController]  
  18.     public class AuthenticateController : ControllerBase  
  19.     {  
  20.         private readonly UserManager<ApplicationUser> userManager;  
  21.         private readonly RoleManager<IdentityRole> roleManager;  
  22.         private readonly IConfiguration _configuration;  
  23.   
  24.         public AuthenticateController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration)  
  25.         {  
  26.             this.userManager = userManager;  
  27.             this.roleManager = roleManager;  
  28.             _configuration = configuration;  
  29.         }  
  30.   
  31.         [HttpPost]  
  32.         [Route(“login”)]  
  33.         public async Task<IActionResult> Login([FromBody] LoginModel model)  
  34.         {  
  35.             var user = await userManager.FindByNameAsync(model.Username);  
  36.             if (user != null && await userManager.CheckPasswordAsync(user, model.Password))  
  37.             {  
  38.                 var userRoles = await userManager.GetRolesAsync(user);  
  39.   
  40.                 var authClaims = new List<Claim>  
  41.                 {  
  42.                     new Claim(ClaimTypes.Name, user.UserName),  
  43.                     new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),  
  44.                 };  
  45.   
  46.                 foreach (var userRole in userRoles)  
  47.                 {  
  48.                     authClaims.Add(new Claim(ClaimTypes.Role, userRole));  
  49.                 }  
  50.   
  51.                 var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration[“JWT:Secret”]));  
  52.   
  53.                 var token = new JwtSecurityToken(  
  54.                     issuer: _configuration[“JWT:ValidIssuer”],  
  55.                     audience: _configuration[“JWT:ValidAudience”],  
  56.                     expires: DateTime.Now.AddHours(3),  
  57.                     claims: authClaims,  
  58.                     signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)  
  59.                     );  
  60.   
  61.                 return Ok(new  
  62.                 {  
  63.                     token = new JwtSecurityTokenHandler().WriteToken(token),  
  64.                     expiration = token.ValidTo  
  65.                 });  
  66.             }  
  67.             return Unauthorized();  
  68.         }  
  69.   
  70.         [HttpPost]  
  71.         [Route(“register”)]  
  72.         public async Task<IActionResult> Register([FromBody] RegisterModel model)  
  73.         {  
  74.             var userExists = await userManager.FindByNameAsync(model.Username);  
  75.             if (userExists != null)  
  76.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User already exists!” });  
  77.   
  78.             ApplicationUser user = new ApplicationUser()  
  79.             {  
  80.                 Email = model.Email,  
  81.                 SecurityStamp = Guid.NewGuid().ToString(),  
  82.                 UserName = model.Username  
  83.             };  
  84.             var result = await userManager.CreateAsync(user, model.Password);  
  85.             if (!result.Succeeded)  
  86.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User creation failed! Please check user details and try again.” });  
  87.   
  88.             return Ok(new Response { Status = “Success”, Message = “User created successfully!” });  
  89.         }  
  90.   
  91.         [HttpPost]  
  92.         [Route(“register-admin”)]  
  93.         public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)  
  94.         {  
  95.             var userExists = await userManager.FindByNameAsync(model.Username);  
  96.             if (userExists != null)  
  97.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User already exists!” });  
  98.   
  99.             ApplicationUser user = new ApplicationUser()  
  100.             {  
  101.                 Email = model.Email,  
  102.                 SecurityStamp = Guid.NewGuid().ToString(),  
  103.                 UserName = model.Username  
  104.             };  
  105.             var result = await userManager.CreateAsync(user, model.Password);  
  106.             if (!result.Succeeded)  
  107.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User creation failed! Please check user details and try again.” });  
  108.   
  109.             if (!await roleManager.RoleExistsAsync(UserRoles.Admin))  
  110.                 await roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));  
  111.             if (!await roleManager.RoleExistsAsync(UserRoles.User))  
  112.                 await roleManager.CreateAsync(new IdentityRole(UserRoles.User));  
  113.   
  114.             if (await roleManager.RoleExistsAsync(UserRoles.Admin))  
  115.             {  
  116.                 await userManager.AddToRoleAsync(user, UserRoles.Admin);  
  117.             }  
  118.   
  119.             return Ok(new Response { Status = “Success”, Message = “User created successfully!” });  
  120.         }  
  121.     }  
  122. }  

We have added three methods “login”, “register”, and “register-admin” inside the controller class. Register and register-admin are almost same but the register-admin method will be used to create a user with admin role. In login method, we have returned a JWT token after successful login.

We can make below changes in “ConfigureServices” and “Configure” methods in “Startup” class as well.

Startup.cs

  1. using JWTAuthentication.Authentication;  
  2. using Microsoft.AspNetCore.Authentication.JwtBearer;  
  3. using Microsoft.AspNetCore.Builder;  
  4. using Microsoft.AspNetCore.Hosting;  
  5. using Microsoft.AspNetCore.Identity;  
  6. using Microsoft.EntityFrameworkCore;  
  7. using Microsoft.Extensions.Configuration;  
  8. using Microsoft.Extensions.DependencyInjection;  
  9. using Microsoft.Extensions.Hosting;  
  10. using Microsoft.IdentityModel.Tokens;  
  11. using System.Text;  
  12.   
  13. namespace JWTAuthentication  
  14. {  
  15.     public class Startup  
  16.     {  
  17.         public Startup(IConfiguration configuration)  
  18.         {  
  19.             Configuration = configuration;  
  20.         }  
  21.   
  22.         public IConfiguration Configuration { get; }  
  23.   
  24.         // This method gets called by the runtime. Use this method to add services to the container.  
  25.         public void ConfigureServices(IServiceCollection services)  
  26.         {  
  27.             services.AddControllers();  
  28.   
  29.             // For Entity Framework  
  30.             services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString(“ConnStr”)));  
  31.   
  32.             // For Identity  
  33.             services.AddIdentity<ApplicationUser, IdentityRole>()  
  34.                 .AddEntityFrameworkStores<ApplicationDbContext>()  
  35.                 .AddDefaultTokenProviders();  
  36.   
  37.             // Adding Authentication  
  38.             services.AddAuthentication(options =>  
  39.             {  
  40.                 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;  
  41.                 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;  
  42.                 options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;  
  43.             })  
  44.   
  45.             // Adding Jwt Bearer  
  46.             .AddJwtBearer(options =>  
  47.             {  
  48.                 options.SaveToken = true;  
  49.                 options.RequireHttpsMetadata = false;  
  50.                 options.TokenValidationParameters = new TokenValidationParameters()  
  51.                 {  
  52.                     ValidateIssuer = true,  
  53.                     ValidateAudience = true,  
  54.                     ValidAudience = Configuration[“JWT:ValidAudience”],  
  55.                     ValidIssuer = Configuration[“JWT:ValidIssuer”],  
  56.                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration[“JWT:Secret”]))  
  57.                 };  
  58.             });  
  59.         }  
  60.   
  61.         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.  
  62.         public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
  63.         {  
  64.             if (env.IsDevelopment())  
  65.             {  
  66.                 app.UseDeveloperExceptionPage();  
  67.             }  
  68.   
  69.             app.UseRouting();  
  70.   
  71.             app.UseAuthentication();  
  72.             app.UseAuthorization();  
  73.   
  74.             app.UseEndpoints(endpoints =>  
  75.             {  
  76.                 endpoints.MapControllers();  
  77.             });  
  78.         }  
  79.     }  
  80. }  

We can add “Authorize” attribute inside the “WeatherForecast” controller.

We must create a database and required tables before running the application. As we are using entity framework, we can use below database migration command with package manger console to create a migration script.

“add-migration Initial”

Use below command to create database and tables.

“update-database”

If you check the database using SQL server object explorer, you can see that below tables are created inside the database.

Above seven tables are used by identity framework to manage authentication and authorization.

We can run the application and try to access get method in weatherforecast controller from Postman tool.

We have received a 401 unauthorized error. Because, we have added Authorize attribute to entire controller. We must provide a valid token via request header to access this controller and methods inside the controller.

We can create a new user using register method in authenticate controller.

We can use above user credentials to login and get a valid JWT token.

We have received a token after successful login with above credentials.

We can pass above token value as a bearer token inside the authorization tab and call get method of weatherforecast controller again.

This time, we have successfully received the values from controller.

We can modify the weatherforecast controller with role-based authorization.

Now, only users with admin role can access this controller and methods.

We can try to access the weatherforecast controller with same token again in Postman tool.

We have received a 403 forbidden error now. Even though, we are passing a valid token we don’t have sufficient privilege to access the controller. To access this controller, user must have an admin role permission. Current user is a normal user and do not have any admin role permission.

We can create a new user with admin role. We already have a method “register-admin” in authenticate controller for the same purpose.

We can login with this new user credentials and get a new token and use this token instead of old token to access the weatherforecast controller.

We have again received the values from weatherforecast controller successfully.

We can see the token payload and other details using jwt.io site.

Inside the payload section, you can see the user name, role and other details as claims.

Conclusion

In this post, we have seen how to create a JSON web token in ASP.NET Core Web API application and use this token for authentication and authorization. We have created two users, one without any role and one with admin role. We have applied the authentication and authorization in controller level and saw the different behaviors with these two users.

Overview of ASP.NET Core authentication

  • Article
  • 06/04/2022
  • 11 minutes to read
  • 14 contributors

By Mike Rousos

Authentication is the process of determining a user’s identity. Authorization is the process of determining whether a user has access to a resource. In ASP.NET Core, authentication is handled by the authentication service, IAuthenticationService, which is used by authentication middleware. The authentication service uses registered authentication handlers to complete authentication-related actions. Examples of authentication-related actions include:

  • Authenticating a user.
  • Responding when an unauthenticated user tries to access a restricted resource.

The registered authentication handlers and their configuration options are called “schemes”.

Authentication schemes are specified by registering authentication services in Program.cs:

For example, the following code registers authentication services and handlers for cookie and JWT bearer authentication schemes:

C#Copy

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
        options => builder.Configuration.Bind("JwtSettings", options))
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
        options => builder.Configuration.Bind("CookieSettings", options));

The AddAuthentication parameter JwtBearerDefaults.AuthenticationScheme is the name of the scheme to use by default when a specific scheme isn’t requested.

If multiple schemes are used, authorization policies (or authorization attributes) can specify the authentication scheme (or schemes) they depend on to authenticate the user. In the example above, the cookie authentication scheme could be used by specifying its name (CookieAuthenticationDefaults.AuthenticationScheme by default, though a different name could be provided when calling AddCookie).

In some cases, the call to AddAuthentication is automatically made by other extension methods. For example, when using ASP.NET Core IdentityAddAuthentication is called internally.

The Authentication middleware is added in Program.cs by calling UseAuthentication. Calling UseAuthentication registers the middleware that uses the previously registered authentication schemes. Call UseAuthentication before any middleware that depends on users being authenticated.

Authentication concepts

Authentication is responsible for providing the ClaimsPrincipal for authorization to make permission decisions against. There are multiple authentication scheme approaches to select which authentication handler is responsible for generating the correct set of claims:

There’s no automatic probing of schemes. If the default scheme isn’t specified, the scheme must be specified in the authorize attribute, otherwise, the following error is thrown:

InvalidOperationException: No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).

Authentication scheme

The authentication scheme can select which authentication handler is responsible for generating the correct set of claims. For more information, see Authorize with a specific scheme.

An authentication scheme is a name that corresponds to:

  • An authentication handler.
  • Options for configuring that specific instance of the handler.

Schemes are useful as a mechanism for referring to the authentication, challenge, and forbid behaviors of the associated handler. For example, an authorization policy can use scheme names to specify which authentication scheme (or schemes) should be used to authenticate the user. When configuring authentication, it’s common to specify the default authentication scheme. The default scheme is used unless a resource requests a specific scheme. It’s also possible to:

  • Specify different default schemes to use for authenticate, challenge, and forbid actions.
  • Combine multiple schemes into one using policy schemes.

Authentication handler

An authentication handler:

Based on the authentication scheme’s configuration and the incoming request context, authentication handlers:

  • Construct AuthenticationTicket objects representing the user’s identity if authentication is successful.
  • Return ‘no result’ or ‘failure’ if authentication is unsuccessful.
  • Have methods for challenge and forbid actions for when users attempt to access resources:
    • They’re unauthorized to access (forbid).
    • When they’re unauthenticated (challenge).

RemoteAuthenticationHandler<TOptions> vs AuthenticationHandler<TOptions>

RemoteAuthenticationHandler<TOptions> is the class for authentication that requires a remote authentication step. When the remote authentication step is finished, the handler calls back to the CallbackPath set by the handler. The handler finishes the authentication step using the information passed to the HandleRemoteAuthenticateAsync callback path. OAuth 2.0 and OIDC both use this pattern. JWT and cookies don’t since they can directly use the bearer header and cookie to authenticate. The remotely hosted provider in this case:

  • Is the authentication provider.
  • Examples include FacebookTwitterGoogleMicrosoft, and any other OIDC provider that handles authenticating users using the handlers mechanism.

Authenticate

An authentication scheme’s authenticate action is responsible for constructing the user’s identity based on request context. It returns an AuthenticateResult indicating whether authentication was successful and, if so, the user’s identity in an authentication ticket. See AuthenticateAsync. Authenticate examples include:

  • A cookie authentication scheme constructing the user’s identity from cookies.
  • A JWT bearer scheme deserializing and validating a JWT bearer token to construct the user’s identity.

Challenge

An authentication challenge is invoked by Authorization when an unauthenticated user requests an endpoint that requires authentication. An authentication challenge is issued, for example, when an anonymous user requests a restricted resource or follows a login link. Authorization invokes a challenge using the specified authentication scheme(s), or the default if none is specified. See ChallengeAsync. Authentication challenge examples include:

  • A cookie authentication scheme redirecting the user to a login page.
  • A JWT bearer scheme returning a 401 result with a www-authenticate: bearer header.

A challenge action should let the user know what authentication mechanism to use to access the requested resource.

Forbid

An authentication scheme’s forbid action is called by Authorization when an authenticated user attempts to access a resource they’re not permitted to access. See ForbidAsync. Authentication forbid examples include:

  • A cookie authentication scheme redirecting the user to a page indicating access was forbidden.
  • A JWT bearer scheme returning a 403 result.
  • A custom authentication scheme redirecting to a page where the user can request access to the resource.

A forbid action can let the user know:

  • They’re authenticated.
  • They’re not permitted to access the requested resource.

See the following links for differences between challenge and forbid:

Authentication providers per tenant

ASP.NET Core doesn’t have a built-in solution for multi-tenant authentication. While it’s possible for customers to write one using the built-in features, we recommend customers to consider Orchard Core or ABP Framework for multi-tenant authentication.

Orchard Core is:

  • An open-source, modular, and multi-tenant app framework built with ASP.NET Core.
  • A content management system (CMS) built on top of that app framework.

See the Orchard Core source for an example of authentication providers per tenant.

Leave a Comment

Your email address will not be published. Required fields are marked *

https://www.cooljerseyedge.com, https://www.collegeshopfan.com, https://www.kcchiefsgearusa.com, https://www.dlionsgearusa.com, https://www.bravensgearusa.com, https://www.cbengalsgearusa.com, https://www.gbpackersgearusa.com, https://www.htexansgearusa.com, https://www.laramsgearusa.com, Josh Allen Wyoming Jersey, https://www.dcowboysgearusa.com, https://www.mdolphinsgearusa.com, https://www.aliexfanshop.com, https://www.bestplayershop.com, https://www.collegeedgeshop.com, https://www.giantsonlinefans.com