Jason
Jason

Reputation: 2617

Authorize attribute not working with IdentityServer4 and .NET Core 3.1

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);
            }
        }
    }
}

Upvotes: 0

Views: 2678

Answers (2)

Beyers
Beyers

Reputation: 9108

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.

Upvotes: 2

Tore Nestenius
Tore Nestenius

Reputation: 19901

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.

Upvotes: 2

Related Questions