Skip to content

Using ConfigureFunctionsWebApplication and AddHttpContextAccessor but injected IHttpContextAccessor _httpContextAccessor is always null #81

@silverleafsolutions

Description

@silverleafsolutions

I integrated the DarkLoop.Azure.Functions.Authorization.Isolated Nuget package into my .NET 9 isolated Function App project, and it works great overall. But I had a CurrentUserService I was using before that worked fine, but once I implemented DarkLoop, it seems to have stopped working. In the constructor, IHttpContextAccessor httpContextAccessor is always null, no matter what. Here is that class:

namespace MyFunctionApp.Core.Services
{
    public class CurrentUserService : ICurrentUserService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public CurrentUserService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string? UserId
        {
            get
            {
                return _httpContextAccessor.HttpContext?.User?.FindFirstValue(JwtRegisteredClaimNames.Sub);
            }
        }

        public string? Username
        {
            get
            {
                return _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.Name);
            }
        }
    }
}

I've tried moving the statements in my Program.cs around, but nothing changes the fact that the httpContextAccessor is always null. Is there any way to continue using the CurrentUserService, or am I going to have to completely refactor it or use something else entirely?

Here is my Program.cs:

using DarkLoop.Azure.Functions.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Worker;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = Host
    .CreateDefaultBuilder(args)
    .ConfigureFunctionsWebApplication(builder =>
    {
        builder.UseFunctionsAuthorization();
        builder.Services.AddHttpContextAccessor();
    })
    .ConfigureServices((ctx, services) =>
    {
        var config = ctx.Configuration;

        // Key Vault
        services.AddSingleton<IKeyVaultService, KeyVaultService>();

        // Application services & HTTP client
        services.AddApplicationServices();

        // Identity
        services.AddIdentityCore<ApplicationUser>(opts =>
        {
            opts.Password.RequireDigit = true;
            opts.Password.RequireLowercase = true;
            opts.Password.RequireUppercase = true;
            opts.Password.RequireNonAlphanumeric = false;
            opts.Password.RequiredLength = 8;
        })
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // JWT Authentication & Authorization Policies
        services
            .AddFunctionsAuthentication(JwtFunctionsBearerDefaults.AuthenticationScheme)
            .AddJwtFunctionsBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = true;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                };
            });

        services.AddFunctionsAuthorization(opts =>
        {
            opts.ConfigurePolicies();
        });

        // Current user & EF interceptor
        services.AddScoped<ICurrentUserService, CurrentUserService>();
        services.AddScoped<AuditableEntityInterceptor>();

        // Build the service provider.
        var sp = services.BuildServiceProvider();
        var keyVaultService = sp.GetRequiredService<IKeyVaultService>();

        // Resolve JWT settings from KeyVault
        var jwtSettingsJson = keyVaultService
            .GetSecretAsync(Constants.KEYVAULT_KEY_JWT_TOKEN_SETTINGS_JSON)
            .GetAwaiter().GetResult();
        var jwtSettings = System.Text.Json.JsonSerializer.Deserialize<JwtTokenSettings>(jwtSettingsJson ?? "", JsonHelper.GetDefaultJsonSerializerOptions())!;

        // Resolve JWT settings from KeyVault.
        services.PostConfigure<JwtBearerOptions>(
            JwtFunctionsBearerDefaults.AuthenticationScheme,
            options =>
            {
                options.TokenValidationParameters.IssuerSigningKey =
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey));
                options.TokenValidationParameters.ValidAudience = jwtSettings.Audience;
                options.TokenValidationParameters.ValidIssuer = jwtSettings.Issuer;
                options.TokenValidationParameters.ValidateIssuerSigningKey = true;
                options.TokenValidationParameters.ValidateIssuer = true;
                options.TokenValidationParameters.ValidateAudience = true;
                options.TokenValidationParameters.ValidateLifetime = true;
                options.TokenValidationParameters.ClockSkew = TimeSpan.Zero;
            });
    });

var host = builder.Build();
host.Run();

Lastly, I have the AuditableEntityInterceptor class that needs CurrentUserService to get the current user's ID, but it obviously doesn't work right now either:

namespace MyProject.Infrastructure.Data.Interceptors
{
    public class AuditableEntityInterceptor : SaveChangesInterceptor
    {
        private readonly ICurrentUserService _currentUserService;

        public AuditableEntityInterceptor(ICurrentUserService currentUserService)
        {
            _currentUserService = currentUserService;
        }

        public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
        {
            UpdateEntities(eventData.Context);
            return base.SavingChanges(eventData, result);
        }

        public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
        {
            UpdateEntities(eventData.Context);
            return base.SavingChangesAsync(eventData, result, cancellationToken);
        }

        private void UpdateEntities(DbContext? context)
        {
            if (context == null)
            {
                return;
            }

            var userId = _currentUserService.UserId;
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries().Where(e => e.Entity is BaseEntity<object>))
            {
                var entity = entry.Entity as BaseEntity<object>;
                if (entity == null)
                {
                    continue;
                }

                if (entry.State == EntityState.Added)
                {
                    entity.CreatedBy = userId ?? string.Empty;
                    entity.CreatedDate = now;
                }
                else if (entry.State == EntityState.Modified)
                {
                    entity.LastModifiedBy = userId;
                    entity.LastModifiedDate = now;
                }
                else if (entry.State == EntityState.Deleted)
                {
                    entry.State = EntityState.Modified;
                    entity.IsDeleted = true;
                    entity.DeletedBy = userId;
                    entity.DeletedDate = now;
                }
            }
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions