0

I have an Azure SQL DB that initially had the following columns:

user name password hash password salt

This DB serves a .NET Core C# API that checks username and password to return a JWT token.

The API had a User object that comprised all three columns with the correct types, a DbContext with a DbSet<User>, and an IServiceCollection that used said DbContext.

The API worked fine, returning a JWT token as needed.

I have since needed to add an extra parameter to check and pass to the JWT creation - the relevant column has been created in the DB, the User object in the API has been updated to include the extra parameter and that extra parameter is observed in the Intellisense throughout the API code.

The issue is that when the API is deployed to Azure, the extra parameter isn't being recognised and populated; how do I make the API correctly update to use the new DbContext and retrieve the User with the extra parameter?

(I've omitted the interfaces for brevity, as they're essentially the corresponding classes)

User, UserRequest and MyApiDbContext Classes:

using Microsoft.EntityFrameworkCore;

namespace MyApi.Models
{
    // Basic user model used for authentication
    public class User
    {
        public string UserId { get; set; }
        public byte[] PasswordHash { get; set; }
        public byte[] PasswordSalt { get; set; }
        public string ExtraParam { get; set; } // newly added parameter
    }

    public class UserRequest
    {
        public string UserId { get; set; }
        public string password { get; set; }
    }

    public class MyApiDbContext : DbContext
    {
        public MyApiDbContext(DbContextOptions<MyApiDbContext> options)
            : base(options)
        {
        }
                
        public DbSet<User> Users { get; set; }
    }
}

The AuthRepository that retrieves the user:

using Microsoft.EntityFrameworkCore;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;

namespace MyApi.Services
{
    public class AuthRepository : IAuthRepository
    {
        private readonly MyApiDbContext _context;
        public AuthRepository(MyApiDbContext context)
        {
            _context = context;
        }
        public async Task<User> Login(string username, string password)
        {
            // my test user gets returned
            User returnedUser = await _context.Users.FirstOrDefaultAsync(x => x.UserId == username);

            if (returnedUser == null)
            {
                return null;
            }

            // the password get verified
            if (!VerifyPasswordHash(password, returnedUser.PasswordHash, returnedUser.PasswordSalt))
            {
                return null;
            }

            // this does not get changed, but the value set in the DB is definitely a string
            if (returnedUser.ExtraParam == null || returnedUser.ExtraParam == "")
            {
                returnedUser.ExtraParam = "placeholder"
            }

            return returnedUser;
        }
    }
}

The AuthService that calls the AuthRepository for the user then "creates the JWT token" (just returning a string for this example), currently set up to return the user details:

using Microsoft.Extensions.Options;
using MyApi.Interfaces;
using MyApi.Models;
using System;
using System.Threading.Tasks;

namespace MyApi.Services
{
    public class AuthService : IAuthService
    {
        private readonly IOptions<MyApiBlobStorageOptions> _settings;
        private readonly IAuthRepository _repository;

        public AuthService(IOptions<MyApiBlobStorageOptions> settings, IAuthRepository repository)
        {
            _repository = repository;
            _settings = settings;
        }

        public async Task<string> Login(string username, string password)
        {
            User returnedUser = await _repository.Login(username, password);

            if (returnedUser != null)
            {
                // currently returns "UserIdInDB,ProvidedPasswordFromLogin,"
                return $"{returnedUser.UserId},{password},{returnedUser.ExtraParam}";
            }

            return null;
        }
    }
}

The controller that calls the AuthService:

using Microsoft.AspNetCore.Mvc;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace MyApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly MyApiDbContext _context;
        private readonly IAuthService _authService;

        public AuthController(MyApiDbContext context, IAuthService authService)
        {
            _context = context;
            _authService = authService;
        }

        [HttpPost("login")]
        public async Task<IActionResult> Login(UserRequest loginUser)
        {
            string token = await _authService.Login(loginUser.UserId, loginUser.Password);

            if (token != null)
            {
                return Ok(token);
            }

            return Unauthorized("Access Denied!!");
        }
    }
}

The startup class that registers everything:

using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MyApi.Interfaces;
using MyApi.Models;
using MyApi.Services;
using Microsoft.Extensions.Azure;
using Azure.Storage.Queues;
using Azure.Storage.Blobs;
using Azure.Core.Extensions;
using System;

