3

I am fairly new to Angular and MVC web applications, and I am building an ASP.NET Core 2.2 MVC, Angular 9 and EF Core SPA application with the authentication and authorization features of Microsoft.AspNetCore.Identity and Microsoft.AspNetCore.Antiforgery XSRF cookies. My login method is originated in the client app.

In my development environment, I can log in and authenticate and use the appropriate authorization to GET, PUT, POST & DELETE. And all the verbs in my ASP.NET Core MVC api controller methods match the HTTP verbs in spelling.

Once deployed to my Windows Shared production web hosting server (uses IIS/10), I can log in, Authenticate and with appropriate Authorization GET, PUT, POST and DELETE successfully for the first 10 minutes on the website.

After about 10 minutes, when I try to update data, for a PUT I receive an HTTP 302 - Found, followed by an HTTP 405 - Method Not Allowed for an attempted Redirect to /Account/Login (Server-side).

I have already modified the web.config to eliminate the WebDAV issue, and like I said, I can perform all the HTTP methods for the first ten minutes after being signed in.

I have posted some code below, please ask if it helps to see any ClientApp http datasource methods.

Does anyone have any suggestions of things to try?

Web.config

 <?xml version="1.0" encoding="utf-8"?>
 <configuration>
   <location path="." inheritInChildApplications="false">
     <system.webServer>
       <modules>
         <remove name="WebDAVModule" />
       </modules>
    <handlers>
      <remove name="WebDAVHandler" />
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath=".\ServerApp.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="InProcess" />
   </system.webServer>
  </location>
  <connectionStrings>
   <remove name="LocalSqlServer" />
  </connectionStrings>
 </configuration>

Startup.cs

 using ...
 using Microsoft.AspNetCore.Identity;
 using Microsoft.AspNetCore.Antiforgery;

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

         public IConfiguration Configuration { get; }

         public void ConfigureServices(IServiceCollection services)
         {
             services.Configure<CookiePolicyOptions>(options =>
             {
                 options.CheckConsentNeeded = context => true;
                 options.MinimumSameSitePolicy = SameSiteMode.None;
             });

             string connectionString = ... ;
             services.AddDbContext<ApplicationDbContext>(options =>
               options.UseSqlServer(connectionString));

             services.AddIdentity<IdentityUser, IdentityRole>()
                  .AddEntityFrameworkStores<IdentityDataContext>();

             services.AddTransient<IWebService ... >();
             services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

             services.AddAntiforgery(options =>
             {
                 options.HeaderName = "X-XSRF-TOKEN";
             });

             services.AddMvc(options =>
             {
                 options.Filters.Add(new ValidateAntiForgeryTokenAttribute());
             });
         }

     // to configure the HTTP request pipeline.
         public void Configure(IApplicationBuilder app, IHostingEnvironment env,
                 IServiceProvider services, IAntiforgery antiforgery)
         {
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
             }
             else
             {
                 app.UseExceptionHandler("/Home/Error");
                 app.UseHsts();
             }

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

             app.UseStaticFiles(new StaticFileOptions
             {
               RequestPath = "",
               FileProvider = new PhysicalFileProvider(
               Path.Combine(Directory.GetCurrentDirectory(), "./wwwroot/app"))
             });  

             app.UseCookiePolicy();
             app.UseAuthentication();   

             app.Use(nextDelegate => context =>
             {  
                 string path = context.Request.Path.Value;
                 string[] directUrls = { "/xxxxx", "/aaaxxx", "/form", "/table" };
                 if (path.StartsWith("/api") || string.Equals("/", path) ||
                     directUrls.Any(url => path.StartsWith(url)))
                 {
                     var tokens = antiforgery.GetAndStoreTokens(context);
                     context.Response.Cookies.Append("XSRF-REQUEST-TOKEN", 
                                  tokens.RequestToken,
                         new CookieOptions()
                         {
                             HttpOnly = false,
                             Secure = false,
                             IsEssential = true,
                             Expires = DateTime.Now.AddMinutes(120)  
                         });
                 }
                 return nextDelegate(context);
             });

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

         // used in Development only - commented out for deployment to Production
         //    app.UseSpa(spa =>
         //    {
         //        spa.Options.SourcePath = "../ClientApp";
         //        spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
         //    });

         }
     }
 }

