6

I need to implement custom "authentication" for my company. I say that in quotes because the user technically gets authenticated before it hits the application and if so, the userId will exist in request headers.

What I need to do is figure out a way to query the database and get additional user information based on that Id, and set the HttpContext.User object so that it can be used easily within the application.

The route I am taking now involves using Cookie Authentication without ASP.NET Core Identity. I have combined that idea with custom middleware that will query the database for the user, populate Claims from the db fields, and use the context.SignInAsync to create the cookie. I place this middleware before app.UseAuthentication(). The problem is upon first request the .User object is not set, because it seems the SignIn method only creates the cookie but doesn't set the .User object. The Authentication middleware doesn't yet see the cookie because it does not exist on first request.

Could anyone provide any ideas? Maybe I'm going about it wrong, or this technique is fine but I'm missing what I need to make it work.

in Startup.cs:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();


        services.AddAuthentication("MyAuthenticationCookie")
           .AddCookie("MyAuthenticationCookie");
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseMyUserMiddleware();

        app.UseAuthentication();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

custom Middleware:

    public class MyUserMiddleware
{
    private readonly RequestDelegate _next;

    public MyUserMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context)
    {
        // Sign in user if this auth cookie doesn't exist
        if (context.Request.Cookies[".AspNetCore.MyAuthenticationCookie"] == null)
        {
            // Get user from db - not done

            // Set claims from user object - put in dummy test name for now
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, "TEST"),

            };

            var claimsIdentity = new ClaimsIdentity(claims, "MyAuthenticationCookie");

            context.SignInAsync("MyAuthenticationCookie", new ClaimsPrincipal(claimsIdentity));
        }

        return this._next(context);
    }
}

public static class MyUserMiddlewareExtensions
{
    public static IApplicationBuilder UseMyUserMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyUserMiddleware>();
    }
}
Royi Namir
  • 144,742
  • 138
  • 468
  • 792
user1560457
  • 93
  • 4
  • 9
  • "the user technically gets authenticated before it hits the application"? That sounds like an intranet application. Have you tried Windows Authentication? "the userId will exist in request headers"? So there will be just a custom request header property "x-user-id", 2 in the request header? whooo that's something. – David Liang Oct 25 '17 at 18:07
  • I don't get to decide how that part is done. – user1560457 Oct 25 '17 at 18:13
  • Try replacing your `MyAuthenticationCookie` with `CookieAuthenticationDefaults.AuthenticationScheme` in the `services.AddAuthentication()` and `context.SignInAsync()`. And then set the name of the cookie in `.AddCookie(options => { options.Cookie.Name = "xxxxxx"; })`. – David Liang Oct 25 '17 at 18:26
  • Does not change the behavior it seems. – user1560457 Oct 25 '17 at 18:38

1 Answers1

8

Short answer: you should use a custom AuthorizationHandler to authenticate & retrieve claims.

Long answer: With ASP.NET CORE you should walk away from authentication middleware. Instead you should use an AuthenticationHandler microsoft

To create a custom Authentication handler, you will need to create a new class inheriting from AuthenticationHandler<TOption>. TOption is a simple class used to pass parameters to your handler.

public class TecMobileOptions : AuthenticationSchemeOptions
{ 
   // Add your options here
}

public class MyNewHandler : AuthenticationHandler<MyOptions>
{
    private readonly ILogger _logger;

    public TecMobileHandler(
        IOptionsMonitor<MyOptions> options,
        ILoggerFactory loggerFactory,
        UrlEncoder encoder,
        ISystemClock clock) : base(options, loggerFactory, encoder, clock)
    {
       // Inject here your DbContext
        _logger = loggerFactory.CreateLogger("name...");
    }
}

Then you will need to implement the HandleAuthenticateAsync method. It will be called by the Auth middleware when necessary:

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authorization = Request.Headers["UserId"].ToString();
        (...)
        return AuthenticateResult.Success(
            new AuthenticationTicket(**your claims**, Scheme.Name));
    }

Claims returned by this method will be available through the HttpContext.User object.

Once that done, you will need to add your scheme to the authentication builder.

services.AddAuthentication()
   .AddCookie("MyAuthenticationCookie");
   .AddScheme<MyOptions, MyHandler>("MyHandlerName");

Don't forget to add in Startup.cs / Configure methods the following code line

 app.UseAuthentication();

Finally, you will need to add the Authorize attribute on all classes/methods you want to secure

[Authorize(AuthenticationSchemes = "MyHandlerName")]
public class MyControllerController : BaseController
{  }

OR

[Authorize(AuthenticationSchemes = "MyHandlerName")]
public IActionResult MyMethod()
{  }

EDIT: Here the solution covering the full login process. Let's consider you define two authentication schemes - Cookie based is called CookieScheme - AutoSignInScheme: create the corresponding handler following the steps above

[Authorize(AuthenticationSchemes = "CookieScheme")]
public class SecuredController : Controller
{
     (...)
}

Then you will need to add the AccountController

public class AccountController : Controller
{
    [HttpGet]
    [Authorize(AuthenticationSchemes = "AutoSignInScheme")]
    public async Task<IActionResult> AutoSignIn(string returnUrl)
    {
        await HttpContext.SignInAsync(
           "CookieScheme",
           new ClaimsPrincipal(new ClaimsIdentity(User.Claims, "CookieScheme")));
        return Redirect(returnUrl);
    }
 }

In your Startup.cs, add the following lines:

       services.AddAuthentication()
            .AddCookie("CookieScheme", opts =>
            {
                opts.LoginPath = new PathString("/account/AutoSignIn");
                opts.LogoutPath = ** TODO IF REQUIRED **
                opts.Cookie.Expiration = TimeSpan.FromHours(8);
            })
            .AddScheme<MyOptions, MyHandler>("AutoSignInScheme");

When the users tries to access your site, he is redirected to the autosignin controller. Claims are then retrieved from your DB, stored in a cookie and the user is finally redirected to his initial destination!.

Seb

  • I appreciate your answer. It doesn't seem to be creating a cookie after first request, which is a concern because without being able to keep user information in that I'd have to call the database every time from the custom AuthenticationHandler (after getting the Id from the header). – user1560457 Oct 26 '17 at 19:02
  • Sorry I was not exhaustive! This creates the claims you can then save to a cookie. Typically if the user is not logged in, you redirect him to a page where you get the claims, creates the cookie and redirect him back to where ever you need! My post was showing you how to get the claims! Sorry for the mistundertanding –  Oct 27 '17 at 11:29
  • Is there way to set cookie in middleware? No need for login action.. user does not need to manually do anything. – user1560457 Oct 27 '17 at 18:36
  • You should walk away from authentication cookie. I have updated my answer to include the full flow. Good luck –  Oct 28 '17 at 10:14
  • Sorry, I said middleware but I meant handler. Do you think it is feasible to have everything contained in the custom handler and not have to incorporate accountcontroller and redirects? Could we just extend CookieAuthenticationHandler? – user1560457 Oct 28 '17 at 14:12
  • You have access to the HttpContext so in theory you could do something like HttpContext.SignInAsync(...). –  Oct 30 '17 at 00:34