6

I am using the Cookie Middleware to authenticate the user. I have been following this official tutorial.

Inside my Startup class, an excerpt from my Configure method looks like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
  // ...

  // Cookie-based Authentication
  app.UseCookieAuthentication(new CookieAuthenticationOptions()
  {
    AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,        
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    Events = new CustomCookieAuthenticationEvents(app),
  });

  // ...
}

The CustomCookieAuthenticationEvents class is defined as follows:

public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
  private IApplicationBuilder _app;
  private IMyService _myService = null;
  private IMyService MyService
  {
    get
    {
      if(_myService != null)
      {
        return _myService;
      } else
      {
        return _myService = (IMyService) _app.ApplicationServices.GetService(typeof(IMyService));
      }
    }
  }

  public CustomCookieAuthenticationEvents(IApplicationBuilder app)
  {
    _app = app;
  }

  public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
  {
    string sessionToken = context.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
    LogonSession response = null;

    var response = await MyService.CheckSession(sessionToken);

    if (response == null)
    {
      context.RejectPrincipal();
      await context.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    }
  }
}

Since the dependency injection is not available at Startup.Configure (the services are not even registered at that point), I made a bit of a workaround:

  1. Pass IApplicationBuilder service to the CustomCookieAuthenticationEvents class
  2. Fetch IMyService upon first request inside a read-only property (singleton pattern)

tl;dr

My solution works, but it's ugly. There is no dependency injection involved, as it is not possible at that time.

The essence of the problem is that I must instantiate CustomCookieAuthenticationEvents. As far as I have read the source code, there is no way around this, because the UseCookieAuthentication throws an exception if I omit the options parameter.

Any suggestion how can one make my current solution nicer?

alesc
  • 2,776
  • 3
  • 27
  • 45

2 Answers2

13

Startup.ConfigureServices() is called before Startup.Configure() (see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup for more information). So Dependency Injection is available at that time ;)
As a consequence, you can resolve your dependence in your configure method like this:

app.ApplicationServices.GetRequiredService<CustomCookieAuthenticationEvents>()
arnaudauroux
  • 740
  • 6
  • 10
  • You're right. I don't know why I thought the `Configure` is called before `ConfigureServices`. But I am glad it isn't. – alesc Apr 14 '17 at 12:33
  • 7
    this approach uses the service locator pattern, you don't need to do that, you can simply add what you need into the Startup.Configure method signature and it will be injected into the method – Joe Audette Apr 14 '17 at 12:53
  • You are right, I have just shown how to get a dependence from the Configure method if you really need it ;) – arnaudauroux Apr 14 '17 at 12:55
4

You should be really careful when you resolve services inside middleware. Your current approach (and the one suggested by @arnaudauroux) can result in difficulties when you use/need/require scoped services (i.e. usage of DbContext).

Resolving via app.ApplicationServices results in static (singleton) services, when the service is registered as scoped (transient are resolved per call, so they are not affected). It would be better to resolve your service during the request from HttpContext inside ValidatePrincipal method.

public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
    string sessionToken = context.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
    LogonSession response = null;

    var myService = context.HttpContext.RequestServices.GetService<IMyService >();
    var response = await myService.CheckSession(sessionToken);

    if (response == null)
    {
        context.RejectPrincipal();
        await context.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    }
}

With this approach you don't need to pass any dependencies inside your CustomCookieAuthenticationEvents class at all. HttpContext.RequiredServices is made specifically for such classes (any other can be solved via constructor injection, but not middleware and http context related pipeline, as there is no other otherway to correctly resolve scoped services in middlewares - Middleware instance is static and only instantiated once per request)

This way you won't have lifetime issues with your scoped services. When you resolve transient services, they will be disposed at the end of request. Whereas transient services resolved via app.ApplicationServices will be resolved at some point in future after the request is finished and when garbage collection triggers (means: your resources will be freed at the earliest possible moment, which is when the request ends).

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • The `CustomCookieAuthenticationEvents` is a singleton, so it's OK if I pass it at constructor time via dependency injection. But will probably use your approach for scoped variables. – alesc Apr 14 '17 at 12:34
  • 1
    @alesc: Yes, `CustomCookieAuthenticationEvents` is a singleton, but `IMyService` **and it's dependencies** may not be singletons. That's the point I was going for. That's why you should resolve requesttime dependencies from `HttpContext.RequestServices` – Tseng Apr 14 '17 at 12:45
  • One issue you may encounter is that scoped services become singletons by this usage,because when it's first resolved by `app.ApplicationServices` it gets stored in the parent container (which has lifetime that equals the lifetime of the application, since the parent container is only created and disposed when the app starts).When you now resolve that service via `HttpRequest.RequestServices` you work on child container and the child looks in the parent containers lookup table. If it finds a scoped/singleton there,it uses it instead of creating it.As such it doesn't dispose it at end of request – Tseng Apr 14 '17 at 13:00
  • A question then: I need to seed the database inside Startup, or in other words, when I start the app. Is there a way to do this without explicitly calling the service? The logic is too big to just hardcode it into another method in startup so I'm calling an existing service/method to do it. Ah, and to make things clear: I'm fetching the data from remote server, I can't use the DbSeeder, since i don't have the data – Cubelaster Feb 01 '19 at 09:08
  • 1
    @Cubelaster: Common/recommended Seeding pattern for EF Core can be seen in [this answer](https://stackoverflow.com/a/38704828/455493) rather than inside Startup – Tseng Feb 01 '19 at 09:12
  • @Tseng Thank you for your answer. It indeed helped with my problem. However, I still needed to take from ServiceProvider. Now, reading the answer you provided about scoped becoming sinlgetons, how is this diffferent? is it because Program.cs is disposed or something after starting, oposed to Startup,whose container continues running? – Cubelaster Feb 01 '19 at 12:29
  • 1
    @Cubelaster: The scoped container is resolved in program.cs (or rather the extension method), then the service from it. Once done, the scope (and all services that were initialized by it) get disposed. Notice the line with `using (var scope = webhost.Services.GetService().CreateScope()) { ... }` – Tseng Feb 01 '19 at 12:32
  • @Tseng ah, yes, indeed. This was so obvious, yet I failed to notice. Thank you again :) – Cubelaster Feb 01 '19 at 20:00