Reputation: 752
How to implement Jwt token with ASP.NET CORE Authorization AspNetRoleClaims and Angular?
I have the following default roles (user types) for example:
Admin (Gerencia)
User
Client
A user that has the claim to viewRoles can add its own roles with custom claims.
For example
worker
claims canViewClients canViewInventory....
A user that has a claim to view employees and edit them can select from a dropdown the user type has you can see on my image.
When the user selects a user type my application auto fills the claims the user can have access to by default. However the user can always uncheck or check different claims the auto fill is only there to save some time. Here is what my checkbox look like visually.
Each checkbox represents a claim.
To recap
Here are the important parts of my startup.cs file.
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.Configure<ApplicationSettings>(Configuration.GetSection("AppSettings"));
services.AddTransient<DatabaseMigrator>();
services.AddDbContext<erp_colombiaDbContext>(options => options.UseMySql(
Configuration.GetConnectionString("DefaultConnection"),
optionsBuilder => optionsBuilder.MigrationsAssembly(typeof(DesignTimeDbContextFactory).Assembly.FullName)));
services.TryAddScoped<UserManager<Employee>>();
services.TryAddScoped<SignInManager<Employee>>();
//Should I add roles/policies here?
services.AddIdentityCore<Employee>(options =>
options.SignIn.RequireConfirmedAccount = false
).AddRoles<Entities.Type>().AddEntityFrameworkStores<erp_colombiaDbContext>();
//Jwt Authentication
var key = Encoding.UTF8.GetBytes("myKey");
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 4;
});
services.AddCors();
//Jwt Authentication
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.IncludeErrorDetails = true;
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = new TokenValidationParameters
{
RequireSignedTokens = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
x.Events = new JwtBearerEvents
{
OnAuthenticationFailed = c =>
{
// break point here
return Task.CompletedTask;
},
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("ViewClientsPolicy",
policy => policy.RequireClaim("View Clients"));
});
//services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IMenuService, MenuService>();
//Service for identity user.
services.AddScoped<IEmployeeService, EmployeeService>();
//Service for identity role.
services.AddScoped<ITypeService, TypeService>();
services.AddScoped<IGeneralConfigurationService, GeneralConfigurationService>();
services.AddScoped<IComponentLocationService, ComponentLocationService>();
services.AddScoped<INewsService, NewsService>();
services.AddScoped<INewsCategoryService, NewsCategoryService>();
services.AddScoped<INewsCategoriesService, NewsCategoriesService>();
services.AddScoped<IExternalLinksService, ExternalLinksService>();
services.AddScoped<IClientService, ClientService>();
services.AddScoped<ISupplierService, SupplierService>();
services.AddScoped<IContactService, ContactService>();
services.AddScoped<IUserTypeService, UserTypeService>();
services.AddScoped<IComponentService, ComponentService>();
services.AddScoped<IFamilyService, FamilyService>();
services.AddScoped<IContractMenuService, ContractMenuService>();
services.AddScoped<IContractService, ContractService>();
services.AddScoped<IHolidayService, HolidayService>();
services.AddScoped<IExpeditionService, ExpeditionService>();
services.AddScoped<IKanbanService, KanbanService>();
}
// 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();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
}
Here is what my employee looks like
[Table("erp_empleados")]
public class Employee : IdentityUser<ulong>
{
[Display(Name = "empName", ResourceType = typeof(SharedResource))]
[Column("nombre")]
public string Name { get; set; }
[Display(Name = "empLastName", ResourceType = typeof(SharedResource))]
[Column("apellido")]
public string FamilyName { get; set; }
[Display(Name = "empEmail", ResourceType = typeof(SharedResource))]
[Column("correo")]
public override string Email { get; set; }
//And many more fields
}
Here is what my user role class looks like
[Table("erp_tipo_usuario")]
public class Type : IdentityRole<ulong>
{
}
In my angular app I have the following code to guard my endpoints. I am not sure how to modify it so it requires claims? Update I now Understand I do not have to change anything in the jwt token the claims/roles will be included which is how it will work.
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
if (localStorage.getItem('token') != null)
return true;
else {
this.router.navigate(['/user/login']);
return false;
}
}
}
Too login In my app I have the following code.
[HttpPost, Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel user)
{
string passowrdHashed = HashPassword(user.Password);
var userFromDb = await _userManager.FindByNameAsync(user.UserName);
if (userFromDb == null)
{
return Unauthorized();
}
else if (user != null && userFromDb.UserName==user.UserName
&& userFromDb.PasswordHash == passowrdHashed)
{
if (userFromDb.PasswordHash == HashPassword(_employeeService.DEFAULT_PASSWORD))
{
return Ok("Please change password!");
}
else
{
//*********************************************************************
//****** How to add Code to add ASP.NET Identity claims??????????????? *******
//*********************************************************************
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim("UserID", "1")
new Claim(ClaimTypes.Role, role.NormalizedName),
}),
Issuer = "jcortenbach",
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superlongKeyWithALotOfWordsToMakeItMoreSecureWhichIsGoodThankYouForReadingMySecretKey@45")), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);
return Ok(new { token });
}
}
else
{
return Unauthorized();
}
}
Finally when it comes to my backend I think it should look something like this. For example the get client list code.
[Route("api/[controller]")]
[ApiController]
public class ClientsController : ControllerBase
{
private readonly ILogger<ClientsController> _logger;
private readonly IClientService _clientService;
public ClientsController(erp_colombiaDbContext context, ILogger<ClientsController> logger, IClientService clientService)
{
_logger = logger;
_clientService = clientService;
}
// GET api/clients
// **Update** I have add [Authorize(Roles = "comercial")] and comment out the [Authorize(Policy = "ViewClientsPolicy")] and the authentication works for user role based authentication I however would like to have policy based authentication as well.
[HttpGet]
[Authorize(Roles = "comercial")]
[Authorize(Policy = "ViewClientsPolicy")]
public async Task<IEnumerable<ClientViewModel>> GetAsync()
{
ClientViewModel clientViewModel;
List<ClientViewModel> listClientViewModels = new List<ClientViewModel>();
var clients = await _clientService.GetAllClients();
foreach (var client in clients)
{
clientViewModel = new ClientViewModel();
clientViewModel.ClientId = client.ClientId;
clientViewModel.Active = client.Active;
clientViewModel.Address = client.Address;
clientViewModel.City = client.City;
clientViewModel.ClienteName = client.ClienteName;
clientViewModel.ComercialEmployeeId = client.ComercialEmployeeId;
clientViewModel.Confirmed = client.Confirmed;
clientViewModel.CountryId = client.CountryId;
clientViewModel.CreationDate = client.CreationDate;
clientViewModel.DANE = client.DANE;
clientViewModel.Department = client.Department;
clientViewModel.ElectronicBillingEmail = client.ElectronicBillingEmail;
clientViewModel.Eliminated = client.Eliminated;
clientViewModel.NIT = client.NIT;
clientViewModel.PostalCode = client.PostalCode;
clientViewModel.Phone = client.Phone;
listClientViewModels.Add(clientViewModel);
}
return listClientViewModels;
}
I am uncertain what to do next has there are many puzzle pieces. Your help is greatly appreciated.
Upvotes: 1
Views: 538
Reputation: 27962
If you want to add some claims inside the Login method to new claim at new Claim[] array.
More details, you could refer to below codes:
[HttpPost, Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel user)
{
string passowrdHashed = HashPassword(user.Password);
var userFromDb = await _userManager.FindByNameAsync(user.UserName);
if (userFromDb == null)
{
return Unauthorized();
}
else if (user != null && userFromDb.UserName==user.UserName
&& userFromDb.PasswordHash == passowrdHashed)
{
if (userFromDb.PasswordHash == HashPassword(_employeeService.DEFAULT_PASSWORD))
{
return Ok("Please change password!");
}
else
{
//*********************************************************************
//****** How to add Code to add claims??????????????? *******
//*********************************************************************
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
//If you want to add cliams you could add as below:
new Claim("UserName", "test"),
new Claim("UserID", "1")
}),
Issuer = "jcortenbach",
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superlongKeyWithALotOfWordsToMakeItMoreSecureWichIsGoodThankYouForReadingMySecretKey@45")), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);
return Ok(new { token });
}
}
else
{
return Unauthorized();
}
}
Upvotes: 1