Search code examples
c#asp.net.netjwtasp.net-identity

JWT Bearer Token not working with ASP.Net Core 3.1 + Identity Server 4


I'm currently trying to create a web api for an mobile app im developing. I decided on using ASP.Net Core 3.1 with the included Indentity Server to use OpenID Connect + PKCE for Authentication. The User will authorize the app via WebView and the access token will be used for subsequent calls to the api. The Controllers of the api are decorated with the [Authorize] Attribute.

I tried to use some of the scaffolded code for authentication but right now I struggling since the jwt token doesn't seem to get recognized under some conditions.

In the scaffolded code, the ConfigureServices method in the Startup Class called services.AddDefaultIdentity<ApplicationUser>(). The problem with that was, that this also added all of the default endpoints for Identity which I didnt need/want (/Account/Manage/Disable2fa, /Account/Manage/PersonalData etc.). Therefore I changed to code to services.AddIdentity<ApplicationUser, IdentityRole>(), because I read multiple times nowthat AddDefaultIdentity() apparently does the same as AddIdentity() but also calls .AddDefaultUI()

After testing the login flow, it turns out that the api controllers no longer accept the Authorization: Bearer ... Header after making the change and api calls always get redirected to the login page as if the user is not logged in

When I change the code back to AddDefaultIdentity, the JWT Bearer Token is correctly authenticated again and I can access the controller with the [Authorize] Attribute but then i got the problem again with the default Identity Pages ...

My Question is: How do I secure my API Controllers with the [Authorize] Arribute and JWT Bearer Header without having to include the default Identity UI?

Startup.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Munchify.Web.Data;
using Munchify.Web.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using APIBackend.Web.Services;

namespace APIBackend.Web
{
    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<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.ConfigureApplicationCookie(options =>
            {
                options.LoginPath = "/Identity/Account/Login";
            });

            services.AddIdentityServer()
                .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

            services.AddAuthentication()
                .AddIdentityServerJwt();

            services.AddTransient<IEmailSender, EmailSender>();

            services.AddControllersWithViews();
            services.AddRazorPages();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseIdentityServer();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });
        }
    }
}

Solution

  • I managed to fix it 🥳

    I basically just replaced

    services.AddIdentity<ApplicationUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    

    with

    services.AddAuthentication(o =>
    {
        o.DefaultScheme = IdentityConstants.ApplicationScheme;
        o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies(o => { });
    
    var identityService = services.AddIdentityCore<ApplicationUser>(o =>
    {
        o.Stores.MaxLengthForKeys = 128;
        o.SignIn.RequireConfirmedAccount = true;
    })
        .AddDefaultTokenProviders()
        .AddEntityFrameworkStores<ApplicationDbContext>();
    
    identityService.AddSignInManager();
    identityService.Services.TryAddTransient<IEmailSender, EmailSender>();
    

    because that's what AddDefaultIdentity() does behind the scenes without the UI part.

    Now my JWT Header Authentication works again

    Hope this helps someone with the same issue :)