91

With Core 1.1 followed @blowdart's advice and implemented a custom middleware:

https://stackoverflow.com/a/31465227/29821

It worked like this:

  1. Middleware ran. Picked up a token from the request headers.
  2. Verified the token and if valid built an identity (ClaimsIdentity) that contained multiple claims which then it added via HttpContext.User.AddIdentity();
  3. In ConfigureServices using services.AddAuthorization I've added a policy to require the claim that is provided by the middleware.
  4. In the controllers/actions I would then use [Authorize(Roles = "some role that the middleware added")]

This somewhat works with 2.0, except that if the token is not valid (step 2 above) and the claim is never added I get "No authenticationScheme was specified, and there was no DefaultChallengeScheme found."

So now I'm reading that auth changed in 2.0:

https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x

What's the right path for me to do the same thing in ASP.NET Core 2.0? I don't see an example to do truly custom authentication.

abatishchev
  • 92,232
  • 78
  • 284
  • 421
pbz
  • 8,185
  • 13
  • 52
  • 68
  • Try out this link, even though it says 2 schemes but it would give u heads up on Authentication https://wildermuth.com/2017/08/19/Two-AuthorizationSchemes-in-ASP-NET-Core-2 – Mithun Pattankar Aug 22 '17 at 12:23
  • could you add your code so we can take a look? I know i had issues with JWT in core2.0 - was a case of moving it around in the startup – Webezine Sep 01 '17 at 14:28

2 Answers2

201

So, after a long day of trying to solve this problem, I've finally figured out how Microsoft wants us to make custom authentication handlers for their new single-middleware setup in core 2.0.

After looking through some of the documentation on MSDN, I found a class called AuthenticationHandler<TOption> that implements the IAuthenticationHandler interface.

From there, I found an entire codebase with the existing authentication schemes located at https://github.com/aspnet/Security

Inside of one of these, it shows how Microsoft implements the JwtBearer authentication scheme. (https://github.com/aspnet/Security/tree/master/src/Microsoft.AspNetCore.Authentication.JwtBearer)

I copied most of that code over into a new folder, and cleared out all the things having to do with JwtBearer.

In the JwtBearerHandler class (which extends AuthenticationHandler<>), there's an override for Task<AuthenticateResult> HandleAuthenticateAsync()

I added in our old middleware for setting up claims through a custom token server, and was still encountering some issues with permissions, just spitting out a 200 OK instead of a 401 Unauthorized when a token was invalid and no claims were set up.

I realized that I had overridden Task HandleChallengeAsync(AuthenticationProperties properties) which for whatever reason is used to set permissions via [Authorize(Roles="")] in a controller.

After removing this override, the code had worked, and had successfully thrown a 401 when the permissions didn't match up.

The main takeaway from this is that now you can't use a custom middleware, you have to implement it via AuthenticationHandler<> and you have to set the DefaultAuthenticateScheme and DefaultChallengeScheme when using services.AddAuthentication(...).

Here's an example of what this should all look like:

In Startup.cs / ConfigureServices() add:

services.AddAuthentication(options =>
{
    // the scheme name has to match the value we're going to use in AuthenticationBuilder.AddScheme(...)
    options.DefaultAuthenticateScheme = "Custom Scheme";
    options.DefaultChallengeScheme = "Custom Scheme";
})
.AddCustomAuth(o => { });

In Startup.cs / Configure() add:

app.UseAuthentication();

Create a new file CustomAuthExtensions.cs

public static class CustomAuthExtensions
{
    public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
    {
        return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>("Custom Scheme", "Custom Auth", configureOptions);
    }
}

Create a new file CustomAuthOptions.cs

public class CustomAuthOptions: AuthenticationSchemeOptions
{
    public CustomAuthOptions()
    {

    }
}

Create a new file CustomAuthHandler.cs

internal class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
    public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
        // store custom services here...
    }
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // build the claims and put them in "Context"; you need to import the Microsoft.AspNetCore.Authentication package
        return AuthenticateResult.NoResult();
    }
}
Jonathan B.
  • 1,834
  • 14
  • 17
