Search code examples
c#vue.jsasp.net-core-webapiasp-net-core-spa-services

ASP.Net Core 2.1 Vue.JS SPA - Cookie Auth and the Vue.JS Dev Server


I have an application which is essentially a Vue.JS SPA sat within a dotnet core 2.1 app, providing API services. When I start up the app, with my current Startup.cs configuration, it starts up 3 windows. 1 of these windows is the actual ASP.Net Core app root - the other 2 (don't ask me why 2) are copies of the Vue.js dev server that you get when you do npm run serve.

The problem that I have is that, whilst authentication is working fine if I use the instance running in the first window, if I try to log in using the Vue.JS server windows then I just get a 401 back.

My first thought was CORS, so I set up a CORS policy purely for when running in development mode but this hasn't rectified the issue. Developing without the Vue.JS server isn't really viable as without it I have no HMR and will need to reload the entire application state every time I make a design change.

I have had this setup working before with a .net Framework 4.6 API back end with no problem at all so I can only think it's a modern security enhancement that's blocking the proxying which I need to configure to be off in Development or something.

Any suggestions on how I can solve this are welcome.

Here's my current Startup.cs Configure...

       public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
        ApplicationDbContext context, RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
    {
        if (env.IsDevelopment())
        {
            app
                .UseDeveloperExceptionPage()
                .UseDatabaseErrorPage()
                .UseCors("DevelopmentPolicy");
        }
        else
        {
            app
                .UseExceptionHandler("/Home/Error")
                .UseHsts();
        }

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

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

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

            if (env.IsDevelopment())
            {
                spa.UseVueCliServer("serve");
            }
        });

        DbInitializer.Initialize(context, roleManager, userManager, env, loggerFactory);
    }

...and ConfigureServices...

   public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddLogging(builder => builder
                .AddConsole()
                .AddDebug());

        services
            .AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
                .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());

        services
            .AddCors(options =>
            {
                options.AddPolicy("DevelopmentPolicy",
                    policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
            });

        // In production, the Vue files will be served from this directory
        services.AddSpaStaticFiles(configuration => { configuration.RootPath = "wwwroot"; });
        services.AddAuthentication();

        services
            .ConfigureEntityFramework(Configuration)
            .ConfigureEntityServices()
            .ConfigureIdentityDependencies()
            .ConfigureDomainServices()
            .ConfigureApplicationCookie(config =>
            {
                config.SlidingExpiration = true;
                config.Events = new CookieAuthenticationEvents
                {
                    OnRedirectToLogin = cxt =>
                    {
                        cxt.Response.StatusCode = 401;
                        return Task.CompletedTask;
                    },
                    OnRedirectToAccessDenied = cxt =>
                    {
                        cxt.Response.StatusCode = 403;
                        return Task.CompletedTask;
                    },
                    OnRedirectToLogout = cxt => Task.CompletedTask
                };
            });
    }

...and Identity gets lumped in with EF configuration here...

       public static IServiceCollection ConfigureEntityFramework(this IServiceCollection services, string connectionString)
    {
        services
            .AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString))
            .AddIdentity<ApplicationUser, IdentityRole>(options =>
                {

                    // Password settings
                    options.Password.RequireDigit = true;
                    options.Password.RequiredLength = 8;
                    options.Password.RequireNonAlphanumeric = true;

                    // Lockout settings
                    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
                    options.Lockout.MaxFailedAccessAttempts = 10;

                    // User settings
                    options.User.RequireUniqueEmail = true;
                })
            .AddRoleManager<RoleManager<IdentityRole>>()
            .AddSignInManager<SignInManager<ApplicationUser>>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        return services;
    }

My vue.config.js looks like this...

const baseUrl = ''

module.exports = {
    publicPath: baseUrl + '/',

    // place our built files into the parent project where they will be copied
    // into the distribution for deployment
    outputDir: '../wwwroot',

    filenameHashing: true, //process.env.NODE_ENV === 'production',
    lintOnSave: 'error',

    css: {
        modules: false,
        sourceMap: process.env.NODE_ENV !== 'production',
        loaderOptions: {
            sass: {
                data: `
                    $fa-font-path: ${process.env.NODE_ENV !== 'production' ? '"~/fonts"' : '"' + baseUrl + '/fonts"'};
                    @import "@/scss/base/index.scss";
                    @import "@/scss/helpers/index.scss";
                `
            }
        }
    },

    devServer: {
        host: 'localhost',
        port: 8080,
        hot: true,
        open: true,
        openPage: '',
        overlay: true,
        disableHostCheck: true,
        proxy: {
            // Proxy services to a backend API
            '/api': {
                target: process.env.PROXY || 'https://localhost:44368',
                secure: false,
                changeOrigin: true
            }
        }
    },

    // these node_modules use es6 so need transpiling for IE
    transpileDependencies: [
    ]
}

Solution

  • I solved this in the end by configuring the app so that it only forces HTTPS when not in development mode. HTTPS was killing the underlying Webpack Development Server and therefore Hot Module replacement - I can now debug successfully in the main window!

    Happy to up vote better solutions than that though to get it working without killing SSL (if possible).