How to access JWT User.Claims in class library

Viewed 3582

I've structured my project using DDD, it looks like this:

| CompanyName.API
  | MyControllerThatRequiresJwtToken << Entry point
| CompanyName.Application
| CompanyName.Data
  | EfCoreContext << I would like to get the claims here
| CompanyName.Domain
| CompanyName.IoC
| CompanyName.Test
| CompanyName.UI

I am using Z.EntityFramework.Plus.Audit.EFCore to audit all data changes. I added it to the CompanyName.Data project as this is where my EF Context lives.

Problem is: all requests in the API require a JWT token. I'd like to set the username of the person sending the request in the Audit object that will be saved into the database, however I don't have access to the HttpContext in my data layer.

What would be the best approach to get this information? Injecting IHttpContextAccessor into the data layer perhaps? It doesn't sound like a good plan to make the data layer "Http dependent".

UPDATE

I am not sure how I'd pass it from the Controller to the context. I believe it would need to be injected somehow.

Snippet of EfCoreContext.cs

public override int SaveChanges()
{
   var audit = new Audit();
   audit.CreatedBy = "JWT Username"; // << I'd need it here

   audit.PreSaveChanges(this);
   var rowAffecteds = base.SaveChanges();
   audit.PostSaveChanges();

   if (audit.Configuration.AutoSavePreAction != null)
   {
      audit.Configuration.AutoSavePreAction(this, audit);
      base.SaveChanges();
   }

   return rowAffecteds;
}
4 Answers

Create an interface called IApplicationUser for instance. Give it readonly properties you need like id, name and what not.

Create an implementation of it

public class ApplicationUser : IApplicationUser
{
   private readonly IHttpContextAccessor httpContextAccessor;

   public ApplicationUser(IHttpContextAccessor httpContextAccessor)
   {
      this.httpConntextAccessor = httpContextAccessor;
   }

   public Guid Id => this.GetUserId();

   public string Name => this.GetUserName();

   private Guid GetUserId()
   {
       var subject = this.httpContextAccessor.HttpContext
                         .User.Identity.Claims
                         .FirstOrDefault(claim => claim.Type == JwtClaimTypes.Subject);

       return Guid.TryParse(subject, out var id) ? id : Guid.Empty;
   }

   private Guid GetUserId()
   {
       return this.httpContextAccessor.HttpContext
                         .User.Identity.Claims
                         .FirstOrDefault(claim => claim.Type == JwtClaimTypes.PreferredUserName);
   }
}

Now register that with your DI-Container. For default MS IOC:

services.AddScoped<IApplicationUser, ApplicationUser>();

Inject IApplicationUser where ever you need and use it to get user-information.

Edit: IHttpContextAccessor must be registered. If it isn't the case, do it like that as well

services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();

Edit 2: Just to clarify. This is not meant to be used in a repository or however you would like to call it. Rethink your logic so you can pass that information to your entity before saving data.

I faced something like this situation. My solution steps like that:

1 - Get user information at controller and give that information to your dto (request) object.

I write extensions for getting user id:

public static string GetUserId(this HttpContext httpContext)
{
    return httpContext?.User?.Claims?.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}

Request object:

public class CreateMenuRequest
{
    public string MenuName { get; set; }

    [JsonIgnore]
    public string UpdatedBy { get; set; }
}

2- Set user information into request object

Controller :

[HttpPost, Route("")]
public IActionResult CreateMenu([FromBody] CreateMenuRequest createMenuRequest)
{
    if (createMenuRequest != null)
    {
        createMenuRequest.UpdatedBy = HttpContext.GetUserId();
    }

    CreateMenuResponse createMenuResponse = _menuService.CreateMenu(createMenuRequest);
    return StatusCode(HttpStatusCode.Created.ToInt(), createMenuResponse);
}

3 - In service layer, after validation and other business requirements, I map request to entity object. Entity object like that:

public class Menu : IAudit, ISoftDeletable
{
    public long Id { get; set; }
    ..........

    public string UpdatedBy { get; set; }
    public DateTime UpdateDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime CreateDate { get; set; }

    public bool IsDeleted { get; set; }
}

4 - override SaveChanges for editing UpdateDate and CreatedDate, also if item is added, Updatedby information set into CreatedBy field.

public override int SaveChanges()
{
   ChangeTracker.DetectChanges();

   IEnumerable<EntityEntry> deletedEntities = ChangeTracker.Entries()
                                                           .Where(t => t.State == EntityState.Deleted && t.Entity is ISof

   foreach (EntityEntry deletedEntity in deletedEntities)
   {
       if (!(deletedEntity.Entity is ISoftDeletable item)) continue;
       item.IsDeleted = true;
       deletedEntity.State = EntityState.Modified;
   }

   IEnumerable<object> addedEntities = ChangeTracker.Entries()
                                                    .Where(t => t.State == EntityState.Added && t.Entity is IAudit)
                                                    .Select(t => t.Entity);
   IEnumerable<object> modifiedEntities = ChangeTracker.Entries()
                                                       .Where(t => t.State == EntityState.Modified && t.Entity is IAudit)
                                                       .Select(t => t.Entity);

   DateTime now = DateTime.UtcNow;

   Parallel.ForEach(addedEntities, o =>
                                   {
                                       if (!(o is IAudit item))
                                           return;
                                       item.CreateDate = now;
                                       item.UpdateDate = now;
                                       item.CreatedBy = item.UpdatedBy;
                                   });

   Parallel.ForEach(modifiedEntities, o =>
                                      {
                                          if (!(o is IAudit item))
                                              return;
                                          item.UpdateDate = now;
                                      });

   return base.SaveChanges();
}

Instead of Passing IHttpContextAccessor, Extract the username in Controller and then pass it to the methods that would need it. I have created an extension method like this,

public static class UserResolverService
{
    public static Guid GetUserId(this ControllerBase controller)
    {
        var result = controller.User.FindFirstValue(ClaimTypes.NameIdentifier);
        return Guid.Parse(result);
    }
}

Then in your service method calls,

 entity= await DomainServices.CreateSomething(this.GetUserId(), dtoObject);

This way your services don't depend on HttpContext Directly as that exposes more things than required. However, there is an overhead of passing username everywhere.

Another option is to create a service that depends on IHttpContext accessor and Expose a GetUserMethod. Then let other services depend on this one to get the current user. This way only one of your business layer service will be coupled with IHttpContextAccessor. Best Candidate will be your UserProfile Service.

Create an abstraction of the data you require and then inject into DbContext as a service with DI.

// interface
public interface IAppPrincipal
{
    string Name { get; }
}

// concrete class
public class AppPrincipal : IAppPrincipal
{
    public AppPrincipal(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

// db context
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options, IAppPrincipal principal = null)
    {
        Principal = principal;
    }

    public IAppPrincipal Principal { get; }

    public override int SaveChanges()
    {
        var audit = new Audit();
        audit.CreatedBy = Principal?.Name;

        ...
    }
}

// service registration in API or MVC app
services.AddScoped<IAppPrincipal>(provider => 
{
    var user = provider.GetService<IHttpContextAccessor>()?.HttpContext?.User;

    return new AppPrincipal(user?.Identity?.Name);
});
Related