Hello,
I built a sample “to-do” proof-of-concept app which includes …
an aspnet core WEB API backend,
an angluarjs2 client
and an iphone client. It’s all working just fine, which is basically CRUD operations.
Now, I started to implement security. And that is where I am a bit confused and don’t seem to get the right answers.
I started by adding aspnet core identity to my web api. But it didn’t seem to work in this scenario. I would log in from a client but any subsequent requests were all denied with 401. I thought it might have to do with the fact that the web api is on a complete different domain? Is that the same reason why "Individual Accounts" as authentication method is disabled when creating a new asp.net core WEB API project?
I then went ahead and added jwt. Implemented all necessary code and now it when I log in, check username and password against asp.net identity, and response with a jwt token, any subsequent request from a client using that token are authenticated and authorized perfectly. So the apps works as expected, but I don’t feel I am doing it right.
I am using “app.UseIdentity()” and“app.UseJwtBearerAuthentication”. It doesn’t seem right. When I decorate a controller with [Authorize] what/who decides to use the identity or jwt authentication?
So, my question really boils down to this. Having an asp.net core WEB API and having various different clients apps (web and/or native and so on...) what is the best practice to secure my WEB API endpoints? Keep in mind that I would like users to register and that I would like admins to be able to view a list of registered users.
Below are my startup.cs and my login logic
public class Startup { private SymmetricSecurityKey _signingKey = null; public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true) .AddEnvironmentVariables(); this.Configuration = builder.Build(); } private IConfigurationRoot Configuration { get; } public void ConfigureServices(IServiceCollection services) { var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY") ?? "MyTotalSecretAppKey"; var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; var issuer = Environment.GetEnvironmentVariable("ISSUER") ?? "InxTokenService"; var audience = Environment.GetEnvironmentVariable("AUDIENCE") ?? "http://localhost:5424"; var dbConnection = Environment.GetEnvironmentVariable("DB_CONNECTION") ?? "Server=(local)\\sqlexpress;Database=INX3;Trusted_Connection=True;"; Console.WriteLine($"secret key {secretKey}"); Console.WriteLine($"environment name {environmentName}"); Console.WriteLine($"issuer {issuer}"); Console.WriteLine($"audience {audience}"); Console.WriteLine($"dbConnection {dbConnection}"); this._signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); services.AddOptions(); // Make authentication compulsory across the board (i.e. shut // down EVERYTHING unless explicitly opened up). services.AddMvc(config => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); }); // Use policy auth. services.AddAuthorization(options => { options.AddPolicy("AppUser", policy => { policy.RequireAuthenticatedUser(); policy.RequireAssertion(ctx => ctx.User.HasClaim("Role", "Admin") || ctx.User.HasClaim("Role", "User")); }); options.AddPolicy("AppAdmin", policy => { policy.RequireAuthenticatedUser(); policy.RequireAssertion(ctx => ctx.User.HasClaim("Role", "Admin")); }); }); // Configure JwtIssuerOptions services.Configure<JwtIssuerOptions>(options => { options.Issuer = issuer; options.Audience = audience; options.SigningCredentials = new SigningCredentials(this._signingKey, SecurityAlgorithms.HmacSha256); }); services.AddDbContext<AppDbContext>(options => options.UseSqlServer(dbConnection)); services.AddDbContext<SecurityDbContext>(options => options.UseSqlServer(dbConnection)); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<SecurityDbContext>() .AddDefaultTokenProviders(); services.Configure<IdentityOptions>(options => { options.Password.RequireDigit = true; options.Password.RequiredLength = 8; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = true; options.Password.RequireLowercase = false; options.User.RequireUniqueEmail = true; }); var corsBuilder = new CorsPolicyBuilder(); corsBuilder.AllowAnyHeader(); corsBuilder.AllowAnyMethod(); corsBuilder.AllowAnyOrigin(); corsBuilder.AllowCredentials(); services.AddCors(options => { options.AddPolicy("AllowAll", corsBuilder.Build()); }); services.AddMvc(); services.AddSingleton<IAppDbContext, AppDbContext>(); services.AddSingleton<ITransactionRepo, TransactionRepo>(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, AppDbContext appDbContext, SecurityDbContext securityDbContext, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager) { if (env.IsDevelopment()) { loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); loggerFactory.AddDebug(); AppDbInitializer.Initialize(appDbContext); var secInit = new DbInitializer(securityDbContext, userManager, roleManager); secInit.Initialize(); app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } Console.WriteLine($"Environment.GetEnvironmentVariable('ISSUER') {Environment.GetEnvironmentVariable("ISSUER")}"); Console.WriteLine($"env.ApplicationName {env.ApplicationName}"); var issuer = Environment.GetEnvironmentVariable("ISSUER") ?? "InxTokenService"; var audience = Environment.GetEnvironmentVariable("AUDIENCE") ?? "http://localhost:5424"; Console.WriteLine($"issuer {issuer}"); Console.WriteLine($"audience {audience}"); app.UseIdentity(); var tokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, ValidAudience = audience, ValidateIssuerSigningKey = true, IssuerSigningKey = this._signingKey, RequireExpirationTime = true, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, TokenValidationParameters = tokenValidationParameters }); app.UseStaticFiles(); app.UseCors("AllowAll"); app.UseMvc(); } }
[HttpPost] [Route("login")] public async Task<IActionResult> Login([FromBody] LoginViewModel model) { Console.WriteLine("Login Request"); this._logger.LogInformation("Login Request"); if (!this.ModelState.IsValid) { this._logger.LogError("Invalid model state"); return this.BadRequest("Invalid model state"); } try { //authenticate user in userstore var result = await this._signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { var currentUser = await this._userManager.FindByNameAsync(model.UserName); var isAdmin = await this._userManager.IsInRoleAsync(currentUser, "Admin"); var jwtToken = await JwtFactory.GetJwtToken(model.UserName, isAdmin, this._jwtOptions); var response = new { access_token = jwtToken, expires_in = (int)_jwtOptions.ValidFor.TotalSeconds, current_user = new { currentUser.FirstName, currentUser.LastName, currentUser.Email } }; var json = JsonConvert.SerializeObject(response, _serializerSettings); return new OkObjectResult(json); } } catch (Exception ex) { Console.WriteLine(ex.Message); this._logger.LogError(ex.Message); return BadRequest(ex.Message); } return Ok(); }
public static async Task<string> GetJwtToken(string userName, bool isAdmin, JwtIssuerOptions jwtOptions) { var identity = new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[] { new Claim("Role", isAdmin ? "Admin" : "User") }); 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("Role") }; var jwt = new JwtSecurityToken( jwtOptions.Issuer, jwtOptions.Audience, claims, jwtOptions.NotBefore, jwtOptions.Expiration, jwtOptions.SigningCredentials); return new JwtSecurityTokenHandler().WriteToken(jwt); }