0

I'm fairly new to ASP.NET Core, Angular HttpClient and Observables, so I need some help.

I have created and published a web-based application that is not public facing which has an Angular 9 client and a Microsoft ASP.NET Core 2.2 API controller backend that uses Entity Framework Core ORM to access SQL Server. I use Microsoft ASP.NET Core Identity for authentication, authorization, Identity Model Tokens (JWT) and AntiForgery as security.

I can login (from Angular UI) with credentials and retrieve my required ORM Entity data using Http Get requests for my various tables. But when I try to Post, Put or Delete an entity in a table, generally only the first attempts are successful (200 status) and further attempts fail (302 status). I have to logout/login again to continue making updates. I am not sending credentials in my requests after the initial login request.

I'm including my Angular component, repository and datasource code plus my StartUp.cs and API controller.

Note that I am only generating a JWT as a bearer token once upon login, returning it to the client, and sending it back to API in subsequent requests for comparison. I am not decoding the bearer token for the username value at this time, but I did in the past with same result.

I think the status code 302 is telling me that my Put request found the URL, but that I am no longer authenticated on the server at this point, and I am not sure what to do about that scenario.

Three options that I am considering are (1) sending login credentials with destructive http requests, (2) looking into refresh tokens, or (3) refreshing my JWT token after each Put, Post, & Delete request & returning it to client as a new token. My problem is that I am not sure if any of these options is designed to work with Microsoft ASP.NET Core Identity.

Any help in pointing out what I'm missing or doing wrong would be greatly appreciated.

Update from Angular 9 table works 1st time, not 2nd time:

Angular html table update

Browser network options shows:

F12-Options

Angular 9 component method:

  saveMember(member: Member): boolean {

  this.repo.saveMember(member).subscribe(result => this.members.splice(this.members.
    findIndex(m => m.memberID == member.memberID), 1, result),
    err => console.log('From saveMember(): ', err),
    () => console.log('From saveMember(): Completed'));

  }

Angular 9 repository Observable method includes Http HEAD request:

    // Repository Method:  creates new member or updates existing member
    saveMember(theMember: Member): Observable<Member> {

       if (theMember.memberID == null || theMember.memberID == 0) {
           this.dataSource.saveMemberHeader().subscribe(result => { this.showError = !result },
            err => console.log('From Head() Request: ', err),
            () => console.log('From Head() Request: Completed'));

       return this.dataSource.saveMember(theMember).pipe(map(response => {
          if (response) {
            this.members.push(response);
          }
          return response;
       })); 

    }
    else {
        this.dataSource.updateMemberHeader().subscribe(result => { this.showError = !result },
          err => console.log('From Head() Request: ', err),
          () => console.log('From Head() Request: Completed'));

      return this.dataSource.updateMember(theMember).pipe(map(response => {
          if (response) {
              let index = this.members.findIndex(item => this.locator(item, response.memberID));
                  this.members.splice(index, 1, response);
          }
          return response;
          }));  
        }
     }

Angular 9 Datasource:

  // Datasource http calls to server

  update: string = "update";
  updateMember(member: Member): Observable<Member> {
    return this.sendPutRequest<Member>(`${this.url}/${this.update}`, member);
  }

  private sendPutRequest<T>(url: string, member: Member): Observable<T> {
          let myHeaders = new HttpHeaders();
      if (this.authCookie.JWTauthcookie == null) {
              myHeaders = myHeaders.set("Access-Key", "<secret>");
          } else {
              myHeaders = myHeaders.set("Authorization", "Bearer<" + this.authCookie.JWTauthcookie + ">");
          }
          myHeaders = myHeaders.set("Application-Names", ["ClientApp", "SPA"]);
          return this.http.put<T>(url,
        {
            member: member
        },
        {
            headers: myHeaders
        }).pipe(map(response => { 
        return response;  // response object is a Member entity
        }))
        .pipe(catchError((error: Response) =>
            throwError(`An Error Occurred: ${error.statusText} (${error.status})`)
        ));
  }


  updateMemberHeader(member?: Member): Observable<any> {
        return this.sendRequest<Member>("HEAD", `${this.url}/${this.update}`, member);
  }

  private sendRequest<T>(verb: string, url: string, body?: Member): Observable<T> {
    let myHeaders = new HttpHeaders();
    if (this.authCookie.JWTauthcookie == null) {
        myHeaders = myHeaders.set("Access-Key", "<secret>");
    } else {
      myHeaders = myHeaders.set("Authorization", "Bearer<" + this.authCookie.JWTauthcookie + ">");
        }
        myHeaders = myHeaders.set("Application-Names", ["ClientApp", "SPA"]);
        return this.http.request<T>(verb, url, {
        body: body,
        headers: myHeaders
    }).pipe(map(response => {
        return response;    
          })).pipe(catchError((error: Response) =>
         throwError(`An Error Occurred: ${error.statusText} (${error.status})`)
             ));
      }   

