JwtSecurityToken Expiry Time Invalid .NET Core 3.1 - c#

I upgraded my app from .NET Core 2.2 to .NET Core 3.1. When I tried to test out my endpoints with PostMan I noticed I was getting 401 Unauth Error. When I look at the header I am seeing that the expiry time is invalid:
I took the following bearer token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiQm9iIiwibmJmIjoiMTYxNzk3Nzg1MSIsImV4cCI6IjE2MjMxNjE4NTYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiQmFza2V0YmFsbCIsIlJ1Z2J5IiwiRm9vdGJhbGwiXX0.QRLuXFeopf7QZ1NUzWcctuSfnNXiPgc2UH7NxAuHYvw
that I am getting on my generate token endpoint and decoded it with jwt.io and the exp field is "1623161856".
I converted that to a date object in Javascript and it equals 60 days in the future.
So token certainly is not expired. Not sure if I missing anything in the upgrade to .NET Core 3.1 but here is the relevant code:
In Startup.cs I have
public void ConfigureServices(IServiceCollection services)
{
// Initial Setup
services.AddMvc();
services.AddSingleton<IConfiguration>(Configuration);
// Call this in case you need aspnet-user-authtype/aspnet-user-identity
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Register the Swagger generator, defining one or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc(Configuration["v1"], new OpenApiInfo { Title = Configuration["Sports"], Version = Configuration["v1] });
});
services.AddDataProtection();
//Authentication Setup
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("AnythingYouWant")),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
options.SaveToken = true;
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
var accessToken = context.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", accessToken.RawData));
}
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/" + Configuration["v1"] + "/swagger.json", Configuration["Sports"]);
});
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("swagger", "swagger/");
});
app.UseWelcomePage("/swagger");
}
And the token is generated by one of my api endpoints. That code is as shown:
[HttpPost("SportApi/Token")]
[ServiceFilter(typeof(SportResourceFilter))]
public IActionResult Create(string key)
{
return new ObjectResult(GenerateToken(key));
}
private string GenerateToken(string someKey)
{
JwtSecurityToken token = new JwtSecurityToken();
List<SportAPIKey> ro = new List<SportAPIKey>();
if (!string.IsNullOrEmpty(someKey))
{
using (StreamReader r = new StreamReader("keys.json"))
{
string json = r.ReadToEnd();
ro = JsonConvert.DeserializeObject<List<SportAPIKey>>(json);
}
if (ro.Exists(sak => sak.SportAPIKeyValue.Equals(someKey)))
{
SportAPIKey sportapikey = ro.Find(sak => sak.SportAPIKeyValue.Equals(someKey));
List<Claim> lc = new List<Claim>();
Claim claimClient = new Claim(ClaimTypes.Name, sportapikey.Client);
lc.Add(claimClient);
foreach (string team in sportapikey.Teams)
{
lc.Add(new Claim(ClaimTypes.System, team.Trim()));
}
Claim claimEffDate = new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString());
lc.Add(claimEffDate);
int tokenLifespan = 60;
Claim claimExpDate = new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString());
lc.Add(claimExpDate);
foreach (string sport in sportapikey.Sports.Split(","))
{
lc.Add(new Claim(ClaimTypes.Role, sport.Trim()));
}
var claims = lc.ToArray();
token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("AnythingYouWant")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
}
}
return new JwtSecurityTokenHandler().WriteToken(token);
}

Your token contains the timestamps nbfand expas string:
"nbf": "1617977851",
"exp": "1623161856",
which is invalid. On https:\jwt.io you can see that something is wrong when you hover with your mouse over these values. Ususally it shows the timestamp, but in your example it shows: "Invalid date":
Timestamps in a JWT are supposed to be numerical values:
NumericDate
A JSON numeric value representing the number of seconds from
1970-01-01T00:00:00Z UTC until the specified UTC date/time
The values itself are correct:
The error happens during generation of the claims:
Claim claimExpDate = new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString());
lc.Add(claimExpDate);
Instead use this constructor, which lets you set the ClaimValueType:
new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
Or even better, let the framework add the expiration timestamp for you, using this constructor:
public JwtSecurityToken (string issuer = default, string audience = default, System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> claims = default, DateTime? notBefore = default, DateTime? expires = default, Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials = default);
e.g.:
JwtSecurityToken(expires: DateTime.UtcNow.AddDays(60), ...)

