Role based Authentication: Bearer Token to Cookie using Microsoft .NetCore 3.1 - authentication

I'm trying to transform.NetCore 3.1 Code from Bearer Token implementation to Cookie-based implementation Also trying to make Role-based authorization work with existing code. Can you please help me to change this code? The below code shows currently how Bearer Token is retrieved and the next part shows how role-based authorization is implemented in code.
Here is the current Bearer Token implementation.
var key = Encoding.ASCII.GetBytes(Configuration["AppSettings:Secret"]);
var signingKey = new SymmetricSecurityKey(key);
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,
ValidateLifetime = true,
IssuerSigningKey = signingKey,
ValidateIssuer = false,
ValidateAudience = false
};
});
Following annotation currently used for Role-based Authorization -
[Authorize(Roles = "1")]
[Route("api/[controller]")]
[ApiController]
public class JobLogsController : ControllerBase
{
private readonly EtpRepoContext _context;
private IJobLogsRepository _jobLogsRepository;
private IConfiguration _configuration;
public JobLogsController(EtpRepoContext context, IJobLogsRepository jobLogsRepository, IConfiguration configuration)
{
_context = context;
_jobLogsRepository = jobLogsRepository;
_configuration = configuration;
}
// GET: api/JobLogs
[HttpGet]
public async Task<ActionResult<IEnumerable<JobLog>>> GetJobLog()
{
return await _context.JobLog.ToListAsync();
}
// GET: api/JobLogs/5
[HttpGet("{id}")]
[ProducesResponseType(typeof(JobDetail), 200)]
[ProducesResponseType(typeof(string), 400)]
public IActionResult FindById([FromRoute] String id)
{
string contentStr = "";
try
{
if(id.Length >= 10)
{
contentStr = _jobLogsRepository.GetLogById(id);
}
else
{
contentStr = _jobLogsRepository.GetFileById(id);
}
var content = Newtonsoft.Json.JsonConvert.SerializeObject(new { content = contentStr });
return Ok(content);
}
catch (Exception ex)
{
return StatusCode(500, "Internal server error");
}
}
This is how the Microsoft identity model is used to claim the token.
public class ClaimsTransformer : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
ClaimsIdentity claimsIdentity = (ClaimsIdentity)principal.Identity;
// flatten realm_access because Microsoft identity model doesn't support nested claims
// by map it to Microsoft identity model, because automatic JWT bearer token mapping already processed here
if (claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim((claim) => claim.Type == "identity"))
{
var realmAccessClaim = claimsIdentity.FindFirst((claim) => claim.Type == "identity");
dynamic realmAccessAsDict = JsonConvert.DeserializeObject<Object>(realmAccessClaim.Value);
string role = realmAccessAsDict.role.ToString();
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", role));
//var role = realmAccessClaim.
//var realmAccessAsDict = JsonConvert.DeserializeObject<Object>(realmAccessClaim.Value);
/*if (realmAccessAsDict["role"] != null)
{
foreach (var role in realmAccessAsDict["role"])
{
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", role));
}
}*/
}
return Task.FromResult(principal);
}
}
}

Related

.Net Core microservice and inspecting Cognito JWT claims for use in authorization

