0

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);
            }
        }
    }
}
1
  • According to your code, it seems that you are using two kind of Authentication method to validate the user, right? Try to remove the services.AddAuthentication("Bearer") service, or try to add app.UseAuthentication(); to Configure in Startup. Reference: Using ASP.NET Core Identity with IdentityServer4. Commented Oct 19, 2020 at 2:22

2 Answers 2

2

I think a general issue is that you mix IdentityServer in the same app as ASP.NET Identity, in general my experience is that it gets hard to know who is doing what and its hard to fully understand. I always recommend putting IdentityServer and the API in independent services. Just to get a clean separation of concerns.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the input. I ended up separating IdentityServer out like you suggested and got it working that way.
2

For local API authentication you need the following additional configuration in Startup:

public void ConfigureServices(IServiceCollection services)
{
  ....
  // After services.AddIdentityServer()
  services.AddLocalApiAuthentication();
}

For reference see the docs.

And then you need to specificy the local API policy as part of the Authorize attribute on your API:

[Authorize(LocalApi.PolicyName)]

See a local API example.

2 Comments

That's not quite what I was going for (extending the IdentityServer endpoints) but it's an interesting option. I've decided to separate out IdentityServer into it's own project for now. Thanks!
I don't think the posted config is only to extend IS endpoints, my understanding is if you host your own API on the same project as IS then you need to do it as above. Either way I think having IS in a separate project is probably better.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.