Related

ASP.NET Core 6: Users are logged out after 30 minutes, need 6 hours

I have an issue where no matter which settings I adjust, a user who has signed in is forcibly logged out after 30 minutes. I would like to extend this timeframe to 6 hours.
I am using a system where login credentials are provided externally. The system receives a JWT and uses the information contained there to authenticate the user. This is the root cause of the issue; users that log in via the .NET Framework's built-in authentication system don't have this problem. I don't know why or how.
Here are the highlights of what I have tried:
configuring application cookies in Program.cs:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.ExpireTimeSpan = TimeSpan.FromHours(6);
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
});
Using persistence and authentication properties in the authentication process:
await _signInManager.SignInAsync(
user,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6)
});
I have ensured I have data protection enabled in Program.cs:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
I have published it to a dedicated IIS server where I set the Idle Timeout to 6 hours.
I have set SlidingExpiration = true in all the places that have the option to do so.
I have tried all of the above together, separate, and in combination. User sessions are still restricted to 30 minutes. When I set the Application Cookies to expire after 1 second, the session expires after 1 second. But when I set them to 6 hours, it still expires at 30 minutes. I have no idea why.
What am I missing here? I have been struggling with this for days and have still found no solution. I've found similar questions on Stack Overflow and their solutions all include what I have tried here, to no avail.
I have double-checked and noticed that the 30 minute time-out does not affect users that authenticate via the .NET Framework's built-in login system (where they enter their username/password). This only seems to affect users that authenticate using the credentials delivered via JWT.
Here is the full Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("ConnectionString");
builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddControllersWithViews();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(6);
});
//Add JWT bearer and denied paths
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Jwt:Key"))
};
});
//GDPR compliance
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services.ConfigureNonBreakingSameSiteCookies();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = x =>
{
x.Response.Redirect("https://localhost:44329/Expired/Index/");
return Task.CompletedTask;
}
};
options.ExpireTimeSpan = TimeSpan.FromDays(14);
});
//define policy for different authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Administrator"));
options.AddPolicy("UsersOnly", policy => policy.RequireRole("User", "Editor", "Author"));
options.AddPolicy("RequireApprovedUser", policy => policy.Requirements.Add(new ApprovedUserRequirement(true)));
});
builder.Services.AddScoped<IAuthorizationHandler, ApprovedUserRequirementHandler>();
//Data Protection configuration
var keysFolder = Path.Combine(builder.Environment.ContentRootPath, "UserKeys");
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<DBContext>();
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(6);
options.Lockout.MaxFailedAccessAttempts = 5;
options.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultTokenProviders()
.AddDefaultUI()
.AddEntityFrameworkStores<DBContext>();
builder.Services.AddRazorPages();
//this will get the email settings in appsettings
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
//Use for sending email
builder.Services.AddTransient<IMailService, Project.Services.EmailSender>();
// Register the Google Analytics configuration
builder.Services.Configure<GoogleAnalyticsOptions>(options => builder.Configuration.GetSection("GoogleAnalytics").Bind(options));
// Register the TagHelperComponent for Google Analytics
builder.Services.AddTransient<ITagHelperComponent, GoogleAnalyticsTagHelper>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, UserClaimsPrincipalFactory<IdentityUser, IdentityRole>>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode >= 400)
{
context.Request.Path = "/Error/Index/" + context.Response.StatusCode;
await next();
}
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(builder => builder.AllowAnyOrigin()
.WithOrigins("https://localhost:44329", "https://localhost:44329")
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
Here is the code that authenticates incoming users:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}
Here are the settings I used in IIS. Note that I will have trouble adjusting settings unavailable on Plesk.
Welp, it took a lot of digging, but I found the solution.
I needed to ensure that users were saved to the IdentityUser tables using _userManager.CreateAsync(). The code for registering users I used would create a new IdentityUser every time without ensuring it went in the database. Without an entry in the IdentityUser tables, .NET Core can't use keys or sessions properly.
Here is the change to that code:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser? iUser;
//check to see if user is in IdentityUser
iUser = await _userManager.FindByEmailAsync(user.Email);
//if user is not in IdentityUser, create a new one:
if(iUser == null)
{
iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
//Very important! This allows the system to remember users
//Performing this operation also gives iUser the necessary keys
await _userManager.CreateAsync(iUser);
}
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}

