0

My original configuration was just cookie authentication, and the redirecting for not logged in, or not authorized worked. I then added jwt token auth to use with the api side of our application, and that works, but the redirecting has stopped working. I tried adding the schema to the authorize attrib, but it didn't help.  [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]

I tried handling the Events.OnRedirectToAccessDenied event and it does not fire when I have added the jwt support (AddJwtBearer and adding the schema to the default policy).

    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.AddCors(policy =>
            {
                policy.AddPolicy("OpenCorsPolicy", opt =>
                    opt.AllowAnyOrigin()
                    .AllowAnyHeader()
                    .AllowAnyMethod());
            });

            services.AddTransient<IUserManager, UserManager>();

            var tokenValidationParams = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("JWTSecret"))),
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                RequireSignedTokens = true,

                ClockSkew = TimeSpan.FromMinutes(1)
            };

            // adding the token settings so that hey are DI-able
            services.AddSingleton<TokenValidationParameters>(tokenValidationParams);

            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.Strict;
            });

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, x =>
                {
                    x.LoginPath = new PathString("/Account/Login");
                    x.ExpireTimeSpan = TimeSpan.FromMinutes(Configuration.GetValue<int>("CookieExpiry"));
                    x.AccessDeniedPath = new PathString("/Account/Login");
                    x.SlidingExpiration = true;

                    //x.Events.OnRedirectToAccessDenied = ReplaceRedirector(HttpStatusCode.Forbidden, x.Events.OnRedirectToAccessDenied);
                    //x.Events.OnRedirectToLogin = ReplaceRedirector(HttpStatusCode.Unauthorized, x.Events.OnRedirectToLogin);
                })
                .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x =>
                {
                    x.RequireHttpsMetadata = false;
                    x.SaveToken = true;
                    x.TokenValidationParameters = tokenValidationParams;
                })
                ;

            static Func<RedirectContext<CookieAuthenticationOptions>, Task> ReplaceRedirector(HttpStatusCode statusCode, Func<RedirectContext<CookieAuthenticationOptions>, Task> existingRedirector) =>
                context => {
                    context.Response.Redirect(context.RedirectUri + "&Extra=Foo");
                    System.Diagnostics.Debug.WriteLine(statusCode);
                    return Task.CompletedTask;
                //return existingRedirector(context);
            };

            services.AddAuthorization(options =>
            {
                // setting up that the default authorize can be either cookie or token
                var defaultAuthorizationPolicyBuilder =
                    new AuthorizationPolicyBuilder(
                        CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme);
                defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
                options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
            });

            services.AddMvc(); //.AddJsonOptions(jsonOptions => jsonOptions.JsonSerializerOptions.PropertyNamingPolicy = null);

            services.AddRazorPages(options =>
            {
                // you can set the authentication at the folder level
                options.Conventions.AuthorizeFolder("/");
                // you can set authenication for a single page
                options.Conventions.AllowAnonymousToPage("/Error");
                // you can set in the razor page     [AllowAnonymous] like on index

                /*
                    options.Conventions.AuthorizePage("/Contact");
                    options.Conventions.AuthorizeFolder("/Private");
                    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
                    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
                 */
            });
        }

        // 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();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
            });
       
        }
    }
ToddK
  • 765
  • 9
  • 16

1 Answers1

0

I found this SO answer and adapted it. It basically checks the results of the UseAuthentication and if the status is auth related, fires the challenge and forbid handlers of the cookie scheme if it is not an api call. I'm checking for the existence of the bearer token because api calls may not have it at all, vs being expired or unauthorized. As the original answer says, this must be added to the chain after Authentication is determined.

app.UseAuthentication();

app.Use(async (context, next) =>
{
    await next();
    if (context.Response.StatusCode == 401 || context.Response.StatusCode == 403)
    {
        //var bearerAuth = context.Request.Headers["Authorization"]
        //.FirstOrDefault()?.StartsWith("Bearer ") ?? false;
        var isAPI = context.Request.Path.Value.Contains("/api/", StringComparison.OrdinalIgnoreCase);
        if (!isAPI)
        {
            if (context.Response.StatusCode == 401)
            {
                await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            }
            else if (context.Response.StatusCode == 403)
            {
                await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            }
        }
    }
});

ToddK
  • 765
  • 9
  • 16