ASP.NET Core Web Account controller:

  using System;
  using System.ComponentModel.DataAnnotations;
  using System.Collections.Generic;
  using System.Threading.Tasks;
  using System.Security.Claims;
  using Microsoft.AspNetCore.Identity;
  using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
  using Microsoft.AspNetCore.Mvc;
  using ServerApp.Infrastructure;  // contains the code for "Jwt_GenerateToken"

  namespace ServerApp.Controllers
  {

      [ValidateAntiForgeryToken]
      public class AccountController : Controller
      {
          private UserManager<IdentityUser> userManager;
          private SignInManager<IdentityUser> signInManager;  
          private RoleManager<IdentityRole> roleManager;

          public static string bearerJWTokenString;  
          private string bearerToken;

          public SignInResult signInResult;
          private string plainName = "";
          private string plainLoginPwd = "";

          public AccountController(UserManager<IdentityUser> userMgr,
        SignInManager<IdentityUser> signInMgr, RoleManager<IdentityRole> roleMgr)
          {
              userManager = userMgr;
              signInManager = signInMgr;
              roleManager = roleMgr;
          }

          [HttpPost("/api/account/login")]  
          [IgnoreAntiforgeryToken]
          public async Task<IActionResult> Login([FromBody] LoginViewModel creds)
          {
              if (ModelState.IsValid && await DoLogin(creds))
              {
                  string isAuthenticated = signInResult.Succeeded.ToString(); 
                  object myJWT = "{ " + '\n' + "   " + '"' + "success" + '"' + ": " + '"' + isAuthenticated + '"' + "," + '\n' +
                  "   " + '"' + "token" + '"' + ':' + '"' + _token + '"' + "," + '\n' +
                  "}";

                  return Ok(myJWT);   
              }
              return BadRequest(); 
          }

          public string _token = null;

          public async Task<bool> DoLogin(LoginViewModel creds)
          {
              plainName = creds.Name;
              plainLoginPwd = creds.Password;

              IdentityUser loginuser = await userManager.FindByNameAsync(plainName);

              if (loginuser != null)
              {
                  await signInManager.SignOutAsync();

                  signInResult =
                  await signInManager.PasswordSignInAsync(loginuser, plainLoginPwd, false, false);

                  if (signInResult.Succeeded)
                  {

                      // generates a signed Json Web Token with current user name
                      _token = Jwt_GenerateToken.GenerateToken(loginuser.ToString(), 90); // in Infrastructure folder
                      bearerJWTokenString = _token;
                  }

                  return signInResult.Succeeded; 
             }
             return false;
          }

      }

      public class LoginViewModel
      {
          [Required]
          public string Name { get; set; }
          [Required]
          public string Password { get; set; }
      }

  }

ASP.NET Core Web API controller:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using System.ComponentModel.DataAnnotations;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using ServerApp.Infrastructure;

namespace ServerApp.Controllers {

[Route("api/members")]
[ApiController]
[Authorize]
[AutoValidateAntiforgeryToken]
public class MemberValuesController : Controller 
{
    private string bearerToken;
    private SecurityToken validatedToken;
    private JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
    private ClaimsPrincipal vtoken = new ClaimsPrincipal();

    private ApplicationDbContext context;

    public MemberValuesController(ApplicationDbContext ctx)
    {
        context = ctx;
    }