NET 6 WebAPI with Hotchocolate GraphQL not authorizing

I have a NET 6 api that I am using HotChocolate 12.7 in. My queries work, but when I am trying to add the [Authorize] decorator to a query, and send the request with the Bearer token, I am getting the unauthorized response. I can not get it to recognize a properly authenticated user's JWT.
Here is the Program.cs
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Queries;
using altitude_api.Services;
using altitude_api.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Serilog;
namespace altitude_api
{
public class Program
{
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var key = configuration.GetSection("AppSettings").GetSection("SecretKey");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.SaveToken = true;
opt.RequireHttpsMetadata = false;
opt.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes((key.Value))),
};
});
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddQueryType<Query>()
.AddTypeExtension<HealthQuery>();
var services = builder.Services;
// Add Services
var appSettingsSection = builder.Configuration.GetSection("AppSettings").Get<AppSettings>();
services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
services.AddScoped<IAuthService, AuthService>();
var signinKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(key.Value));
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
Log.Information("App starting up");
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build();
});
var allowedOrigins = appSettingsSection.AllowedOriginList;
services.AddDbContext<AltitudeContext>(
options => options.UseSqlServer(configuration.GetConnectionString("AltitudeContext")));
services.AddCors(options => options.AddPolicy("EnableCORS", build =>
{
build
.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins(allowedOrigins.ToString())
.AllowCredentials();
}));
var app = builder.Build();
// app.MapControllers();
app.UseRouting();
app.UseCors("EnableCORS");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();
}
}
}
And here is the authentication code.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Novell.Directory.Ldap;
using Serilog;
namespace altitude_api.Services;
public class AuthService: IAuthService
{
private readonly AltitudeContext _altitudeContext;
private readonly AppSettings appSettings;
public AuthService(AltitudeContext altitudeContext, IOptions<AppSettings> _appSettings)
{
_altitudeContext = altitudeContext;
appSettings = _appSettings.Value;
}
public UserWithToken AuthenticateUser(string userName, string password)
{
var isValid = false;
var port = appSettings.LdapPort;
Log.Information($"Beginning to authenticate user {userName}");
using (var cn = new LdapConnection())
{
// connect
try
{
cn.Connect(appSettings.LdapServer, Convert.ToInt32(port));
cn.Bind(appSettings.ldapDomain + userName, password);
if (cn.Bound)
{
isValid = true;
Log.Information($"Successfully found user in LDAP {userName}");
}
}
catch (Exception ex)
{
Log.Error( ex,$"Error looking up {userName} in LDAP.");
throw ex;
}
}
return isValid ? GetToken(userName) : throw new Exception("Unable to authenticate user at this time");
}
public Users GetUser()
{
return _altitudeContext.Users.First(p => p.Username == "maxwell.sands");
}
public UserWithToken GetToken(string userName)
{
try
{
var roles = _altitudeContext.Roles;
var dbUser = _altitudeContext.Users.FirstOrDefault(p => p != null && p.Username == userName);
if (dbUser == null)
{
var ex = new Exception("User not found");
Log.Error(ex, "User is not found could not authenticate");
throw ex;
}
if(dbUser.ExpiryDttm < DateTime.Now)
{
var ex = new Exception("ERROR: User access expired.");
Log.Error(ex, "User is expired could not authenticate");
throw ex;
}
var role = (from rle in _altitudeContext.Roles
join m in _altitudeContext.UserRoles on rle.RoleId equals m.RoleId
join usr in _altitudeContext.Users on m.UserId equals usr.UserId
where usr.Username.ToLower() == dbUser.Username.ToLower()
select rle.RoleName).FirstOrDefault();
if (role == null)
{
var ex = new Exception("Role not found");
Log.Error(ex, "User is expired could not authenticate");
throw ex;
}
var secret = appSettings.SecretKey;
// authentication successful so 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.Name, userName),
new Claim(ClaimTypes.Role, role),
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
UserWithToken webuser = new UserWithToken();
webuser.UserName = dbUser.Username;
webuser.FirstName = dbUser.FirstName;
webuser.LastName = dbUser.LastName;
webuser.Id = dbUser.UserId;
webuser.Role = role;
webuser.Token = tokenHandler.WriteToken(token);
Log.Information($"{webuser.FirstName} {webuser.LastName} was successfully logged in.");
return webuser;
}
catch(Exception e)
{
Log.Information(e, "There was an error loading the user");
throw new Exception("There was an issue loading the user", e);
}
}
}
And finally here is the endpoint I am require authorization on.
using HotChocolate.AspNetCore.Authorization;
namespace altitude_api.Queries;
[ExtendObjectType(typeof(Query))]
public class HealthQuery
{
[Authorize]
public bool HeartBeat()
{
return true;
}
}
In response to #Arjav Dave, yes I did find the answer to this, almost by dumb luck. The order of things in the Program.cs is very important. My code looks like the following...
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
// omitted
}).Services
.AddAuthorization();
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddFiltering()
.AddSorting()
.AddQueryType<Query>()
and when I am setting up the app it looks like...
var app = builder.Build();
app.UseRouting();
app.UseCors("EnableCORS");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();
Hope that helps.

