-
Notifications
You must be signed in to change notification settings - Fork 9
Description
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;
}
}
}
}
}