I am attempting to authenticate a google jwt bearer token from my .net core webapi application and continually receive 401s. I have verified that the token is valid via jwt.io. I am trying to implement the solution offered here,
google-jwt-authentication-with-aspnet-core-2-0
Can anyone see what is wrong with my code?
Below is my code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Configure SnapshotCollector from application settings
services.Configure<SnapshotCollectorConfiguration>(Configuration.GetSection(nameof(SnapshotCollectorConfiguration)));
// Add SnapshotCollector telemetry processor.
services.AddSingleton<ITelemetryProcessorFactory>(sp => new SnapshotCollectorTelemetryProcessorFactory(sp));
conString = Microsoft
.Extensions
.Configuration
.ConfigurationExtensions
.GetConnectionString(this.Configuration, "DefaultConnection");
services.AddDbContext<GotNextDBContext>(
options =>
options.UseSqlServer(conString));
services.AddTransient<ILocationService, LocationService>();
services.AddTransient<ICompanyService, CompanyService>();
services.AddTransient<IUserLocationLogService, UserLocationLogService>();
services.AddTransient<IUserService, UserService>();
services.AddTransient<ILanguageService, LanguageService>();
services.AddTransient<IGenderService, GenderService>();
services.AddTransient<ISportService, SportService>();
services.AddTransient<IMeasurementService, MeasurementService>();
var clientIds = new List<string>();
clientIds.Add("[myClientId]");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.SecurityTokenValidators.Clear();
o.SecurityTokenValidators.Add(new GoogleTokenValidator(clientIds: clientIds ));
});
services.AddRouting();
services.AddAutoMapper();
services.AddAntiforgery(options =>
{
options.Cookie.Name = "X-CSRF-TOKEN-GOTNEXT-COOKIE";
options.HeaderName = "X-CSRF-TOKEN-GOTNEXT-HEADER";
options.SuppressXFrameOptionsHeader = false;
});
var serviceProvider = services.BuildServiceProvider();
var context = serviceProvider.GetService<GotNextDBContext>();
}
GoogleTokenValidator.cs
public class GoogleTokenValidator : ISecurityTokenValidator
{
private readonly JwtSecurityTokenHandler _tokenHandler;
private readonly IEnumerable<string> _clientIds;
public GoogleTokenValidator()
{
_tokenHandler = new JwtSecurityTokenHandler();
}
public GoogleTokenValidator(IEnumerable<string> clientIds)
{
_tokenHandler = new JwtSecurityTokenHandler();
_clientIds = clientIds;
}
public bool CanValidateToken => true;
public int MaximumTokenSizeInBytes { get; set; } = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
public bool CanReadToken(string securityToken)
{
return _tokenHandler.CanReadToken(securityToken);
}
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
validatedToken = null;
var payload = GoogleJsonWebSignature.ValidateAsync(securityToken, new GoogleJsonWebSignature.ValidationSettings() { Audience = _clientIds }).Result; // here is where I delegate to Google to validate
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, payload.Name),
new Claim(ClaimTypes.Name, payload.Name),
new Claim(JwtRegisteredClaimNames.FamilyName, payload.FamilyName),
new Claim(JwtRegisteredClaimNames.GivenName, payload.GivenName),
new Claim(JwtRegisteredClaimNames.Email, payload.Email),
new Claim(JwtRegisteredClaimNames.Sub, payload.Subject),
new Claim(JwtRegisteredClaimNames.Iss, payload.Issuer),
};
try
{
var principle = new ClaimsPrincipal();
principle.AddIdentity(new ClaimsIdentity(claims));
return principle;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
}
I am hitting the endpoint from the belowHttpClient call in Xamarin Forms.
using (var client = new HttpClient() { BaseAddress = new Uri("https://gotnext.azurewebsites.net/api/user/post/") })
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add(verificationToken.tokenName, verificationToken.token);
//client.DefaultRequestHeaders.Add("X-ZUMO-AUTH", googleUser.GoogleAuthToken);
client.DefaultRequestHeaders.Add("Bearer", googleUser.GoogleIdToken);
var json = JsonConvert.SerializeObject(newUser);
var content = new StringContent(json, Encoding.UTF8, "application/json");
response = client.PostAsync(client.BaseAddress, content).Result;
}
The ClaimsIdentity that you add to your ClaimsPrincipal does not have an AuthenticationType set so IsAuthenticated will always be false (source).
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Google"));
You can set the authentication type to any value as long as it's not empty.
Stepping throw the code locally, this line: return new ClaimsPrincipal(new
ClaimsIdentity(claims, "Google")); Is returning an authenticated identity. However it
still is returning a 401. I feel like I'm so close!!
I am making the authentification with google in my web application too(and i walked from there: enter link description here) and i spended much time to investigate these all.
In your solution just add these pieces:
1)
//at "ConfigureServices" in "Startup.cs":
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
2) How was said above that it is requied too
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Google"));
Related
I create my token in the following way
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Role, "tada")
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var encryptedtoken = tokenHandler.WriteToken(token);
And now i would like to simply get the users id from my authorize attribute and put it in the context somehow?
I know i can decode the token like so
[Authorize(Roles = "tada")]
public IActionResult Get()
{
var token = HttpContext.Request.Headers[HeaderNames.Authorization][0];
var tokenArray = token.Split(" ");
var handler = new JwtSecurityTokenHandler();
var tokenS = handler.ReadToken(tokenArray[1]) as JwtSecurityToken;
return Ok(tokenS.Payload.SingleOrDefault(t => t.Key == "unique_name").Value);
}
But how do i reuse this code in a more clever way can i create my own authorization attribute that will store it in the context if there is no way, how do i create a singleton/scoped/transient service?
here's how i configure jwt
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.AddDbContext<CatstagramDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddIdentity<User, IdentityRole>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<CatstagramDbContext>();
var applicationSettingConfiguration = Configuration.GetSection("ApplicationSettings");
services.Configure<AppSettings>(applicationSettingConfiguration);
var appSettings = applicationSettingConfiguration.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
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
};
});
services.AddControllers();
}
// 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.UseMigrationsEndPoint();
}
app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.ApplyMigrations();
}
}
I am using this function to get any token claim value
public static string GetClaimValue(HttpContext httpContext, string valueType)
{
if (string.IsNullOrEmpty(valueType)) return null;
var identity = httpContext.User.Identity as ClaimsIdentity;
var valueObj = identity == null ? null : identity.Claims.FirstOrDefault(x => x.Type == valueType);
return valueObj==null? null:valueObj.Value;
}
you can use it like this
var name = GetClaimValue(HttpContext, "unique_name");
When the authentication middleware authenticates the request, it populates HttpContext.User property with a ClaimsPrincipal that holds the claims for the current user.
ClaimsPrincipal class has an extension method in System.Security.Claims in namespace called FindFirstValue.
User.FindFirstValue("unique_name");
gives you the value for the first unique_name claim.
Source: https://github.com/dotnet/aspnetcore/blob/2be49d930a5fb53e781abd175c3b2a8f8b7827d4/src/Identity/Extensions.Core/src/PrincipalExtensions.cs
I am working on a API right now where i can send /api/login get a jwt token and use it further to do some stuff on that api with my client app later
thats my startup where i add the services :
services.AddScoped<IUserCoreController>(x => new UserCoreController(x.GetRequiredServic<TerraContext>(), x.GetRequiredService<IHashEngine>()));
services.AddScoped<ITokenFactory>(x => new TokenFactory(x.GetRequiredService<IConfiguration>(), x.GetService<JwtSettings>(), x.GetService<AppDataSettings>(),x.GetRequiredService<IUserCoreController>()));
With this method i try to access to my db _context and check if a user with that email exists and return true/false
public async Task<ResponseModel> Authenticate(string email, string password)
{
using (var db = _context)
{
//logic of my method
}
the code below shows the constructor for this class and how i inject the dbcontext and a hashengine
public class UserCoreController : IUserCoreController
{
TerraContext _context;
IHashEngine _hashEngine;
public UserCoreController(TerraContext context, IHashEngine engine)
{
_hashEngine = engine;
_context = context;
}
until now i get a true from this method after that i generate a jwt token with the class below
{
public class TokenFactory : ITokenFactory
{
IUserCoreController _userCore;
TerraContext _context;
IConfiguration _config;
JwtSettings _jwtSettings;
AppDataSettings _appDataSettings;
public TokenFactory(IConfiguration config, JwtSettings jwtsettings, AppDataSettings appDataSettings, IUserCoreController userCore)
{
_config = config;
_jwtSettings = jwtsettings;
_appDataSettings = appDataSettings;
_userCore = userCore;
}
public async Task<ResponseModel> Authenticate(LoginModel model)
{
var response = await _userCore.Authenticate(model.Email, model.Password);
return response;
}
public async Task<TokenResponse> GenerateJwtToken(string id)
{
var token = new TokenResponse();
var user = await _userCore.GetUser(id);
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>();
if (!String.IsNullOrEmpty(user.Firstname))
claims.Add(new Claim("firstname", user.Firstname));
if (!String.IsNullOrEmpty(user.Lastname))
claims.Add(new Claim("lastname", user.Lastname));
if (!String.IsNullOrEmpty(user.Username))
claims.Add(new Claim("userId", user.UserId));
if (!String.IsNullOrEmpty(user.Username))
claims.Add(new Claim("appId", _appDataSettings.ApplicationId));
if (!String.IsNullOrEmpty(user.Username))
claims.Add(new Claim("orgId", _appDataSettings.OrganisationId));
var jwt = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
expires: DateTime.Now.AddHours(3),
signingCredentials: credentials,
claims: claims
);
var tokstring = new JwtSecurityTokenHandler().WriteToken(jwt);
token.Token = tokstring;
token.Email = user.Email;
token.TokenId = Guid.NewGuid().ToString();
return token;
}
public async Task<bool> ValidateToken(string token)
{
await Task.Delay(100);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
SecurityToken validatedToken;
IPrincipal principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return true;
}
public async Task<ClaimsPrincipal> GetUser(string token)
{
await Task.Delay(100);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return principal;
}
private TokenValidationParameters GetValidationParameters()
{
return new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidAudience = _jwtSettings.Audience,
ValidIssuer = _jwtSettings.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)),
ClockSkew = TimeSpan.Zero
};
}
}
I get the Exception System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Should i just stop coding with a using() statement with my DbContext?
This application should automatically sign-in users using their Environment.Username, but I'm struggling to do it.
In the following HomeController, the variable "ThisGivesNegative" remains false even after the "HttpContext.SignInAsync" is invoked.
When I put this code in an HTTPPost action, the sign in is correct so I guess there has to be something with the configuration but after navigating in the web none of the StackOverflow posts worked.
Could any of you give me a hand? Thanks!
public class HomeController : Controller
{
private readonly IAppUserService _appUserService;
private readonly ILogger _logger;
private readonly ApplicationDbContext _context;
public HomeController(
ApplicationDbContext context,
IAppUserService appUserService,
ILoggerFactory loggerFactory
)
{
_context = context;
_appUserService = appUserService;
_logger = loggerFactory.CreateLogger<AccountController>();
}
public async Task<IActionResult> Index()
{
string WindowsUsername = Environment.UserName;
if (WindowsUsername != null)
{
List<AppRole> RolesForThisUser = new List<AppRole>();
RolesForThisUser = _context.AppUserAppRoles.Where(x => x.AppUser.ApexID == WindowsUsername).Select(x => x.AppRole).ToList();
var properties = new AuthenticationProperties
{
//AllowRefresh = false,
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1)
};
List<Claim> MyClaims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, WindowsUsername),
};
foreach (AppRole ThisRole in RolesForThisUser)
{
MyClaims.Add(new Claim(ClaimTypes.Role, ThisRole.RoleName.ToString()));
}
var identity = new ClaimsIdentity(MyClaims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(principal, properties);
bool ThisGivesNegative = HttpContext.User.Identity.IsAuthenticated;
}
return View();
}
}
Here my ConfigureServices code:
public void ConfigureServices(IServiceCollection services)
{
services
.Configure<API_IP21_CurrentValues>(ConfigAppSettings.GetSection("API_IP21_CurrentValues"))
.Configure<API_IP21_HistoricValues>(ConfigAppSettings.GetSection("API_IP21_HistoricValues"))
.Configure<API_PPM_DailyValues>(ConfigAppSettings.GetSection("API_PPM_DailyValues"))
.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
})
.AddDbContextPool<ApplicationDbContext>(options =>
{
options.UseSqlServer(ConfigAppSettings.GetSection("ConnectionStrings").GetSection("DefaultConnection").Value);
})
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Views/Home/Index.cshtml";
options.LogoutPath = "/Views/Home/Index.cshtml";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddTransient<IAppUserService, AppUserService>()
.AddTransient<IEquipmentRepository, EquipmentRepository>()
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
I found the issue, here some help for those in need!
Esentially, cookies are read before the Home Controller is fired so when the view is rendered, the program doesn't know this user has those cookies. We need to reload.
I solve this by adding the following code just before "return view()" in the Home Controller.
if (FirstPostback == false)
{
RedirectToAction("Index", "Home", new { FirstPostback = true});
}
return View(new LoginViewModel { ReturnUrl = returnUrl });
I am working on an ASP.NET Core 2.2 with ASP.Net Core Identity project.
I would like to set the authenticated User, with its UserId, globally for testing.
It this possible?
For Integration Test, you could progammly login the application, save the cookies and then attach the cookies for sub-requests.
Try to implement custom WebApplicationFactory like
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
});
base.ConfigureWebHost(builder);
}
public new HttpClient CreateClient()
{
var cookieContainer = new CookieContainer();
var uri = new Uri("https://localhost:44344/Identity/Account/Login");
var httpClientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
HttpClient httpClient = new HttpClient(httpClientHandler);
var verificationToken = GetVerificationToken(httpClient, "https://localhost:44344/Identity/Account/Login");
var contentToSend = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("Email", "test#outlook.com"),
new KeyValuePair<string, string>("Password", "1qaz#WSX"),
new KeyValuePair<string, string>("__RequestVerificationToken", verificationToken),
});
var response = httpClient.PostAsync("https://localhost:44344/Identity/Account/Login", contentToSend).Result;
var cookies = cookieContainer.GetCookies(new Uri("https://localhost:44344/Identity/Account/Login"));
cookieContainer.Add(cookies);
var client = new HttpClient(httpClientHandler);
return client;
}
private string GetVerificationToken(HttpClient client, string url)
{
HttpResponseMessage response = client.GetAsync(url).Result;
var verificationToken =response.Content.ReadAsStringAsync().Result;
if (verificationToken != null && verificationToken.Length > 0)
{
verificationToken = verificationToken.Substring(verificationToken.IndexOf("__RequestVerificationToken"));
verificationToken = verificationToken.Substring(verificationToken.IndexOf("value=\"") + 7);
verificationToken = verificationToken.Substring(0, verificationToken.IndexOf("\""));
}
return verificationToken;
}
}
And then
public class IntegrationTestWithIdentityTest : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public IntegrationTestWithIdentityTest(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task IndexRendersCorrectTitle()
{
var response = await _client.GetAsync("https://localhost:44344/About");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Send Email", responseString);
}
}
Source Code: IntegrationTestWithIdentityTest.
If you want to mock a user which is not exist in the Identity Table, you need to define a new endpoint which will sign the user with
public async Task<IActionResult> Login()
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "edward"));
identity.AddClaim(new Claim(ClaimTypes.Name, "edward zhou"));
//add your own claims from jwt token
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = true });
return View();
}
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.