User.Identity.IsAuthenticated always false in .net core when user id type changed from string to integer

I have an asp.net core application using json web tokens for authentication, this worked fine when my user Id was a string, but stopped working after changing to an int.
My IdentityUser type was originally this
public class AppUser : IdentityUser
{
//other properties...
}
I updated this;
public class AppUser : IdentityUser<int>
{
//other properties...
}
I changed my Startup configuration from this;
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtIssuerOptions.Issuer;
options.Audience = jwtIssuerOptions.Audience;
options.SigningCredentials = jwtIssuerOptions.SigningCredentials;
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtIssuerOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtIssuerOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtIssuerOptions.Issuer;
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// 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;
o.SignIn.RequireConfirmedEmail = true;
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
To this;
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtIssuerOptions.Issuer;
options.Audience = jwtIssuerOptions.Audience;
options.SigningCredentials = jwtIssuerOptions.SigningCredentials;
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtIssuerOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtIssuerOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtIssuerOptions.Issuer;
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// add identity
var builder = services.AddIdentity<AppUser, IdentityRole<int>>(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
o.SignIn.RequireConfirmedEmail = true;
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole<int>), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
I use a the following method to generate the token, this is unchanged between implementations.
public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
identity.FindFirst(JwtClaimIdentifiers.Rol),
identity.FindFirst(JwtClaimIdentifiers.Id)
};
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
And finally this method is used to create the claims identity;
public ClaimsIdentity GenerateClaimsIdentity(string userName, int id)
{
return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[]
{
new Claim(JwtClaimIdentifiers.Id, id.ToString(), ClaimValueTypes.Integer32),
});
}
User.Identity.IsAuthenticated is now always false when validating and the ClaimsIdentity appears to be empty of the information I saw before converting my AppUser from a string ID type to an int Id type.
The best theory I have so far is that the problem may be related to serialisation/deserialisation of the token values but I'm clutching at straws and have little idea how I might debug it.
What have I missed?
Nothing glaring at me in your code (it looks like the primary difference is you swapped from AddIdentity to AddIdentityCore?), but I find with JWT auth issues, it really helps to run the app in debug mode and add handlers for JwtBearerEvents.
Add this to your Startup .AddJwtBearer call:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// ...
// Add this at the end:
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
// Put a breakpoint here
System.Console.WriteLine(context.Exception);
return Task.CompletedTask;
},
};
})
If you run in debug, will be able to inspect the context.Exception:
This should give you a better explanation of why JWT authentication is failing.
Taking Connor Low's advice I supplied delegates to all the JwtBearerOptions events. None of these where hit, indicating that my setup was insufficient and that the authentication scheme I had configured was not being used at all.
I reverted to addIdentityCore
var builder = services.AddIdentityCore<AppUser>(o =>
Although I could no longer specify that I was using IdentityRole<int> instead of the default like this;
var builder = services.AddIdentity<AppUser, IdentityRole<int>>(o =>
This seemed to be unnecessary provided I had the following line;
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole<int>), builder.Services);
With these changes in place authentication worked and the breakpoints I had added at Connor Low's suggestion were being hit.