Good PUT - HTTP Headers:

 GENERAL:
 Request URL: https://aaa.com/api/widgets
 Request Method: PUT
 Status Code: 200  
 Remote Address: xxx.xxx.xx.xx:443
 Referrer Policy: strict-origin-when-cross-origin
 
 RESPONSE HEADERS:
 cache-control: no-cache, no-store
 content-length: 815
 content-type: application/json; charset=utf-8
 date: Sat, 16 Apr 2022 23:32:15 GMT
 pragma: no-cache
 server: Microsoft-IIS/10.0
 set-cookie: XSRF-REQUEST-TOKEN=CfDJ ... olg; expires=Sun, 17 Apr 2022 01:02:16 GMT; path=/; samesite=lax
 strict-transport-security: max-age=2592000
 x-powered-by: ASP.NET
 x-powered-by-plesk: PleskWin

 REQUEST HEADERS:
 :authority: aaa.com
 :method: PUT
 :path: /api/widgets
 :scheme: https
 accept: application/json, text/plain, */*
 accept-encoding: gzip, deflate, br
 accept-language: en-US,en;q=0.9
 access-key: <secret>
 application-names: ClientApp,Angular
 cache-control: no-cache
 content-length: 815
 content-type: application/json
 cookie: .AspNetCore.Antiforgery.xxxxxxxxxxx=CfDJ ... Jfs; .AspNetCore.Identity.Application=CfDJ ... OqA;  XSRF-REQUEST-TOKEN=CfDJ ... EuQ
 origin: https://aaa.com
 pragma: no-cache
 referer: https://aaa.com/form/edit/57
 sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
 sec-ch-ua-mobile: ?0
 sec-ch-ua-platform: "Windows"
 sec-fetch-dest: empty
 sec-fetch-mode: cors
 sec-fetch-site: same-origin
 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
 x-xsrf-token: CfDJ ... EuQ

 Response body: [has good data]

Bad PUT - HTTP headers:

 GENERAL: 
 Request URL: https://aaa.com/api/widgets
 Request Method: PUT
 Status Code: 302 
 Remote Address: xxx.xxx.xx.xx:443
 Referrer Policy: strict-origin-when-cross-origin

 RESPONSE HEADERS:
 cache-control: no-cache, no-store
 date: Sat, 16 Apr 2022 23:43:50 GMT
 location: https://aaa.com/Account/Login?ReturnUrl=%2Fapi%2Fwidgets
 pragma: no-cache
 server: Microsoft-IIS/10.0
 set-cookie: .AspNetCore.Antiforgery.xxxxxKXldp8=CfDJ ... rVM; path=/; samesite=strict; httponly
 set-cookie: XSRF-REQUEST-TOKEN=CfDJ ... W9M; expires=Sun, 17 Apr 2022 01:13:50 GMT; path=/; 
 samesite=lax
 strict-transport-security: max-age=2592000
 x-frame-options: SAMEORIGIN
 x-powered-by: ASP.NET
 x-powered-by-plesk: PleskWin
 
 REQUEST HEADERS:
 :authority: aaa.com
 :method: PUT
 :path: /api/widgets
 :scheme: https
 accept: application/json, text/plain, */*
 accept-encoding: gzip, deflate, br
 accept-language: en-US,en;q=0.9
 access-key: <coolsecret>
 application-names: ClientApp,Angular
 cache-control: no-cache
 content-length: 815
 content-type: application/json
 cookie: .AspNetCore.Antiforgery.xxxxxKXldp8=CfDJ ... Jfs; .AspNetCore.Identity.Application=CfDJ ... OqA; XSRF-REQUEST-TOKEN=CfDJ ... olg
 origin: https://aaa.com
 pragma: no-cache
 referer: https://aaa.com/form/edit/57
 sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
 sec-ch-ua-mobile: ?0
 sec-ch-ua-platform: "Windows"
 sec-fetch-dest: empty
 sec-fetch-mode: cors
 sec-fetch-site: same-origin
 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
 x-xsrf-token: CfDJ ... olg

 Response body:       Failed to load response data.  No content available becuase this request was redirected.
 