namespace MyApi
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        private readonly ILogger<Startup> _logger;
        private readonly IConfiguration _config;
        public Startup(ILogger<Startup> logger, IConfiguration config)
        {
            _logger = logger;
            _config = config;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add dBContext for DB
            services.AddDbContextPool<MyApiDbContext>(options => options.UseSqlServer(_config.GetConnectionString("MyAzureDb")));
            
            // Add DI Reference for Repository
            services.AddScoped<IAuthRepository, AuthRepository>();

            // Add DI Reference for Azure Blob Storage Processes
            services.AddScoped<IBlobService, AzureBlobService>();

            // DI Reference for AuthService
            services.AddScoped<IAuthService, AuthService>();

            // Add configuration section for Constructor Injection
            services.Configure<ApiBlobStorageOptions>(_config.GetSection("MyApiBlobStorage"));

            services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
                            .GetBytes(_config.GetSection("MyApiBlobStorage:Secret").Value)),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                    options.Events = new JwtBearerEvents()
                    {
                        OnAuthenticationFailed = context =>
                            {
                                _logger.LogWarning("Token authentication failed whilst attempting to upload file");

                                return Task.CompletedTask;
                            }
                    };
                });
            services.AddAzureClients(builder =>
            {
                builder.AddBlobServiceClient(Configuration["ConnectionStrings:MyApiBlobStorage/AzureBlobStorageConnectionString:blob"], preferMsi: true);
            });
        }

        // 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
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
            app.UseAuthentication();
            app.UseMvc();
        }
    }
    internal static class StartupExtensions
    {
        public static IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddBlobServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
        {
            if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
            {
                return builder.AddBlobServiceClient(serviceUri);
            }
            else
            {
                return builder.AddBlobServiceClient(serviceUriOrConnectionString);
            }
        }
        public static IAzureClientBuilder<QueueServiceClient, QueueClientOptions> AddQueueServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
        {
            if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
            {
                return builder.AddQueueServiceClient(serviceUri);
            }
            else
            {
                return builder.AddQueueServiceClient(serviceUriOrConnectionString);
            }
        }
    }
}

Let me know if there is anything else required for understanding: the only difference between before and now is the addition of ExtraParam and the corresponding references throughout for the API, and the DB getting the identically named column.

I tried adding the parameter and deploying it to Azure and making the POST request as normal, starting and stopping the app service, deploying the API while the app service was stopped and starting it again, and restarting the app service. I don't know how much I could try changing up what I'm doing, I'm trying to do exactly the same as before, but with an extra parameter getting requested from the DB.

I can also confirm that the DB contains the ExtraParam column, and that it contains values against the existing data rows, as viewed using the Azure Portal's DB Query Editor.

SGBCLDS
  • 1
  • 2
  • Can you run it successfully in your local ? I mean maybe the default value in this line not meet the condition, so set value in this line `returnedUser.ExtraParam = "placeholder"` failed. Pls add a log or debug it. We need more details. – Jason Pan Apr 01 '22 at 02:07
  • @JasonPan the "default" value has to be a string that isn't empty, because of the DB rules for the column, and it is _definitely_ populated in the DB; that check was to see how it recognises the ExtraParam attribute on the User class - if there was, for some reason, no value, it should be given a placeholder. The ideal situation is that the API correctly retrieves the ExtraParam value as set in the DB, e.g. "ThisIsAnExtraParamValue" and uses that, but it seemingly doesn't have any. The fact that UserId and PasswordHash/Salt are retrieved makes it confusing that the ExtraParam isn't, at all. – SGBCLDS Apr 01 '22 at 09:34
  • What I am saying is that maybe there have a string value in `returnedUser.ExtraParam` , so this judgment statement will not be executed. – Jason Pan Apr 01 '22 at 09:38

1 Answers1

0

I've resolved the issue, partially because of posting this question and sanitising the code for public discussion.

In the Login Controller, in my development code the request for the user to be returned was subsequently ignored, passing through the user request details which had a null ExtraParam, not the returned user which had the ExtraParam populated.

The moral of the story is to confirm which objects are being used at which points in the code, or have one object that is passed into, updated by, then returned from functions to maintain consistency.

SGBCLDS
  • 1
  • 2