Render View at runtime in ASP.NET Core 3.1

We are in the middle of upgrading our ASP.NET Core 1.0 web app to ASP.NET Core 3.1.
One thing that isn't working for us right now is our export service, which is used to generate PDFs from cshtml files. This code used to work on ASP.NET Core 1 app. Here's the code for rendering the cshtml files into a string:
public async Task<string> Export(object viewModel, string reportName, eExportType exportType, PdfExportOptions options)
{
using (var writer = new StringWriter())
{
var viewEngine = _context.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
// Find the view
var viewResult = viewEngine.GetView($"~/Views/Reports/{reportName}.cshtml", $"~/Views/Reports/{reportName}.cshtml", false);
if (viewResult?.Success == false) // This is the line that's not working: Success equals false
{
throw new Exception($"The report {reportName} could not be found.");
}
// Build ViewContext
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = viewModel
};
var tempData = new TempDataDictionary(_context, new SessionStateTempDataProvider(null));
var routingFeature = _context.Features[typeof(IRoutingFeature)] as RoutingFeature;
var actionContext = new ActionContext(_context, routingFeature?.RouteData, new ActionDescriptor());
var viewContext = new ViewContext(actionContext, viewResult?.View, viewData, tempData, writer, new HtmlHelperOptions());
// Render the view
if (viewResult?.View != null)
await viewResult.View?.RenderAsync(viewContext);
// Invoke the node.js service
var htmlContent = writer.GetStringBuilder().ToString();
var nodeServiceName = $"./NodeServices/{exportType.ToString().ToLower()}";
//var exportHandler = GetExportHandler(exportType, options); // TODO: Currently only works with PDF. Use export handlers for different export types
var result = await _nodeServices.InvokeAsync<byte[]>(nodeServiceName, htmlContent, options);
return Convert.ToBase64String(result);
}
}
For some reason viewResult?.Success is always false. I've tried all kinds of combinations to make it work, like viewEngine.GetView($"~/Views/Reports/{reportName}.cshtml", $"~/Views/Reports/{reportName}.cshtml", false),
viewEngine.GetView($"~/Views/Reports/{reportName}.cshtml", $"{reportName}.cshtml", false),
viewEngine.GetView($"Views/Reports/{reportName}.cshtml", $"{reportName}.cshtml", false)
etc. but none of them works, Success is always false, and the proerty viewResult.View is always null.
I've looked at many posts on StackOverflow, like this and many others but none of them solves our problem.
I'm suspecting that we might be doing something wrong at our Startup class, because the variable routingFeature above is also null, but I'm not completely sure about that. But just to make sure that out Startup class is configured correctly, here's the code for it:
public class Startup
{
public Startup(/*IHostingEnvironment env,*/ IConfiguration configuration)
{
Configuration = (IConfigurationRoot)configuration;
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<SmsProvidersSettings>(Configuration.GetSection("SmsProvidersSettings"));
// More configuration sections here, removed for brevity
services.AddControllers().AddNewtonsoftJson(options =>
{
// Use the default property (Pascal) casing
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization();
var sessionTimeout = Convert.ToDouble(Configuration.GetSection("GlobalSettings")["SessionTimeout"]);
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(typeof(Startup).Assembly));
services.AddSingleton(manager);
services.AddDistributedMemoryCache();
services.AddSession(options => options.IdleTimeout = TimeSpan.FromSeconds(sessionTimeout));
////services.AddMvc(options =>
////{
//// options.Filters.Add(new UserActionAuditFilterAttribute(new AuditHandler(Configuration["ConnectionStrings:Audit"])));
//// options.Filters.Add(new ApiValidationFilterAttribute());
//// options.Filters.Add(new GlobalExceptionFilter());
////});
//services.AddMemoryCache();
services.AddNodeServices(options => options.InvocationTimeoutMilliseconds = 60000);
services.Configure<RequestLocalizationOptions>(opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("he-IL")
};
opts.DefaultRequestCulture = new RequestCulture("he-IL", "he-IL");
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
if (Convert.ToBoolean(Configuration.GetSection("GlobalSettings")["UseScheduler"]))
{
services.AddHangfire(configuration =>
configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(
Configuration["ConnectionStrings:DigitalRural"],
new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
UseRecommendedIsolationLevel = true,
PrepareSchemaIfNecessary = true, // Default value: true
EnableHeavyMigrations = true, // Default value: false
UsePageLocksOnDequeue = true,
DisableGlobalLocks = false
}));
}
services.AddTransient<IRecurringJobManager, RecurringJobManager>();
services.AddTransient<IActionSystemService, ActionSystemService>();
// More serivces here, removed for brevity
services.AddScoped<IUnitOfWork>(sp =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var currentUser = httpContextAccessor.HttpContext?.Session.GetJson<SessionUser>(SessionStateKeys.CurrentUser);
int? userId = null;
if (currentUser != null)
userId = currentUser.UserId;
return new UnitOfWork(new DigitalRuralContext(Configuration["ConnectionStrings:DigitalRural"], userId), httpContextAccessor);
});
//
services.AddControllersWithViews();
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime, IGlobalApplicationDataService applicationDataService)
{
//var applicationDataService = app.ApplicationServices.GetService(typeof(IGlobalApplicationDataService)) as IGlobalApplicationDataService;
applicationDataService?.GetApplicationDataVM();
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
// Don't cache index.html
context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
context.Response.Headers.Add("Pragma", "no-cache");
}
await next();
// If there's no available file and the request doesn't contain an extension, we're probably trying to access a page.
// Rewrite request to use app root
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value))
{
context.Request.Path = "/"; // Put your Angular root page here
await next();
}
});
app.UseAuthentication();
app.UseAuthorization();
loggerFactory
.AddSerilog();
//// Ensure any buffered events are sent at shutdown
//appLifetime.ApplicationStopped.Register(Log.CloseAndFlush);
//if (env.IsDevelopment())
//{
// app.UseDeveloperExceptionPage();
// app.UseBrowserLink();
//}
//else
//{
// app.UseExceptionHandler("/Home/Error");
//}
////app.UseDefaultFiles();
//app.UseStaticFiles(new StaticFileOptions
//{
// OnPrepareResponse = context => { }
//});
////app.UseMvc();
if (Convert.ToBoolean(Configuration.GetSection("GlobalSettings")["UseScheduler"]))
{
app.UseHangfireDashboard("/scheduler", new DashboardOptions // Will be available at http://localhost:60209/scheduler
{
Authorization = new[] { new HangfireDashbordAuthorizationFilter() }
});
app.UseHangfireServer(new BackgroundJobServerOptions { StopTimeout = TimeSpan.FromSeconds(10) });
}
var applicationServices = app.ApplicationServices;
var httpContextAccessor = applicationServices.GetService<IHttpContextAccessor>();
app.UseFileServer(new FileServerOptions { FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")), RequestPath = "", EnableDefaultFiles = true });
app.UseRouting();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
}
So any guess as to why our code doesn't work?

ASP.NET Core JWT mapping role claims to ClaimsIdentity

I want to protect ASP.NET Core Web API using JWT. Additionally, I would like to have an option of using roles from tokens payload directly in controller actions attributes.
Now, while I did find it out how to use it with Policies:
Authorize(Policy="CheckIfUserIsOfRoleX")
ControllerAction()...
I would like better to have an option to use something usual like:
Authorize(Role="RoleX")
where Role would be automatically mapped from JWT payload.
{
name: "somename",
roles: ["RoleX", "RoleY", "RoleZ"]
}
So, what is the easiest way to accomplish this in ASP.NET Core? Is there a way to get this working automatically through some settings/mappings (if so, where to set it?) or should I, after token is validated, intercept generation of ClaimsIdentity and add roles claims manually (if so, where/how to do that?)?
You need get valid claims when generating JWT. Here is example code:
Login logic:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] ApplicationUser applicationUser) {
var result = await _signInManager.PasswordSignInAsync(applicationUser.UserName, applicationUser.Password, true, false);
if(result.Succeeded) {
var user = await _userManager.FindByNameAsync(applicationUser.UserName);
// Get valid claims and pass them into JWT
var claims = await GetValidClaims(user);
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
//...
} else {
throw new ApiException('Wrong username or password', 403);
}
}
Get user claims based UserRoles, RoleClaims and UserClaims tables (ASP.NET Identity):
private async Task<List<Claim>> GetValidClaims(ApplicationUser user)
{
IdentityOptions _options = new IdentityOptions();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName)
};
var userClaims = await _userManager.GetClaimsAsync(user);
var userRoles = await _userManager.GetRolesAsync(user);
claims.AddRange(userClaims);
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
var role = await _roleManager.FindByNameAsync(userRole);
if(role != null)
{
var roleClaims = await _roleManager.GetClaimsAsync(role);
foreach(Claim roleClaim in roleClaims)
{
claims.Add(roleClaim);
}
}
}
return claims;
}
In Startup.cs please add needed policies into authorization:
void ConfigureServices(IServiceCollection service) {
services.AddAuthorization(options =>
{
// Here I stored necessary permissions/roles in a constant
foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
options.AddPolicy(prop.GetValue(null).ToString(), policy => policy.RequireClaim(ClaimType.Permission, prop.GetValue(null).ToString()));
}
});
}
ClaimPermission:
public static class ClaimPermission
{
public const string
CanAddNewService = "Tự thêm dịch vụ",
CanCancelCustomerServices = "Hủy dịch vụ khách gọi",
CanPrintReceiptAgain = "In lại hóa đơn",
CanImportGoods = "Quản lý tồn kho",
CanManageComputers = "Quản lý máy tính",
CanManageCoffees = "Quản lý bàn cà phê",
CanManageBillards = "Quản lý bàn billard";
}
Use the similar snippet to get all pre-defined permissions and insert it to asp.net permission claims table:
var staffRole = await roleManager.CreateRoleIfNotExists(UserType.Staff);
foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
await roleManager.AddClaimIfNotExists(staffRole, prop.GetValue(null).ToString());
}
I am a beginner in ASP.NET, so please let me know if you have better solutions.
And, I don't know how worst when I put all claims/permissions into JWT. Too long? Performance ? Should I store generated JWT in database and check it later for getting valid user's roles/claims?
This is my working code! ASP.NET Core 2.0 + JWT. Adding roles to JWT token.
appsettings.json
"JwtIssuerOptions": {
"JwtKey": "4gSd0AsIoPvyD3PsXYNrP2XnVpIYCLLL",
"JwtIssuer": "http://yourdomain.com",
"JwtExpireDays": 30
}
Startup.cs
// ===== Add Jwt Authentication ========
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
// jwt
// get options
var jwtAppSettingOptions = Configuration.GetSection("JwtIssuerOptions");
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 = jwtAppSettingOptions["JwtIssuer"],
ValidAudience = jwtAppSettingOptions["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
AccountController.cs
[HttpPost]
[AllowAnonymous]
[Produces("application/json")]
public async Task<object> GetToken([FromBody] LoginViewModel model)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
if (result.Succeeded)
{
var appUser = _userManager.Users.SingleOrDefault(r => r.Email == model.Email);
return await GenerateJwtTokenAsync(model.Email, appUser);
}
throw new ApplicationException("INVALID_LOGIN_ATTEMPT");
}
// create token
private async Task<object> GenerateJwtTokenAsync(string email, ApplicationUser user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
var roles = await _userManager.GetRolesAsync(user);
claims.AddRange(roles.Select(role => new Claim(ClaimsIdentity.DefaultRoleClaimType, role)));
// get options
var jwtAppSettingOptions = _configuration.GetSection("JwtIssuerOptions");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(Convert.ToDouble(jwtAppSettingOptions["JwtExpireDays"]));
var token = new JwtSecurityToken(
jwtAppSettingOptions["JwtIssuer"],
jwtAppSettingOptions["JwtIssuer"],
claims,
expires: expires,
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Fiddler test GetToken method. Request:
POST https://localhost:44355/Account/GetToken HTTP/1.1
content-type: application/json
Host: localhost:44355
Content-Length: 81
{
"Email":"admin#admin.site.com",
"Password":"ukj90ee",
"RememberMe":"false"
}
Debug response token https://jwt.io/#debugger-io
Payload data:
{
"sub": "admin#admin.site.com",
"jti": "520bc1de-5265-4114-aec2-b85d8c152c51",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "8df2c15f-7142-4011-9504-e73b4681fb6a",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
"exp": 1529823778,
"iss": "http://yourdomain.com",
"aud": "http://yourdomain.com"
}
Role Admin is worked!
For generating JWT Tokens we'll need AuthJwtTokenOptions helper class
public static class AuthJwtTokenOptions
{
public const string Issuer = "SomeIssuesName";
public const string Audience = "https://awesome-website.com/";
private const string Key = "supersecret_secretkey!12345";
public static SecurityKey GetSecurityKey() =>
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Key));
}
Account controller code:
[HttpPost]
public async Task<IActionResult> GetToken([FromBody]Credentials credentials)
{
// TODO: Add here some input values validations
User user = await _userRepository.GetUser(credentials.Email, credentials.Password);
if (user == null)
return BadRequest();
ClaimsIdentity identity = GetClaimsIdentity(user);
return Ok(new AuthenticatedUserInfoJsonModel
{
UserId = user.Id,
Email = user.Email,
FullName = user.FullName,
Token = GetJwtToken(identity)
});
}
private ClaimsIdentity GetClaimsIdentity(User user)
{
// Here we can save some values to token.
// For example we are storing here user id and email
Claim[] claims = new[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email)
};
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(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
return claimsIdentity;
}
private string GetJwtToken(ClaimsIdentity identity)
{
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(
issuer: AuthJwtTokenOptions.Issuer,
audience: AuthJwtTokenOptions.Audience,
notBefore: DateTime.UtcNow,
claims: identity.Claims,
// our token will live 1 hour, but you can change you token lifetime here
expires: DateTime.UtcNow.Add(TimeSpan.FromHours(1)),
signingCredentials: new SigningCredentials(AuthJwtTokenOptions.GetSecurityKey(), SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
In Startup.cs add following code to ConfigureServices(IServiceCollection services) method before services.AddMvc call:
public void ConfigureServices(IServiceCollection services)
{
// Other code here…
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = AuthJwtTokenOptions.Issuer,
ValidateAudience = true,
ValidAudience = AuthJwtTokenOptions.Audience,
ValidateLifetime = true,
IssuerSigningKey = AuthJwtTokenOptions.GetSecurityKey(),
ValidateIssuerSigningKey = true
};
});
// Other code here…
services.AddMvc();
}
Also add app.UseAuthentication() call to ConfigureMethod of Startup.cs before app.UseMvc call.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Other code here…
app.UseAuthentication();
app.UseMvc();
}
Now you can use [Authorize(Roles = "Some_role")] attributes.
To get user id and email in any controller you should do it like this
int userId = int.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);
string email = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
Also userId can be retrived this way (this is due to claim type name ClaimTypes.Name)
int userId = int.Parse(HttpContext.User.Identity.Name);
It's better to move such code to some controller extension helpers:
public static class ControllerExtensions
{
public static int GetUserId(this Controller controller) =>
int.Parse(controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);
public static string GetCurrentUserEmail(this Controller controller) =>
controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
}
The same is true for any other Claim you've added. You should just specify valid key.

Categories

Resources