Zac
  • 2,042
  • 1
  • 7
  • 16
  • 1
    great post, but I have some problems compiling your code. The CustomAuthOptions and AuthenticateResult types are missing. Could you post those? – alexb Sep 11 '17 at 18:35
  • CustomAuthOptions is a simple empty class that inherits from AuthenticationSchemeOptions. AuthenticateResult is defined in Microsoft.AspNetCore.Authentication (you need to import that package). – pbz Sep 11 '17 at 23:50
  • @alexb I've edited the original answer to address your points. – pbz Sep 11 '17 at 23:58
  • 1
    This is a great answer! Just wondering `// build the claims and put them in "Context";` I thought you're supposed to use `return AuthenticateResult.Success(...)`? – MEMark Sep 15 '17 at 19:26
  • @MEMark good question... I don't know the implications for returning .Success() vs .NoResult(). We're using Context.User.AddIdentity() with a ClaimsIdentity that we build/set and it works, [Authorize] is checked and endpoints without are anonymous. With 2.0 I see that there's a new AuthenticationTicket class; would be nice to know the pros/cons of each. – pbz Sep 16 '17 at 17:40
  • 8
    Are you willing to share your conclusions in code on a Github repo? – CSharper Sep 21 '17 at 22:59
  • 1
    Epic answer. Where does one set the login/no access page? – Ian Warburton Sep 28 '17 at 12:58
  • "// store custom services here..." it's not using my dependency container returned from `ConfigureServices`. – Ian Warburton Sep 28 '17 at 13:33
  • @IanWarburton did you use the constructor to request the service? (CustomAuthHandler)... not sure why that wouldn't work... – pbz Sep 29 '17 at 15:43
  • @IanWarburton we use this authentication method for our SPA website; there's no login page. We just have a public login endpoint that handles the login and sets/returns the token. Are you maybe looking for the cookie auth? – pbz Sep 29 '17 at 15:46
  • @pbz I handled `HandleChallengeAsync` to redirect on a missing cookie, and the login page is a property added to the options. Yes, the DI not working is strange - although I'm using a custom provider. It works if the types are added to the built in service. – Ian Warburton Sep 29 '17 at 15:50
  • This was awesome, thanks! I managed to use your ideas to make a custom handler to use during our integration tests with TestServer. We use WindowsAuth. – jpgrassi Dec 05 '17 at 14:01
  • 2
    Could you please explain `DefaultAuthenticateScheme` and `DefaultChallengeScheme`? I do not understand why they are both used? and what is the differences between them. – Mohammed Noureldin Dec 23 '17 at 18:32
  • 11
    +1 for "From there, I found an entire codebase with the existing authentication schemes located at https://github.com/aspnet/Security." Just look at how the ASP.NET team does it as you follow along with this (really excellent) answer. Did any of us ever think that one day we'd be asking questions of MS code and practices and the answer would be, "just take a look at their codebase?" – Marc L. Jan 16 '18 at 18:57
  • 3
    For others coming in later, your `AuthExtension` needs to be inside the `Microsoft.Extensions.DependencyInjection` namespace. See this example: https://github.com/aspnet/Security/blob/rel/2.0.0/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs#L8 – Garry Polley Feb 12 '18 at 20:57
  • It's very strict on naming, I have bumped my head into this for a few hours writing a custom handler, it kept giving the error the topic starter posted about. Best to use a const string in CustomAuthDefaults.AuthenticationScheme and use it in the extension method and AddAuthentication within your startup. – rfcdejong May 18 '18 at 13:51
  • This answer doesn't explain how to actually get at the request to authenticate / authorize (HandleAuthenticateAsync has no parameters). Are you all using HttpContextAccessor via DI to get at the request? – Justin Feb 22 '19 at 17:30
  • It also doesn't explain how you would apply this handler only to some controllers and actions and not others. Some resources are public and don't need to be authenticated.... – Justin Feb 22 '19 at 17:35
  • 1
    @Justin you can access the request via the global variable "Context" defined in this particular, well, context. In our example, we ended up checking the request context path for the prefix "/api" and from there required authentication only on said URLs. If you need something a bit more specific, you may have to play around with custom attributes. – Zac Feb 25 '19 at 04:10
  • This just gives me a blank page while getting a 401 return code. Still better than the error I got before I guess: "No authenticationScheme was specified, and there was no DefaultChallengeScheme found." – BluE Jun 28 '19 at 07:35
  • 1
    Thank you SO MUCH! This is exactly what I needed. I have to manually do authentication because of how this stupid auth server is setup. – John Edwards Aug 31 '20 at 22:43
4

There are considerable changes in Identity from Core 1.x to Core 2.0 as the article you reference points out. The major change is getting away from the middleware approach and using dependency injection to configure custom services. This provides a lot more flexibility in customizing Identity for more complex implementations. So you want to get away from the middleware approach you mention above and move towards services. Follow the migration steps in the referenced article to achieve this goal. Start by replacing app.UseIdentity with app.UseAuthentication. UseIdentity is depreciated and will not be supported in future versions. For a complete example of how to insert a custom claims transformation and perform authorization on the claim view this blog post.

Kevin Junghans
  • 17,102
  • 4
  • 42
  • 60