I have a .NET Core 3.1 project using Identity and IdentityServer4 to implement the Resource Owner Password grant type. I can get the tokens no problem but the [Authorize] attribute isn't working, it just lets everything through. An important note is that my API and Identity server are in the same project. From comments online it seems like it might be a middleware order issue but I can't seem to find a combination that works. I've double checked that when no Authorization header is attached, the endpoint code is still hit.
Here's my Startup.cs file:
using System;
using System.Collections.Generic;
using IdentityServer4.Models;
using LaunchpadSept2020.App;
using LaunchpadSept2020.App.Repositories;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.App.Seeds;
using LaunchpadSept2020.Models.Entities;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace LaunchpadSept2020.Api
{
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)
{
// Set up the database
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
b =>
{
b.MigrationsAssembly("LaunchpadSept2020.App");
})
);
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = true;
});
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.ApiName = "launchpadapi";
options.Authority = "http://localhost:25000";
options.RequireHttpsMetadata = false;
});
services.AddIdentityServer()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
npgSqlOptions =>
{
npgSqlOptions.MigrationsAssembly("LaunchpadSept2020.App");
});
})
.AddInMemoryClients(Clients.Get())
.AddAspNetIdentity<User>()
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryApiResources(Resources.GetApiResources())
.AddInMemoryApiScopes(Resources.GetApiScopes())
.AddDeveloperSigningCredential();
services.AddControllers();
// Add Repositories to dependency injection
services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<IUserRepository, UserRepository>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserManager<User> userManager, RoleManager<IdentityRole> roleManager)
{
// Initialize the database
UpdateDatabase(app);
// Seed data
UserAndRoleSeeder.SeedUsersAndRoles(roleManager, userManager);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//app.UseHttpsRedirection();
app.UseRouting();
app.UseIdentityServer(); // Includes UseAuthentication
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
// Update the database to the latest migrations
private static void UpdateDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices
.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
using (var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>())
{
context.Database.Migrate();
}
}
}
}
internal class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "mobile",
ClientName = "Mobile Client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = { new Secret("MySecret".Sha256()) },
AllowedScopes = new List<String> { "launchpadapi.read" }
//AllowAccessTokensViaBrowser = true,
//RedirectUris = { "http://localhost:25000/signin-oidc" },
//PostLogoutRedirectUris = { "http://localhost:25000/signout-callback-oidc" },
//AllowOfflineAccess = true
}
};
}
}
internal class Resources
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource
{
Name = "launchpadapi",
DisplayName = "Launchpad API",
Description = "Allow the application to access the Launchpad API on your behalf",
Scopes = new List<string> { "launchpadapi.read", "launchpadapi.write"},
ApiSecrets = new List<Secret> {new Secret("ScopeSecret".Sha256())},
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new[]
{
new ApiScope("launchpadapi.read", "Read Access to Launchpad API"),
new ApiScope("launchpadapi.write", "Write Access to Launchpad API")
};
}
}
}
And my controller:
using System.Collections.Generic;
using System.Threading.Tasks;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.Models.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LaunchpadSept2020.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CompanyController : ControllerBase
{
private readonly ICompanyRepository _companyRepository;
public CompanyController(ICompanyRepository companyRepository)
{
_companyRepository = companyRepository;
}
[HttpPost]
[Authorize]
public async Task<ActionResult<CompanyVM>> Create([FromBody] CompanyCreateVM data)
{
// Make sure model has all required fields
if (!ModelState.IsValid)
return BadRequest("Invalid data");
try
{
var result = await _companyRepository.Create(data);
return Ok(result);
}
catch
{
return StatusCode(500);
}
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<CompanyVM>>> GetAll()
{
try
{
var results = await _companyRepository.GetAll();
return Ok(results);
}
catch
{
return StatusCode(500);
}
}
}
}
services.AddAuthentication("Bearer")service, or try to addapp.UseAuthentication();to Configure in Startup. Reference: Using ASP.NET Core Identity with IdentityServer4.