I am developing a .net service that I am passing a Cognito generated JWT in the client that has a group claim that I hope to use to restrict API access as the JWT is passed in as a Bearer token with each API call from the front-end. e.g.
"cognito:groups":["Guest"]
In my code now I have added:
services.AddAuthentication(DefaultScheme = JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
RoleClaimType = "cognito:groups"
};
});
I have setup up my roles and my permissions using the HeimGuard project. In testing as I change roles in the Bearer token none of my APIs that restrict access to Guest do not deny access when decorated with [Authorize]. So, nothing is restricted. Any thoughts or ideas you have in how to solve this problem helps immensly. Thank you!!
I tried custom AuthorizeAttribute as below:
To create the Token:
public class UserModel
{
public string Username { get; set; }
public string Role { get; set; }
public string EmailAddress { get; set; }
public string PassWord { get; set; }
public string Right { get; set; }
}
[HttpPost]
public IActionResult Authenticate(UserModel someuser)
{
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Role, someuser.Role),
new Claim(ClaimTypes.Name, someuser.Username),
new Claim("Right", someuser.Right)
};
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
signingCredentials: credentials,
expires: DateTime.Now.AddMinutes(2),
claims: claims
);
string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
return Content(jwtToken);
}
codes in appsettings.json:
"Jwt": {
"Key": "ThisismySecretKey",
"Issuer": "Test.com"
}
Custom requirement:
public class WriteRequirement : AuthorizationHandler<WriteRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, WriteRequirement requirement)
{
var right = context.User.FindFirst("Right").Value;
if(right=="Write")
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
in startup class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddSingleton<IAuthorizationHandler, WriteRequirement>();
services.AddAuthorization(opyions => { opyions.AddPolicy("Write", policy => policy.Requirements.Add(new WriteRequirement())); });
...
}
The Action need Authrization:
[HttpGet]
[Authorize(Roles ="Admin")]
[Authorize(Policy="Write")]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2", "value3", "value4", "value5" };
}
The Result:

Jwt Token audience validation failed when deployed

I am using ASPNET core 5.0 for both front-end and back-end API. It worked perfectly on the local machine, but I deploy both the front-end and API application it always gives me audience validation failure. here is the code I am using.
"Jwt": {
"Issuer": "RestaurantPortal",
"Audience": "http://mansoor0786-001-site1.ctempurl.com/",
"Key": "ASAscethtCVdAQAAAAEAACcQAAAAEDhnGasldjaslkjdleEnGunGWR4Z79AvrtgIjYXhcWZx4OqpvWbsdsdsdSafcV/ZuPw25KbhKWhg1SIXXU2Ad7maaGAk******"
},
I have kept this in appSettings of both front end and API applications. Here is API startup code
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build();
});
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(config =>
{
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
};
});
Here is the validation I am doing on API end when user wants to login.
public bool ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
}
catch (Exception)
{
return false;
}
return true;
}
Locally it works fine but when deploy these both applications it gives me an error and when I try to login it doesn't allow me to login into system. Here are the URL for both API and front-end application. This where I generate token
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: JwtConfiguration.JWTIssuer,
audience: JwtConfiguration.JWTAudience,
claims: claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return tokenString;
}
In this the configuration gets information from appSettings.json
public static class JwtConfiguration
{
public static readonly string JWTIssuer = Utils._config["Jwt:Issuer"];
public static readonly string JWTAudience = Utils._config["Jwt:Audience"];
public static readonly string JWTKey = Utils._config["Jwt:Key"];
}
This is my response from when I log in the user
if (apiResponseModel != null && apiResponseModel.Data != null && apiResponseModel.Data.Status == 1)
{
var claims = new List<Claim>
{
new Claim(AuthKeys.AccessToken, apiResponseModel.Data.AccessToken),
new Claim(AuthKeys.RefreshToken, apiResponseModel.Data.RefreshToken)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
if (apiResponseModel.Data.RoleName == UserRole.Roles.Customer.GetEnumDescription())
{
return RedirectToAction("index", "Home");
}
return RedirectToAction("index", "dashboard");
}
After that it redirected to dashboard index page where I wrote base controller and added attribute on top of basecontroller which does the following.
[ServiceFilter(typeof(JWT_Authentication))]
public class BaseController : Controller
{
public readonly IOptions<AppSettingDTO> _appSetting;
protected readonly IUserProfileInfo _userService;
public readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IHttpNetClientService _apiService;
public BaseController(IOptions<AppSettingDTO> AppSetting, IHttpNetClientService HttpService, IUserProfileInfo UserInfo, IHttpContextAccessor HttpContext)
{
_appSetting = AppSetting;
_apiService = HttpService;
_userService = UserInfo;
_httpContextAccessor = HttpContext;
}
}
Here is my JWT_Authentication
public class JWT_Authentication : ActionFilterAttribute
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IUserProfileInfo _userService;
public JWT_Authentication(IHttpContextAccessor HttpContext, IUserProfileInfo UserInfo)
{
_httpContextAccessor = HttpContext;
_userService = UserInfo;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string actionName = context.RouteData.Values["Action"].ToString().ToLower();
string controllerName = context.RouteData.Values["Controller"].ToString().ToLower();
if (
controllerName != "account" && actionName != "logout")
{
string accessTokens = _userService.GetToken(_httpContextAccessor);
if (!_userService.ValidateToken(accessTokens))
{
}
else
{
return;
}
context.Result = new RedirectToRouteResult(new RouteValueDictionary(){
{ "action", "LogOut" },
{ "controller", "Account" }
});
return;
}
}
}
API
http://mansoor00786-001-site1.gtempurl.com/
Front-End
http://mansoor0786-001-site1.ctempurl.com/
I am calling login API from the front-end application which is also in asp net core 5.0 but it doesn't log me into the dashboard because of validation failure and that is because of the audience.
Well, as far as i saw it, here is some points I spot
There won't ever be an exception was throw when calling ValidateToken
Cause we put it on try catch block, so where does it throw audience validation failure ? It cannot be during deployment cause catch block doesn't have logging support anywhere, therefore, it might just be your assumption. And behavior on production state should be always redirect to Login page after logout.
The way MVC project handle Jwt Token was cumbersome
As we handmade Jwt Token and validate them ourself, such thing as validation failure with the same setting (Issuer, audience,...) should not exists. If that was fine on the client, have faith and logging those setting out from production state.
And for current approach, We can validate Jwt token and restrict them from access our resource fine, but HttpContext.User object still be null, therefore Authorization process became mostly, unusable.
Instead, how about consider to write our own Authentication scheme ?
What might be the problem here ?
public class JWT_Authentication : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//... Some upper process
if (!_userService.ValidateToken(accessTokens))
{
// Doing something if the jwt invalid ?
}
else
{
return;
}
//... Some below process
}
}
If my block code idea was right, take a look at string accessTokens = _userService.GetToken(_httpContextAccessor);, log it out, as there might be a null here, due to you passing down a IHttpContextAccessor, which was singleton, not a HttpContext which scope for each request (localhost would be fine, cause we have only one client).