    public class MemberModel
    {
        [Required]
        public Member Member { get; set; }  // must capitalize or creates naming convention error!
    }

    [HttpPut("/api/members/update")]  
    [Authorize(Roles = "Admin, SuperUser, User")]
    public async Task<Member> Put([FromBody]  MemberModel MbrModel) 
    {
        bearerToken = Request.Headers["Authorization"];
        var bearer = bearerToken.Substring(7, bearerToken.Length - 8);

        vtoken = tokenHandler.ValidateToken(bearer, Jwt_GenerateToken.vParms, out validatedToken);
        if (vtoken.Identity.Name.ToString() == Jwt_GenerateToken.cPrince.Name.ToString())
        {

            if (vtoken.Identity.IsAuthenticated == true)
            {
                Member UpdatedMember = new Member();
                if (ModelState.IsValid)
                {
                    UpdatedMember = await DoMemberUpdate(MbrModel);
                    return UpdatedMember;
                }
                    return UpdatedMember;
            }
            else
            {
                return ViewBag;
            }
        }
        else
        {
            return ViewBag;
        }
    }

    private async Task<Member> DoMemberUpdate(MemberModel MbrModel)
    {
        Member member = new Member();
        member = MbrModel.Member;

        context.Members.Update(member);

        var saved = false;

        while (!saved)
        {
            try
            {
                await context.SaveChangesAsync();
                saved = true;
                return member;
            }
            catch (DbUpdateConcurrencyException ex)
            {
                foreach (var entry in ex.Entries)
                {
                    if (entry.Entity is Member)
                    {
                        var proposedValues = entry.CurrentValues;
                        var databaseValues = entry.GetDatabaseValues();

                        foreach (var property in proposedValues.Properties)
                        {
                            var proposedValue = proposedValues[property];

                            proposedValues[property] = proposedValue;    //<value to be saved>;
                        }
                        entry.OriginalValues.SetValues(databaseValues);
                    }
                }
            }
        }

        return member;
    }

    [HttpHead("/api/members/update")]
    public async Task<IActionResult> GetUpdateHeader()
    {
        bearerToken = Request.Headers["Authorization"];
        var bearer = bearerToken.Substring(7, bearerToken.Length - 8);

        if (AccountController.bearerJWTokenString == bearer)
        {
            int MbrCount = 0;
            MbrCount = await DoGetMembersHeader();

            if (MbrCount > -1)
            {
                Response.Headers.Add("Items-total", MbrCount.ToString());
            }
            else
            {
                Response.Headers.Add("Items-total", "Bad result");
            }
            return Ok();
        }
        else
        {
            return BadRequest();
        }
    }

    private async Task<int> DoGetMembersHeader()
    {
        return await context.Members.Include(m => m.MemberID)
        .OrderBy(m => m.Last_Name + m.First_Name + m.MidInit)
        .Select(m => new
        {
            m.MemberID,
            m.Last_Name,
            m.First_Name,
            m.MidInit,
            m.Email_Address,
            m.Cell_Phone,

            // ..... more properties

            m.GUIDKey,
            m.RowVersion
        }).CountAsync();
    }
}

Infrastructure class that generates token:

  using System;
  using System.IdentityModel.Tokens.Jwt;
  using System.Security.Claims;
  using Microsoft.IdentityModel.Tokens;

  namespace ServerApp.Infrastructure
  {
      public class Jwt_GenerateToken
      {
          public static ClaimsIdentity cPrince;
          public static TokenValidationParameters vParms = new TokenValidationParameters();

