8

I'm trying to implement authentication and access control with IdentityServer4 on an ASP.NET MVC Core app (.NetCore 2). While it's not the first time I implement a backend, it's the first time with .net, and I'm struggling with some things.

I've followed the instructions at https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html as well as the page before that.

I have also added the sample IdentityController as they show:

using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace leafserver.Controllers
{
    [Route("/api/identity")]
    [Authorize]
    public class IdentityController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
        }
    }
}

There are a few differences between my implementation and their example. As far as I can see:

  • I'm serving on my local network address (192.168.1.x) instead of localhost
  • They're using a "Web Application", where I'm using a "Web Api"
  • They seem to use ControllerBase instead of Controller as a superclass
  • I'm not sure whether there's a difference between the ASP.NET MVC they use and the one I use (I'm using core, they don't seem to, but normally it should still work...)

What I noticed is the following:

  • as long as I don't put a [Authorize], all is well. I get a 200 OK with the expected result
  • when the [Authorize] annotation is there, but I use no authentication bearer token, I am redirected to the login page (which doesn't work since this is a web api, but that's a problem for later)
  • when the [Authorize] annotation is there, and I use (what I think is) a correct authentication token, I get a 404 response.

I was expecting to have a 401 response instead. Why would my routing not work because I'm using an authentication token?

Also, I'm not getting any log from the server, which doesn't help...

aspyct
  • 3,625
  • 7
  • 36
  • 61
  • The redirect is caused by the web app detecting no authentication credentials being present in the request. The web app is attempting to negotiate an authentication scheme which is why you end up with the default HTTP Basic authentication with a form to post. Getting a 404 implies the web app has accepted the Bearer token for authentication. To troubleshoot with more detail, [look at this answer to see how to intercept the Authorize logic](https://stackoverflow.com/questions/9503500/show-404-error-page-after-authorize-failure?answertab=votes#tab-top) – Sixto Saez Feb 27 '18 at 13:52
  • 1
    For what it's worth, in Core, there's no difference between a "web application" and a "web api". It's all just controller actions returning `IActionResult`s. Some may return HTML or some may return JSON/XML/etc. – Chris Pratt Feb 27 '18 at 15:42
  • Good to know. There are quite a few differences, like `services.AddMvc()` vs `.AddMvcCore()`, or the difference between `Controller` and `ControllerBase` (the latter doesn't seem to be used in Core...?) that I wish were more clear. I guess that could be another question. – aspyct Feb 27 '18 at 15:52

3 Answers3

8

For me the answer was setting the Authorize attribute on my controller like this

[Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)]

which is outlined in the docs that leastprivilege has pointed to. https://identityserver4.readthedocs.io/en/release/topics/add_apis.html

If I just had [Authorize] then it would produce 404's

Ryan Dobbs
  • 381
  • 2
  • 11
4

Alright, I've found the problem.

In my Startup.ConfigureServices, I modified the order in which I add the services.

    // https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html
    services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddTestUsers(Config.GetTestUsers()); // TODO Remove for PROD

    // This MUST stay below the AddIdentityServer, otherwise [Authorize] will cause 404s
    services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(o =>
            {
                o.Authority = "http://localhost:5000";
                o.RequireHttpsMetadata = false; // TODO Remove for PROD
                o.ApiName = "leaf_api";
            });

If you add authentication before the identity server, then you'll get the 404s. In this order, it works just fine.

Here's the full Startup.cs file for reference:

using leafserver.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace leaf_server
{
    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<LeafContext>(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));

            services.AddMvcCore()
                    .AddAuthorization()
                    .AddJsonFormatters();

            // https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html
            services.AddIdentityServer()
                    .AddDeveloperSigningCredential()
                    .AddInMemoryApiResources(Config.GetApiResources())
                    .AddInMemoryClients(Config.GetClients())
                    .AddTestUsers(Config.GetTestUsers()); // TODO Remove for PROD

            // This MUST stay below the AddIdentityServer, otherwise [Authorize] will cause 404s
            services.AddAuthentication("Bearer")
                    .AddIdentityServerAuthentication(o =>
                    {
                        o.Authority = "http://localhost:5000";
                        o.RequireHttpsMetadata = false; // TODO Remove for PROD
                        o.ApiName = "leaf_api";
                    });

            // https://dotnetcoretutorials.com/2017/01/17/api-versioning-asp-net-core/
            services.AddApiVersioning(o =>
            {
                o.ReportApiVersions = true;
                o.AssumeDefaultVersionWhenUnspecified = true;
                o.DefaultApiVersion = new ApiVersion(1, 0);
                o.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseStatusCodePages();
            }

            app.UseIdentityServer();
            app.UseAuthentication();

            app.UseMvc();
        }
    }
}
aspyct
  • 3,625
  • 7
  • 36
  • 61
  • You typically don't mix IdentityServer and the APIs in the same application - if you do, this is how it's done correctly: https://identityserver4.readthedocs.io/en/release/topics/add_apis.html – leastprivilege Feb 28 '18 at 12:17
  • Good to know! I guess that makes sense for security. I'll make sure to separate both apps when deploying to prod. We're just fiddling around right now. – aspyct Feb 28 '18 at 14:46
  • The weird thing with your statement is that the order of adding services in `ConfigureServices` is not supposed to matter. Only the order of middleware usages, which is done in `Configure`, is supposed to have importance. Are you sure that your diagnostics were correct? – Konrad Viltersten Jul 13 '21 at 18:11
0

for me it was builder.Services.AddAuthentication().AddIdentityServerAuthentication();

to get everything to work again.

[Authorize(Roles = "Admin")] threw a 404 before, now it works.

SSchmid
  • 431
  • 4
  • 3