ASP.NET Core Web API and roles authorization - c#

I write an application where ASP.NET Core Identity is responsible for authentication and authorization on the server. I also use JWT tokens in this purpose. Everything works fine, besides authorization based on roles. Simple [Authorize] attribute works, I need to be logged in to get the resource, but when I use [Authorize(Roles="Administrator")] none gets authorization.
private static async Task CreateUsers(
RoleManager<IdentityRole> roleManager,
UserManager<ApplicationUser> userManager)
{
string role_Administrator = "Administrator";
string role_RegisteredUser = "RegisteredUser";
await roleManager.CreateAsync(new IdentityRole(role_Administrator));
await roleManager.CreateAsync(new IdentityRole(role_RegisteredUser));
var user_Admin = new ApplicationUser()
{
SecurityStamp = Guid.NewGuid().ToString(),
UserName = "Admin",
Email = "admin#test.com",
};
await userManager.CreateAsync(user_Admin, "Pass4Admin");
await userManager.AddToRoleAsync(user_Admin, role_RegisteredUser);
await userManager.AddToRoleAsync(user_Admin, role_Administrator);
}
And example method from controller
[HttpGet("{id}")]
[Authorize(Roles = "Administrator")]
public IActionResult Get(int id)
{
return Ok();
}
I thought Identity is able to check roles assigned to authenticated user basing on data from created tables like aspnetroles and aspnetuserroles when the request comes from a client application, but apparently I don't understand something about this mechanism and I should configure it somehow.
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApplicationUser>().ToTable("AppUsers");
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddEntityFrameworkMySql();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))
);
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 7;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(opts =>
{
opts.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = true;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Auth:Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Auth:Jwt:Key"])),
ValidAudience = Configuration["Auth:Jwt:Audience"],
ClockSkew = TimeSpan.Zero,
RequireExpirationTime = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateAudience = true
};
cfg.IncludeErrorDetails = true;
});
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole",
policy => policy.RequireRole("Administrator"));
});
services.AddAutoMapper(typeof(WebMappingProfile));
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var dbContext = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
var roleManager = serviceScope.ServiceProvider.GetService<RoleManager<IdentityRole>>();
var userManager = serviceScope.ServiceProvider.GetService<UserManager<ApplicationUser>>();
dbContext.Database.Migrate();
DbSeeder.Seed(dbContext, roleManager, userManager);
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
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");
}
});
}

add role into your token as claim if you use custom jwt middleware
login action and token generator :
var role = await _userManager.GetRolesAsync(user);
IdentityOptions _options = new IdentityOptions();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim("UserID",user.Id.ToString()),
new Claim(_options.ClaimsIdentity.RoleClaimType,role.FirstOrDefault())
}),
Expires = DateTime.UtcNow.AddDays(_applicationSettings.Token_Expire_Day),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_applicationSettings.JWT_Secret)),
SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);

Related

ClaimTypes.NameIdentifier always return null