          private const string Beans = "bhmYDKCEN4pGSEoJcI6t ... more ... ==";
          public static string GenerateToken(string username, int expireMinutes)
          {
              // "expireMinutes" value along with "username" comes from "DoLogin()" method in AccountController.cs
              byte[] symmetricKey = Convert.FromBase64String(Beans);
              var tokenHandler = new JwtSecurityTokenHandler();

              var now = DateTime.UtcNow;
              var tokenDescriptor = new SecurityTokenDescriptor
              {
                  Subject = new ClaimsIdentity(new[]
                  {
                      new Claim(ClaimTypes.Name, username)
                  }),

                  Expires = now.AddMinutes(Convert.ToDouble(expireMinutes)),

                  SigningCredentials = new SigningCredentials(
                      new SymmetricSecurityKey(symmetricKey),
                      SecurityAlgorithms.HmacSha256Signature)
              };
              var stoken = tokenHandler.CreateToken(tokenDescriptor);
              var token = tokenHandler.WriteToken(stoken);

              // creating a static public ClaimsPrincipal object to compare to JWT token in client request
              //  https://stackoverflow.com/questions/40281050/jwt-authentication-for-asp-net-web-api?rq=1
              vParms.IssuerSigningKey = tokenDescriptor.SigningCredentials.Key;
              vParms.RequireExpirationTime = true;
              vParms.ValidateIssuer = false;
              vParms.ValidateAudience = false;
              cPrince = tokenDescriptor.Subject;  // the Claims Identity object to compare to future requests
              // end update

              return token;    
          }

      }
  }

StartUp.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;  
using Microsoft.Extensions.DependencyInjection;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
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.AddAuthentication();   // is this necessary or redundant here?

            string conString_A = Configuration["TheDatabaseProd:AConnectionString"];

            services.AddDbContext<ApplicationDbContext>(options =>
                      options.UseSqlServer(conString_A));
        
            string conString_I = Configuration["TheDatabaseProd:IConnectionString"];

            services.AddDbContext<IdentityDataContext>(options =>
                      options.UseSqlServer(conString_I));

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

            services.AddMvc()
                    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

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

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

            services.AddMemoryCache();    // is this necessary or redundant?

            services.AddDistributedMemoryCache();  

            services.AddSession(); 
            services.AddSession(options =>
            {
                   options.Cookie.Name = "MyApp.Session";
                   options.IdleTimeout = TimeSpan.FromMinutes(10); 
                   options.Cookie.IsEssential = true;  
                   options.Cookie.SameSite = SameSiteMode.Lax;
             });
        }  

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

                 app.UseStaticFiles();

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

                 app.UseAuthentication();

                 app.UseSession();  

                 app.Use(nextDelegate => context =>
                 {  
                    string path = context.Request.Path.Value;
                    string[] directUrls = { "/appmenu", "/table", "/form", "/form/edit", "/form/create",
                    "/queryTable", "/oneMemberTable", "/retreatTable", "/coreTeamMemberTable" };

                    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,  // HttpOnly must be false or the  x-xsrf-token header is not filled in the Request     
                                 Secure = true,
                                 IsEssential = true   // was true, 2023.06.06
                        });
                    }

                    return nextDelegate(context);
                 });

                 app.UseMvc(routes =>
                 {
                     routes.MapRoute(
                         name: "default",
                         template: "{controller=Home}/{action=Index}/{id?}");   
                 });
        }
    }
}
Cwinds
  • 167
  • 11
  • Please check that the site and redirect URL you are requesting the second time are the same as the first time. Also check whether you have carried a certificate or token when you request for the second time. – Chen Jul 03 '23 at 08:06
  • Thanks @Chen. If I hurry, I can get up to 3 updates completed successfully before I get the 302 Found error condition. That takes about 20 seconds according to the time stamps in Network options. Also, the Bearer token sent with each successive PUT request is the same as the first original from login. The redirects come from Identity, not me. I also EDITED my API Controller - changing it to show that I can successfully pass the Bearer token user comparison to the Claim Principal and that the server recognizes that I have authenticated - at least for the first 20 seconds. Thanks again. – Cwinds Jul 04 '23 at 02:12
  • Another question: is there a max number of times that I can use the same security token before I have to regenerate a new token? – Cwinds Jul 04 '23 at 22:14
  • Or is there a max number of times in ASP.NET Core that I can send a request to an API Controller without re-authenticating? – Cwinds Jul 07 '23 at 03:48
  • Theoretically speaking, if you don't set a limit on the number of times, you can use it until the token expires without re-authentication to generate a new token. – Chen Jul 14 '23 at 08:57

0 Answers0