diff --git a/FileSystemWeb/ClientApp/src/Helpers/API.js b/FileSystemWeb/ClientApp/src/Helpers/API.js index 3a89366..f72ff13 100644 --- a/FileSystemWeb/ClientApp/src/Helpers/API.js +++ b/FileSystemWeb/ClientApp/src/Helpers/API.js @@ -1,16 +1,32 @@ import formatUrl from './formatUrl'; export default class API { + static antiforgaryToken = null; static config = null; - static fetch(resource, { path, query, method, body, headers, ...options } = {}) { + static async fetch(resource, { path, query, method = 'GET', body, headers, ...options } = {}) { + let antiforgaryHeaders = null; + if (method !== 'GET') { + if (!API.antiforgaryToken || true) { + const antiforgary = await API.getAntiforgary(); + if (!antiforgary.ok) { + return antiforgary; + } + API.antiforgaryToken = await antiforgary.text(); + } + antiforgaryHeaders = { + 'X-CSRF-TOKEN-HEADERNAME': API.antiforgaryToken, + }; + } + const isFormData = body instanceof FormData; - const contentType = (!isFormData && method && method !== 'GET' && body) ? 'application/json;charset=utf-8' : undefined; + const contentType = (!isFormData && method !== 'GET' && body) ? 'application/json;charset=utf-8' : undefined; return window.fetch(`/api${formatUrl({ resource, path, query })}`, { credentials: 'include', method, headers: { ...(contentType ? { 'Content-Type': contentType } : null), + ...antiforgaryHeaders, ...headers, }, body: body && !isFormData ? JSON.stringify(body) : body, @@ -18,20 +34,36 @@ export default class API { }); } - static login(username, password, keepLoggedIn) { - return this.fetch('/auth/login', { - method: 'POST', - body: { - Username: username, - Password: password, - KeepLoggedIn: keepLoggedIn, - }, - }); + static async login(username, password, keepLoggedIn) { + try { + API.antiforgaryToken = null; + return await this.fetch('/auth/login', { + method: 'POST', + body: { + Username: username, + Password: password, + KeepLoggedIn: keepLoggedIn, + }, + }); + } finally { + API.antiforgaryToken = null; + } } - static logout() { - return this.fetch('/auth/logout', { - method: 'POST', + static async logout() { + try { + API.antiforgaryToken = null; + return await this.fetch('/auth/logout', { + method: 'POST', + }); + } finally { + API.antiforgaryToken = null; + } + } + + static getAntiforgary() { + return this.fetch('/auth/antiforgary', { + method: 'GET', }); } diff --git a/FileSystemWeb/Controllers/AuthController.cs b/FileSystemWeb/Controllers/AuthController.cs index e0acfd0..ca26055 100644 --- a/FileSystemWeb/Controllers/AuthController.cs +++ b/FileSystemWeb/Controllers/AuthController.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using FileSystemCommon.Models.Auth; using FileSystemWeb.Models; +using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; @@ -12,10 +13,12 @@ namespace FileSystemWeb.Controllers public class AuthController : ControllerBase { private readonly SignInManager signInManager; + private readonly IAntiforgery antiforgery; - public AuthController(SignInManager signInManager) + public AuthController(SignInManager signInManager, IAntiforgery antiforgery) { this.signInManager = signInManager; + this.antiforgery = antiforgery; } [HttpPost("login")] @@ -33,5 +36,11 @@ public async Task Logout() await signInManager.SignOutAsync(); return Ok(); } + + [HttpGet("antiforgary")] + public ActionResult GetAntiforgary() + { + return Ok(antiforgery.GetAndStoreTokens(HttpContext).RequestToken); + } } } \ No newline at end of file diff --git a/FileSystemWeb/Startup.cs b/FileSystemWeb/Startup.cs index 0f504c6..ab7369d 100644 --- a/FileSystemWeb/Startup.cs +++ b/FileSystemWeb/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -64,7 +65,25 @@ public void ConfigureServices(IServiceCollection services) config.Cookie.HttpOnly = true; }); - services.AddControllersWithViews(); + services.AddAntiforgery(options => + { + // Set Cookie properties using CookieBuilder properties†. + options.Cookie.Name = "fs_csrf"; + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.IsEssential = true; + options.Cookie.SameSite = SameSiteMode.Strict; + options.Cookie.Expiration = null; + options.Cookie.MaxAge = null; + options.FormFieldName = "AntiforgeryFieldname"; + options.HeaderName = "X-CSRF-TOKEN-HEADERNAME"; + options.SuppressXFrameOptionsHeader = false; + }); + + services.AddControllersWithViews(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }); // In production, the React files will be served from this directory services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/build"; }); @@ -91,17 +110,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseAuthorization(); - if (!env.IsDevelopment()) - { - string sslCertificateDirPath = Path.Combine(Directory.GetCurrentDirectory(), @".well-known"); - if (!Directory.Exists(sslCertificateDirPath)) Directory.CreateDirectory(sslCertificateDirPath); - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = new PhysicalFileProvider(sslCertificateDirPath), - RequestPath = new PathString("/.well-known"), - ServeUnknownFileTypes = true // serve extensionless files - }); - } + //app.Use((context, next) => + //{ + // context.Response.Headers.Add("Content-Security-Policy", "default-src 'self' 'unsafe-inline';"); + // return next(); + //}); app.UseEndpoints(endpoints => {