actually am new in asp.net core 3.1 , i am trying to create user login and register with cookies
when i am try to get ClaimTypes.NameIdentifier always return null, can you help me please ?
controller code
public class AccountController : ControllerBase
{
private readonly ApiSiteDbContext _db;
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
private readonly RoleManager<AppRole> _roleManager;
public AccountController(ApiSiteDbContext db,
UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager,
RoleManager<AppRole> roleManager)
{
_db = db;
_userManager = userManager;
_signInManager = signInManager;
_roleManager = roleManager;
}
[AllowAnonymous]
[HttpPost("Login")]
public async Task<IActionResult> Login(LoginModel loginModel)
{
var user = await _userManager.FindByEmailAsync(loginModel.Email);
// **** this is always return null *****
var id = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (id != null)
{
return BadRequest("User already logged !!");
}
var result = await _signInManager.PasswordSignInAsync(user, loginModel.Password, loginModel.RememberMe, true);
if (result.Succeeded)
{
if (await _roleManager.RoleExistsAsync("User"))
{
if (!await _userManager.IsInRoleAsync(user, "User"))
{
await _userManager.AddToRoleAsync(user, "User");
}
}
var roleName = await GetRoleNameByUserId(user.Id);
if (roleName != null)
{
AddCookies(user.UserName, user.Id, roleName, loginModel.RememberMe, user.Email);
}
return Ok();
}
else if (result.IsLockedOut)
{
return Unauthorized("Your account were locked");
}
return BadRequest("Wrong password!");
//return StatusCode(StatusCodes.Status204NoContent);
}
public async void AddCookies(string userName, string userId, string roleName, bool remember, string email)
{
var claim = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Email, email),
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Role, roleName),
};
var claimIdentity = new ClaimsIdentity(claim, CookieAuthenticationDefaults.AuthenticationScheme);
if (remember)
{
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddDays(10)
};
await HttpContext.SignInAsync
(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimIdentity),
authProperties
);
}
else
{
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
IsPersistent = false,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30)
};
await HttpContext.SignInAsync
(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimIdentity),
authProperties
);
}
}
}
and in Startup.cs
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<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = Context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddControllers();
//services.AddControllersWithViews();
services.AddDbContext<ApiSiteDbContext>();
services.AddIdentity<AppUser, AppRole>(option =>
{
option.Password.RequireDigit = true;
option.Password.RequiredLength = 6;
option.Password.RequiredUniqueChars = 0;
option.Password.RequireLowercase = true;
option.Password.RequireNonAlphanumeric = true;
option.Password.RequireUppercase = true;
option.SignIn.RequireConfirmedEmail = true;
option.Lockout.MaxFailedAccessAttempts = 5;
option.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
}).AddEntityFrameworkStores<ApiSiteDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LogoutPath = "/api/Account/Logout";
//options.LoginPath = "/api/Account/Login";
//options.AccessDeniedPath = "/api/Account/accessDenied";
options.SlidingExpiration = true;
});
services.AddMvc(options => options.EnableEndpointRouting = false)
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
services.AddCors();
}
// 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.UseHttpsRedirection();
app.UseRouting();
app.UseCors(x => x.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod().AllowCredentials());
app.UseMvc();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
when i make this variable to check the NameIdentifier if return id or not before i use it in a different controller .
Make sure that the roleName variable is not a null or string empty, because adding cookies depends on this condition

Asp.Net Core 3.1 WebAPI with Asp-Net-Identity authorization failing

I have a WebAPI designed for beeing used as Authentication Server that has an endpoint for registering users, another one for login in users and another one to get the user profile.
Register works correctly and login works good also aparently.
My problem is the following: The user profile endpoint needs Authorization so I use the token that login endpoint returns and append it to the request as bearer token. Requests always return 401: Unauthorized even though I am using the token that login returned.
I test the services with postman and it doesnt return any error with token, only says that I'm unauthorized to make that request.
This are the responses in Postman:
Login request:
UserProfile request:
Here is the code:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
//Inject AppSettings
services.Configure<ApplicationSettings>(Configuration.GetSection("ApplicationSettings"));
services.AddMvc(option => option.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddDbContext<AuthenticationContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AuthenticationContext>();
services.AddCors();
// JWT Authentication
var key = Encoding.UTF8.GetBytes(Configuration["ApplicationSettings:JWT_Secret"].ToString());
var signingKey = new SymmetricSecurityKey(key);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
x.RequireHttpsMetadata = false;
x.SaveToken = false;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
});
}
// 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.UseCors(builder =>
builder.WithOrigins(Configuration["ApplicationSettings:Client_URL"].ToString())
.AllowAnyHeader()
.AllowAnyMethod()
);
app.UseMvc();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
ApplicationUserController (register and login endpoints):
{
[Route("api/[controller]")]
[ApiController]
public class ApplicationUserController : ControllerBase
{
private UserManager<ApplicationUser> _userManager;
private SignInManager<ApplicationUser> _signInManager;
private readonly ApplicationSettings _appSettings;
public ApplicationUserController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IOptions<ApplicationSettings> appSettings)
{
_userManager = userManager;
_signInManager = signInManager;
_appSettings = appSettings.Value;
}
[HttpPost]
[Route("Register")]
// POST: /api/ApplicationUser/Register
public async Task<object> PostApplicationUser(ApplicationUserModel model)
{
var applicationUser = new ApplicationUser()
{
UserName = model.UserName,
Email = model.Email,
FullName = model.FullName
};
try
{
var result = await _userManager.CreateAsync(applicationUser, model.Password);
return Ok(result);
}
catch(Exception ex)
{
throw ex;
}
}
[HttpPost]
[Route("Login")]
// POST: /api/ApplicationUser/Login
public async Task<IActionResult> Login(LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.UserName);
if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserId", user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.JWT_Secret)), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);
return Ok(new { token });
}
else
{
return BadRequest(new { message = "Username or password is incorrect" });
}
}
}
}
UserProfileController (getUserProfile endpoint):
{
[Route("api/[controller]")]
[ApiController]
public class UserProfileController : ControllerBase
{
private UserManager<ApplicationUser> _userManager;
public UserProfileController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
[HttpGet]
[Authorize]
//GET : /api/UserProfile
public async Task<Object> GetUserProfile()
{
string userId = User.Claims.First(c => c.Type == "UserID").Value;
var user = await _userManager.FindByIdAsync(userId);
return new
{
user.FullName,
user.Email,
user.UserName
};
}
}
Thanks for your help and if you need some extra info just tell me

ASP.Net Core 3 API always returns 401- JwtBearer

I have an ASP .NET Core WebAPI and I generate a JWT token for authorization purposes but whenever I send the request I get 401 - Unauthorized.
The order of operations:
1. GET for token
2. GET for user <-- 401
I checked my token on jwt.io and it was correct.
When I remove [Authorize] attrivute everything works fine
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
var appSettingsSection = Configuration.GetSection("Jwt");
services.Configure<JwtSettings>(appSettingsSection);
var appSettings = appSettingsSection.Get<JwtSettings>();
services.AddControllers();
services.AddOptions();
services.AddAuthentication(x =>
{
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x=>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidAudience = appSettings.Issuer,
ValidIssuer = appSettings.Issuer,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.Key))
};
}
);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
CreateToken method
public JwtDto CreateToken(string email, string role)
{
var now = DateTime.UtcNow;
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub,email),
new Claim(ClaimTypes.Role, role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,now.ToTimestamp().ToString(),ClaimValueTypes.Integer64)
};
var expires = now.AddMinutes(360);
var singingCredentails = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.Key)),SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(
issuer: _settings.Issuer,
claims: claims,
notBefore: now,
expires: expires,
signingCredentials: singingCredentails
);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return new JwtDto
{
Token = token,
Expiry = expires.ToTimestamp()
};
}
GetToken - API
[HttpGet]
[Route("token")]
public IActionResult GetToken()
{
var token = _jwtHandler.CreateToken("test", "user");
return Json(token);
}
GetUser - API <---------- 401 error
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("{email}")]
public async Task<UserDto> Get(string email)
{
return await _userService.GetUserAsync(email);
}
I have come across the exact same problem and believe the issue is in the Configure() method in Startup.cs. You have the correct UseAuthentication() and UseAuthorization() calls, in the correct order, which is important, and was the problem I discovered. For you therefore I think the problem is the lack of the UseCors() call. My working Startup class is below:
public class Startup
{
private bool _isDevelopmentEnvironment = true;
public IConfiguration configuration { get; }
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Retrieve App Settings:
var appSettingsSection = configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
var appSettings = appSettingsSection.Get<AppSettings>();
// Configure JWT:
var key = Encoding.ASCII.GetBytes(appSettings.JwtSharedSecret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = !_isDevelopmentEnvironment;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = appSettings.JwtValidateIssuer,
ValidateAudience = appSettings.JwtValidateAudience,
ValidateLifetime = appSettings.JwtValidateLifetime,
ClockSkew = TimeSpan.Zero
};
});
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IHydrator<User, UserModel>, UserModelHydrator>();
}
// 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
{
_isDevelopmentEnvironment = false;
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
// TODO: Adjust CORS settings appropriately
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I also confirmed that both the token generation code and the Startup.cs code use the same key from application settings, I can't see how you get that in your CreateToken() method, but I assume it's from the same settings file. Hope this helps!

JWT Auth works with Authorize Attribute but not with [Authorize (Policy = "Administrator")]

I have a .NetCore 2.2 Application using Json Web Tokens to authenticate and authorize users.
When I add the [Authorize] Attribute to my controllers, I am able to add the Bearer Token to any requests to those controllers and interact with data.
When I change the Auth attribute to include a role, e.g. [Authorize (Policy="Administrator")] the requests always return a 403.
The User.cs model contains a Role enum with values User/Administrator.
Within Startup.cs I have added RequireRole/RequireAuthenticatedUser.
See Startup.cs
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.AddCors();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(options => { options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
#region JWT
// Configure AppSettings and add to DI
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// Configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
// Add Jwt Authentication Service
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
};
});
#endregion
#region Add Transient DI
services.AddTransient<IPlayerService, PlayerService>();
#endregion
#region Add Authorization
services.AddAuthorization(options =>
{
options.AddPolicy("Administrator",
p => p.RequireAuthenticatedUser().RequireRole(Role.Administrator.ToString())
);
options.AddPolicy("User",
p => p.RequireAuthenticatedUser().RequireRole(
new[] { Role.User.ToString(), Role.User.ToString() }
)
);
});
#endregion
#region Cookies
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.AccessDeniedPath = "/User/ErrorNotAuthorised";
options.LoginPath = "/User/ErrorNotAuthenticated";
});
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// seeder recreates and seeds database on each execution
new DataSeeder(new PlayerService(), new ClubService(), new TeamService(), new TeamPlayerService(), new UserService()).Seed();
}
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();
app.UseSpaStaticFiles();
app.UseCookiePolicy();
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{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");
}
});
}
}
Sample controller method:
// POST: api/Player
[Authorize(Policy="Administrator")]
[HttpPost]
[ValidateAntiForgeryToken]
public void Post([FromBody] Player player)
{
_service.AddPlayer(player);
}
This controller method returns a 403 unauthorized request from all interactions. I think my JWT token doesn't contain the Role value, but I'm not sure how to check or how to include it.
Any help is appreciated.
EDIT:
Watch on Users
Users class
public enum Role
{
Administrator,
User
}
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public Team Team { get; set; }
public Role Role { get; set; }
public string Token { get; set; }
}
EDIT 2:
So all that is really needed for the JWT to use Roles as a form of authentication is included in the Startup.cs function ConfigureServices below. I left out the JWT class, and have also included that below.
I changed the auth attribute on controllers to look for Roles = "Administrator" instead of Policies.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
// Configure AppSettings and add to DI
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// Configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
// Add Jwt Authentication Service
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
};
});
JWT Helper class that previous I did not understand:
{
// generate Jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Role, user.Role.ToString()),
new Claim(ClaimTypes.Sid, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(50),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
return user;
}
Sample of controller w/ Role attribute:
[Authorize(Roles = "Administrator")]
[HttpPost]
public void Post([FromBody] Player player)
{
_service.AddPlayer(player);
}
Finally, most of this is obvious and I should've knew before I started the project never mind this post - but updating so anyone who comes across this in the future sees the more appropriate route.
Make sure Role claims are picked up from JWT token. Role claim name can be set this way:
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
RoleClaimType = "role" // same name as in your JWT token, as by default it is
// "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var jwt = (context.SecurityToken as JwtSecurityToken)?.ToString();
// get your JWT token here if you need to decode it e.g on https://jwt.io
// And you can re-add role claim if it has different name in token compared to what you want to use in your ClaimIdentity:
AddRoleClaims(context.Principal);
return Task.CompletedTask;
}
};
});
private static void AddRoleClaims(ClaimsPrincipal principal)
{
var claimsIdentity = principal.Identity as ClaimsIdentity;
if (claimsIdentity != null)
{
if (claimsIdentity.HasClaim("role", "AdminRoleNameFromToken"))
{
if (!claimsIdentity.HasClaim("role", Role.Administrator.ToString()))
{
claimsIdentity.AddClaim(new Claim("role", Role.Administrator.ToString()));
}
}
}
}
And I would re-configure your policy as
options.AddPolicy("Administrator", policy => policy.RequireAssertion(context =>
context.User.IsInRole(Role.Administrator.ToString())
));
I misused the Policy extension of the Authorize attribute.
I should have been using [Authorize(Roles = "")].
I updated the question to reflect my mistake.