Redirect to login:

 GENERAL: 
 Request URL: https://aaa.com/Account/Login?ReturnUrl=%2Fapi%2Fwidgets
 Request Method: PUT
 Status Code: 405 
 Remote Address: xxx.xxx.xx.xx:443
 Referrer Policy: strict-origin-when-cross-origin

 RESPONSE HEADERS:
 allow: GET, POST
 date: Sat, 16 Apr 2022 23:43:50 GMT
 server: Microsoft-IIS/10.0
 strict-transport-security: max-age=2592000
 x-powered-by: ASP.NET
 x-powered-by-plesk: PleskWin
 
 REQUEST HEADERS:
 :authority: aaa.com
 :method: PUT
 :path: /Account/Login?ReturnUrl=%2Fapi%2Fwidgets
 :scheme: https
 accept: application/json, text/plain, */*
 accept-encoding: gzip, deflate, br
 accept-language: en-US,en;q=0.9
 access-key: <coolsecret>
 application-names: ClientApp,Angular
 cache-control: no-cache
 content-length: 815
 content-type: application/json
 cookie: .AspNetCore.Identity.Application=CfDJ ... OqA; .AspNetCore.Antiforgery.xxxxxxxxxxx=CfDJ ... rVM;  XSRF-REQUEST-TOKEN=CfDJ ... W9M
 origin: https://aaa.com
 pragma: no-cache
 referer: https://aaa.com/form/edit/57
 sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
 sec-ch-ua-mobile: ?0
 sec-ch-ua-platform: "Windows"
 sec-fetch-dest: empty
 sec-fetch-mode: cors
 sec-fetch-site: same-origin
 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
 x-xsrf-token: CfDJ ... olg
 
 Payload:   ReturnUrl: %2Fapi%2Fwidgets
 Response:  This request has no response data available.

ServerApp.AccountController

 namespace ServerApp.Controllers
 {
     [ValidateAntiForgeryToken]
     public class AccountController : Controller
     {
         private UserManager<IdentityUser> userManager;
         private SignInManager<IdentityUser> signInManager;
 
         public AccountController(UserManager<IdentityUser> userMgr,
                           SignInManager<IdentityUser> signInMgr)
         {
             userManager = userMgr;
             signInManager = signInMgr; 
         }
 
         [HttpGet]
         public IActionResult Login(string returnUrl)
         {
             ViewBag.returnUrl = returnUrl;
             return View();
         }

         [HttpPost]
         public async Task<IActionResult> Login(LoginViewModel creds, string returnUrl)
         {
             if (ModelState.IsValid)
             {
                 if (await DoLogin(creds))
                 {
                     return Redirect(returnUrl ?? "/");
                 }
                 else
                 {
                    ModelState.AddModelError("", "Invalid username or password");
                 }
             }
             return View(creds);
         }
 
         [HttpPost]
         public async Task<IActionResult> Logout(string redirectUrl)
         {
             await signInManager.SignOutAsync();
             return Redirect(redirectUrl ?? "/");
         }

         [HttpPost("/api/account/login")]  // login request from client posts here
         [IgnoreAntiforgeryToken]  
         public async Task<IActionResult> Login([FromBody] LoginViewModel creds)
         {
             if (ModelState.IsValid && await DoLogin(creds))
             {
                 return Ok("true");
             }
             return BadRequest(); // http 400
         }
 
         // called by login from client (see above)
         private async Task<bool> DoLogin(LoginViewModel creds)
         {
             IdentityUser user = await userManager.FindByNameAsync(creds.Name);
             if (user != null)
             {
                 await signInManager.SignOutAsync();
                 Microsoft.AspNetCore.Identity.SignInResult result =
                     await signInManager.PasswordSignInAsync(user, creds.Password, false, false);
                 return result.Succeeded;
             }
             return false;
         }
 
         [HttpPost("/api/account/logout")]
         public async Task<IActionResult> Logout()
         {
             await signInManager.SignOutAsync();
             return Ok();
         }
     }
 
     public class LoginViewModel
     {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Password { get; set; }
     }
 }

ServerApp.AntiforgeryController

 namespace ServerApp.Controllers
 {
     [ApiController]
     [IgnoreAntiforgeryToken]
     public class AntiForgeryController : Controller
     {
         private IAntiforgery _antiForgery;
         public AntiForgeryController(IAntiforgery antiForgery)
         {
             _antiForgery = antiForgery;
         }
 
         [Route("api/antiforgery")]
         [IgnoreAntiforgeryToken]
         public IActionResult GenerateAntiForgeryTokens()
         {
             var tokens = _antiForgery.GetAndStoreTokens(HttpContext);
             Response.Cookies.Append("XSRF-REQUEST-TOKEN", tokens.RequestToken, new CookieOptions
             {
                 HttpOnly = false,
             });
             return Ok("true");              
         }
     }
 }

HomeController.Error

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }

ServerApp.WidgetValuesController (API) HTTP methods

  using Microsoft.AspNetCore.Authorization;

  namespace ServerApp.Controllers {

  [Route("api/widgets")]
  [Authorize(Roles = "AdminRole1, AdminRole2")]
  [ValidateAntiForgeryToken]
  public class WidgetValuesController : Controller {
    private IWebServiceWidgetRepository repository;

    public WidgetValuesController(IWebServiceWidgetRepository repo) 
        => repository = repo;


    [HttpGet]
    public object Widgets() { 
        return repository.GetWidgets(); 
    }

    [HttpPost] 
    public Widget Post([FromBody] Widget widget) { 
        return repository.StoreWidget(widget); 
    }

    [HttpPut]  
    public Widget Put([FromBody] Widget widget) 
    {
          return repository.UpdateWidget(widget);
    }

  }
 }
Cwinds
  • 167
  • 11
  • look at [this](https://www.loginradius.com/blog/identity/refresh-tokens-jwt-interaction/) – Dean Van Greunen Apr 17 '22 at 17:22
  • and [this](https://jasonwatmore.com/post/2020/05/25/aspnet-core-3-api-jwt-authentication-with-refresh-tokens) – Dean Van Greunen Apr 17 '22 at 17:23
  • 1
    Thanks @Dean Van Greunen - for pointing me in the right direction. I added a new method to create JWT Token at login, which I now add to each subsequent http request as Authorization cookie. WIth user activity, I was able to stay online for about 30 minutes before timing out with same error responses. I set my JWT token to expire at 120 minutes. Should I add this to StartUp.cs: **options.ConsentCookie.Expiration = TimeSpan.FromHours(2);** – Cwinds Apr 19 '22 at 15:30
  • yes @Cwinds 100% – Dean Van Greunen Apr 19 '22 at 15:50

0 Answers0