Search code examples
c#asp.nethangfire

ASP.NET Core 2.1, Angular 5: Hangfire Auth only works on login


I have an ASP.NET Core 2.1 Web API that uses an Angular 5 front-end. When I call the Login() method, it saves a Cookie to the session.

Response.Cookies.Append("UserRole", userFromRepo.UserRole.ToString());

Then when accessing the Hangfire dashboard, I check for that cookie, and authorize if the user is an Admin (See Startup.cs below).

However, since I am using JWT tokens, and the front-end token doesn't expire right away, if the user closes the browser and re-opens my site, they don't have to log in, but it is a new session, so the cookie no longer exists.

Is there a more persistent way to store the user's role? (no, I'm not using Identity) Or is there a way to re-instantiate the cookie at the startup of the new session?


AuthController.cs

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] UserForLoginDto userForLoginDto)
    {
        var userFromRepo = await _repo.Login(userForLoginDto.Username.ToLower(), userForLoginDto.Password);

        //stuff to generate tokenString

        Response.Cookies.Append("UserRole", userFromRepo.UserRole.ToString());

        return Ok(new { tokenString, user });
    }


Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHangfire(config =>
            config.UseSqlServerStorage(Configuration.GetConnectionString("DefaultConnection")));
        services.AddMvc()
            .AddJsonOptions(opt =>
            {
                opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
            })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddScoped<IAuthRepository, AuthRepository>();

        services.AddScoped<IBaseRepository, BaseRepository>();

        services.AddSingleton(Configuration);

        var key = Encoding.ASCII.GetBytes(Configuration.GetSection("AppSettings:Token").Value);

        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });

        services.AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "ClientApp/dist";
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseAuthentication();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler(builder =>
            {
                builder.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

                    var error = context.Features.Get<IExceptionHandlerFeature>();
                    if (error != null)
                    {
                        context.Response.AddApplicationError(error.Error.Message);
                        await context.Response.WriteAsync(error.Error.Message);
                    }
                });
            });
        }

        app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials());

        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();
        app.UseHangfireDashboard("/hangfire", new DashboardOptions()
        {
            Authorization = new[] {new HangfireAuthorizationFilter()}
        });

        app.UseHangfireServer();

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

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller}/{action=Index}/{id?}");
        });

        app.UseSpa(spa =>
        {
            spa.Options.SourcePath = "ClientApp";

            if (env.IsDevelopment())
            {
                spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
            }
        });
    }
}

public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize([NotNull] DashboardContext context)
    {
        try
        {
            var httpContext = context.GetHttpContext();

            var userRole = httpContext.Request.Cookies["UserRole"];
            return userRole == UserRole.Admin.ToString();
        }
        catch
        {
            return false;
        }
    }
}


AuthController.cs


Solution

  • Ok, a couple months later, I came back to this... here's how I got it to work!

  • Connect to the DB, get the current user via ID, and add an identity claim within the authentication events section that contains the user's role, and use AddCookie() to save the value within request's cookies (within ConfigureServices)

  • Create an HangfireAuthorizationFilter that searches for the UserRole cookie and compares it to my UserRoles enum.

  • Use my custom AuthorizationFilter when starting Hangfire (within Configure)
  • Startup.cs

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
    
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<DataContext>(x => x
                .UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                    b =>
                    {
                        b.MigrationsAssembly(("MyApp"));
                        b.EnableRetryOnFailure();
                    })
                .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning)));
    
            services.AddMvc()
                .AddJsonOptions(opt =>
                {
                    opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Events = new JwtBearerEvents
                    {
                        OnTokenValidated = async ctx =>
                        {
                            var clientId = ctx.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
                            var db = ctx.HttpContext.RequestServices.GetRequiredService<DataContext>();
                            var user = await db.Users.FirstOrDefaultAsync(u => u.Id == int.Parse(clientId));
    
                            if (user != null)
                            {
                                var userRole = user.UserRole;
                                var claims = new List<Claim>
                                {
                                    new Claim(ClaimTypes.Role, userRole.ToString())
                                };
                                var appIdentity = new ClaimsIdentity(claims);
                                ctx.Principal.AddIdentity(appIdentity);
                            }
                        }
                    };
    
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(key),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                }).AddCookie();
    
            //...
        }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            app.UseAuthentication();
    
            app.UseHangfireDashboard("/hangfire", new DashboardOptions()
            {
                Authorization = new[] { new HangfireAuthorizationFilter() }
            });
            app.UseHangfireServer();
    
            //...
        }
    }
    
    public class HangfireAuthorizationFilter : ControllerBase, IDashboardAuthorizationFilter
    {
        public bool Authorize([NotNull] DashboardContext context)
        {
            try
            {
    
                var httpContext = context.GetHttpContext();
                var userRole = httpContext.Request.Cookies["UserRole"];
                return userRole == UserRole.Admin.ToString();
            }
            catch
            {
                return false;
            }
        }
    }