Add To Context Items in SignalR Middleware

So I want to setup a User Middleware which works for SignalR Hubs and Controllers.
It works fine with normal requests but with signalr it gets called but doesnt add to context.
Is it even possible? If so how can i do it?
namespace PortalCore.Middleware
{
public class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, AuthService authService)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
{
AttachUserToContext(context, authService, token);
}
await _next(context);
}
private async void AttachUserToContext(HttpContext context, AuthService authService, string token)
{
User user = null;
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(authService.SecretKey)),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
user = await authService.GetUserByUid(jwtToken.Claims.FirstOrDefault()?.Value);
}
catch (Exception e)
{
}
context.Items["User"] = user;
}
}
}
if you want to check auth of signalR hub then you can do it with query string.you can send token with signalR client url.After take token from query string and set to context.
Hub Code:
[Authorize]
public class ChatHub : Hub
you can add token in Context :
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrWhiteSpace(accessToken) &&
(path.StartsWithSegments("/api/hubs/chatHub")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},

ASP.Net Core Identity JWT Role-Base Authentication is Forbidden

Good day.
The API is for a Quote sharing web-app.
I have setup role-based JWT authentication where I have "Member" and "Admin" roles with users of those roles correctly registered and able to retrieve tokens.
So far, methods (or classes) with only
[Authorize]
can be correctly accessed provided a registered token.
Now once I added roles, access to methods or classes that require a certain role
[Authorize(Role="Admin")]
is Forbidden (403), even though I do pass a correct token with the Authorization header.
Please note: I have verified that users are correctly created (dbo.AspNetUsers), roles are correctly created (dbo.AspNetRoles containing "Admin" and "Member" roles) and user-roles are correctly mapped (dbo.AspNetUserRoles).
This is the Startup class which contains a method CreateRoles() that's called by Configure():
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<QuotContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<Member, IdentityRole>()
.AddEntityFrameworkStores<QuotContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 2;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
services.AddLogging(builder =>
{
builder.AddConfiguration(Configuration.GetSection("Logging"))
.AddConsole()
.AddDebug();
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider, QuotContext dbContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
app.UseDatabaseErrorPage();
}
app.UseAuthentication();
app.UseMvc();
dbContext.Database.EnsureCreated();
CreateRoles(serviceProvider).Wait();
}
private async Task CreateRoles(IServiceProvider serviceProvider)
{
//initializing custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<Member>>();
string[] roleNames = { "Admin", "Member" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (!roleExist)
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
var poweruser = new Member
{
UserName = Configuration["AppSettings:AdminEmail"],
Email = Configuration["AppSettings:AdminEmail"],
};
string password = Configuration["AppSettings:AdminPassword"];
var user = await UserManager.FindByEmailAsync(Configuration["AppSettings:AdminEmail"]);
if (user == null)
{
var createPowerUser = await UserManager.CreateAsync(poweruser, password);
if (createPowerUser.Succeeded)
await UserManager.AddToRoleAsync(poweruser, "Admin");
}
}
}
This is the MembersController class containing Register() and Login() methods:
[Authorize]
public class MembersController : Controller
{
private readonly QuotContext _context;
private readonly UserManager<Member> _userManager;
private readonly SignInManager<Member> _signInManager;
private readonly ILogger<MembersController> _logger;
private readonly IConfiguration _configuration;
public MembersController(QuotContext context, UserManager<Member> userManager,
SignInManager<Member> signInManager, ILogger<MembersController> logger,
IConfiguration configuration)
{
_context = context;
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_configuration = configuration;
}
[HttpPost("register")]
[AllowAnonymous]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
if (ModelState.IsValid)
{
var newMember = new Member
{
UserName = model.Email,
Email = model.Email,
PostCount = 0,
Reputation = 10,
ProfilePicture = "default.png"
};
var result = await _userManager.CreateAsync(newMember, model.Password);
if (result.Succeeded)
{
_logger.LogInformation(1, "User registered.");
await _signInManager.SignInAsync(newMember, false);
return Ok(new { token = BuildToken(model.Email, newMember) });
}
_logger.LogInformation(1, "Registeration failed.");
return BadRequest();
}
return BadRequest();
}
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Email,
model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in." + _configuration["AppSettings:AdminPassword"]);
var member = _userManager.Users.SingleOrDefault(r => r.Email == model.Email);
return Ok(new { token = BuildToken(model.Email, member) });
}
_logger.LogInformation(1, "Login failed.");
return BadRequest();
}
return BadRequest(ModelState);
}
private string BuildToken(string email, Member member)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, member.Id)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(Convert.ToDouble(_configuration["JwtExpireDays"]));
var token = new JwtSecurityToken(
_configuration["JwtIssuer"],
_configuration["JwtIssuer"],
claims,
expires: expires,
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Here's the example of 2 methods: the first requiring simple authentication which is successfully accessed provided a user token, and the second which is forbidden even given an admin token:
public class AuthorsController : Controller
{
private readonly QuotContext _context;
public AuthorsController(QuotContext context)
{
_context = context;
}
[HttpGet]
[Authorize]
public IEnumerable<Author> GetAuthors()
{
return _context.Authors;
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> PostAuthor([FromBody] Author author)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.Authors.Add(author);
await _context.SaveChangesAsync();
return StatusCode(201);
}
}
Thank you for your help.
A github repo containing the full project: https://github.com/theStrayPointer/QuotAPI
I got the same problem. I've just find a way. In fact, a JWT token embeds the roles. So you have to add role claims in your token when you generate it.
var roles = await _userManager.GetRolesAsync(user);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(dateTime).ToString(), ClaimValueTypes.Integer64)
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token");
// Adding roles code
// Roles property is string collection but you can modify Select code if it it's not
claimsIdentity.AddClaims(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var token = new JwtSecurityToken
(
_configuration["Auth:Token:Issuer"],
_configuration["Auth:Token:Audience"],
claimsIdentity.Claims,
expires: dateTime,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Auth:Token:Key"])), SecurityAlgorithms.HmacSha256)
);
Found an explanation here and here.

Is there any way to do token base auth in asp.net core application without redundant stuffs?

Anyone have a good example to do token based authorization in asp.net core without this crap like IdentityContext and other? I just want to set up settings for token generating in order to my system can generate and check token in right way and I want to manage authentication process by myself. Thanks
Having used a solution from this article go.microsoft.com/fwlink/?linkid=84547:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = Configuration["AuthOptions:Authority"];
o.RequireHttpsMetadata = false;
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["AuthOptions:Issuer"],
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthOptions:Key"])),
ValidateLifetime = true,
};
});
services.AddMvc();
ConfigureDependincies(services);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var configuration = new SlackConfiguration
{
WebhookUrl = new Uri("https://hooks.slack.com/services/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"),
MinLevel = LogLevel.Warning
};
loggerFactory.AddSlack(configuration, env);
// loggerFactory.AddDebug();
app.UseDefaultFiles();
app.UseDeveloperExceptionPage();
app.UseAuthentication();
//app.UseJwtBearerAuthentication(new JwtBearerOptions()
//{
// AutomaticAuthenticate = true,
// AutomaticChallenge = true,
// RequireHttpsMetadata = false,
// TokenValidationParameters = new TokenValidationParameters()
// {
// ValidIssuer = Configuration["AuthOptions:Issuer"],
// ValidateAudience = false,
// ValidateIssuerSigningKey = true,
// IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthOptions:Key"])),
// ValidateLifetime = true,
// }
//});
app.UseMvc();
}
var token = new JwtSecurityToken(
issuer: _root["AuthOptions:Issuer"],
notBefore: DateTime.UtcNow,
claims: identity.Claims,
expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(Convert.ToDouble(_root["AuthOptions:TokenLifeTime"]))),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_root["AuthOptions:Key"])), SecurityAlgorithms.HmacSha512)
);
return token;
It's working for me.
It can be done in following way
Have a token generator API endpoint (controller) i.e. http://localhost/auth/token
In this, you generate token by verifying the authenticate user (check user against store)
The generated token can be validated by providing authentication schema in ASP.NET Core pipeline. app.AddAuthentication()
Any subsequent calls to API, should have Authorization header with generated token.
This approach can be refined accordingly.
I've done this very thing. I got sick of all the third party things so I wrote my own.
You want to create tokens and provide them/ validate them through an api.
Here is an example of the api controller that creates the token initially.
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly TokenCreatorOption _tco;
private readonly CryptoHash _ch;
public TokenController(IOptions<TokenCreatorOption> ioptTCO, IOptions<CryptoHash> ioptCH, IOptions<ConnectionStrings> ioptConn)
{
_tco = ioptTCO.Value;
_ch = ioptCH.Value;
}
[HttpPost("")]
public async Task<IActionResult> IssueToken([FromBody] CredentialUser model)
{
///if model is null, this is an incorrect format
if(model == null)
{
return BadRequest();
}
var user = GetUserFromDatabaseOrStore(model.userName, model.passWord);
if(user == null)
{
return NotFound();
}
TokenCreatorOption newTCO = _tco; ///get your initial instantiation of the TokenCreatorOption. This is set to default values based off appsettings or in configure services
newTCO.UserObject = user;
newTCO.Expiration = DateTime.UtcNow.AddMinutes(30).ToString("yyyy-MM-dd hh:mm:ss.ss tt");
///anything within the TokenCreatorOption will be hashed, anything in the token Provider is not going to be hashed (not secured), but acts as a good object to store just general things that are needed on client side.
TokenProvider _tpo = new TokenProvider();
_tpo.tco = TokenInteraction.CreateToken(newTCO, _ch.salt);
_tpo.listApp = xapp; ///put anything you wouldn't want to be hashed and claimed against outside of the object. so you always validate things inside the tco, but never exclude anything inside tco. This was a fatal flaw in tokens in the past.
///this is using messagepack to serialize, to make it smaller since this is going to be passed between every request/response. Consider zipping as well if large enough.
var serializer = MessagePackSerializer.Get<TokenProvider>();
byte[] obj = null;
using (var byteStream = new MemoryStream())
{
serializer.Pack(byteStream, _tpo);
obj = byteStream.ToArray();
}
return File(obj, "application/octet-stream");
}
TokenCreatorOption Class
public class TokenCreatorOption
{
public string Issuer { get; set; }
public UserFromThatDatabaseOrStore UserObject { get; set; }
public string Expiration { get; set; }
public string HashValue { get; set; }
}
Notice that all these objects in TokenCreatorOption are claims. Every single one is checked in the hash function.
Here is the Token Creator and the Token Validator, once a token is valid, you can reissue a new one.
TokenInteraction
public static class TokenInteraction
{
public static TokenCreatorOption CreateToken(TokenCreatorOption _tco, byte[] salt)
{
byte[] exp = Encoding.UTF8.GetBytes(_tco.Expiration);
byte[] issuer = Encoding.UTF8.GetBytes(_tco.Issuer);
byte[] user = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_tco.UserObject));
byte[] salty = salt;
IEnumerable<byte> rv = exp.Concat(issuer).Concat(user).Concat(salty);
HashAlgorithm alg = SHA512.Create();
_tco.HashValue = Convert.ToBase64String(alg.ComputeHash(rv.ToArray()));
return _tco;
}
public static bool ValidateToken(TokenCreatorOption _tco, byte[] salt)
{
byte[] exp = Encoding.UTF8.GetBytes(_tco.Expiration);
byte[] issuer = Encoding.UTF8.GetBytes(_tco.Issuer);
byte[] user = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_tco.UserObject));
byte[] salty = salt;
IEnumerable<byte> rv = exp.Concat(issuer).Concat(user).Concat(salty);
HashAlgorithm alg = SHA512.Create();
if (_tco.HashValue != Convert.ToBase64String(alg.ComputeHash(rv.ToArray())))
{
return false;
}
else
{
return true;
}
}
Notice in TokenInteraction The order of bytes added to rv needs to be in the same order when we validate the token.
Now we can have a validate controller.
[Route("api/[controller]")]
public class ValidateController : Controller
{
private readonly TokenCreatorOption _tco;
private readonly CryptoHash _ch;
public ValidateController(IOptions<TokenCreatorOption> ioptTCO, IOptions<CryptoHash> ioptCH)
{
_tco = ioptTCO.Value;
_ch = ioptCH.Value;
}
[HttpPost("")]
public async Task<IActionResult> ValidateToken([FromBody] TokenCreatorOption model)
{
if (model == null)
{
return BadRequest("Model Cannot be Null");
}
///Kick them right now if session is expired, so we don't have to do the full hashing.
if (DateTime.ParseExact(model.Expiration, "yyyy-MM-dd hh:mm:ss.ss tt", CultureInfo.InvariantCulture) < DateTime.UtcNow)
{
return BadRequest("Expired Datetime");
}
if(!TokenInteraction.ValidateToken(model, _ch.salt))
{
return Unauthorized();
}
model.Expiration = DateTime.UtcNow.AddMinutes(30).ToString("yyyy-MM-dd hh:mm:ss.ss tt");
TokenProvider _tpo = new TokenProvider();
_tpo.tco = TokenInteraction.CreateToken(model, _ch.salt);
var serializer = MessagePackSerializer.Get<TokenProvider>();
byte[] obj = null;
using (var byteStream = new MemoryStream())
{
serializer.Pack(byteStream, _tpo);
obj = byteStream.ToArray();
}
return File(obj, "application/octet-stream");
}
And of course when you are initially registering the services. Create your salt value either through a cert, or through a random crypto number generator.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddCors();
services.AddOptions();
services.AddSwaggerGen();
services.ConfigureSwaggerGen(options =>
{
options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info
{
Version = "v1"
});
services.Configure<TokenCreatorOption>(myopt =>
{
myopt.Issuer = "Issuer"; //either from appsettings or manually
myopt.Expiration = null;
myopt.UserObject = null;
myopt.HashValue = "";
});
byte[] salty;
new RNGCryptoServiceProvider().GetBytes(salty = new byte[64]);
services.Configure<CryptoHash>(copt =>
{
copt.salt = (new Rfc2898DeriveBytes("Super!SecretKey!123456789!##$", salty, 1000)).GetBytes(64);
});
services.AddSingleton<IConfiguration>(Configuration);