JWT request can't go through Authorize policy

I am implementing JWT Authorization on Asp.Net but I am having a trouble here. When I am trying to access the Dashboard/Home controller's method I am always recieving '401 Unauthorized' response. According to my research I can say that the request can not go through
[Authorize(Policy = "ApiUser")]
But when comment that statement I recieve an error here
var userId = _caller.Claims.Single(c => c.Type == "id");
Which states that sequence conatains no appropriate elements, and if I will print the
_httpContextAccessor.HttpContext.User.Claims
I basically recieve a '[]'.
Also I want you to notice that the token is
correct
I am testing the application with help of postman
I stacked on this error for two days and really rely on your help. Tell me if some additional code is needed.
Code:
Dashboard Controller:
[Authorize(Policy = "ApiUser")]
[Route("api/[controller]/[action]")]
public class DashboardController : Controller
{
private readonly ClaimsPrincipal _caller;
private readonly BackendContext _appDbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
public DashboardController(UserManager<AppUser> userManager, BackendContext appDbContext, IHttpContextAccessor httpContextAccessor)
{
_caller = httpContextAccessor.HttpContext.User;
_appDbContext = appDbContext;
_httpContextAccessor = httpContextAccessor;
}
// GET api/dashboard/home
[HttpGet]
public async Task<IActionResult> Home()
{
// retrieve the user info
//HttpContext.User
//return new OkObjectResult(_httpContextAccessor.HttpContext.User.Claims);
var userId = _caller.Claims.Single(c => c.Type == "id");
var customer = await _appDbContext.Customers.Include(c => c.Identity).SingleAsync(c => c.Identity.Id == userId.Value);
return new OkObjectResult(new
{
Message = "This is secure API and user data!",
customer.Identity.FirstName,
customer.Identity.LastName,
customer.Identity.PictureUrl,
customer.Identity.FacebookId,
customer.Location,
customer.Locale,
customer.Gender
});
}
}
Startup:
public class Startup
{
private const string SecretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; // todo: get this from somewhere secure
private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<BackendContext>()
.AddDefaultTokenProviders();
services.AddDbContext<BackendContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly("Backend")));
services.AddSingleton<IJwtFactory, JwtFactory>();
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
// add identity
var builder = services.AddIdentityCore<AppUser>(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<BackendContext>().AddDefaultTokenProviders();
services.AddAutoMapper();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddDbContext<BackendContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly("Backend")));
services.AddTransient<IStoreService, StoreService>();
services.AddMvc();//.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}
}
Constants (used in startup):
public static class Constants
{
public static class Strings
{
public static class JwtClaimIdentifiers
{
public const string Rol = "rol", Id = "id";
}
public static class JwtClaims
{
public const string ApiAccess = "api_access";
}
}
}
For your issue is caused by the mismatch configuration between token generation and validation for Issuer and Audience.
In Startup.cs, you configure ValidateIssuer = true and ValidateAudience = true, but for your provided token, there is no iss and aud.
Here are two ways for a try:
Disable to validate Issuer and Audience
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidIssuer = "Issuer",
ValidateAudience = false,
ValidAudience = "Audience",
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
Or, provide the Issuer and Audience while generating the token.
public IActionResult Login()
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: "Issuer",
audience: "Audience",
claims: new List<Claim>() { new Claim("rol", "api_access") },
expires: DateTime.Now.AddMinutes(25),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return Ok(new { Token = tokenString });
}
Here is a working demo CoreJwt.
It has passed about 4 months since this question has been asked, but I hope it will help the future visitors, I had the same problem and it was because I had missed to add the app.UseAuthentication(); to my Configure method:
app.UseAuthentication();
//////////////////////////
app.UseHttpsRedirection();
app.UseStaticFiles();
Also I had missed to add the JwtIssuerOptions to my appsettings.json and appsettings.Development.json:
"JwtIssuerOptions": {
"Issuer": "webApi",
"Audience": "http://localhost:5000/"
},

Categories

Resources