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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions