0

For an ASP.NET WebAPI I'm trying to establish two authentication schemes (for JWTs) to protect different resources, since I cannot use a single authentication (for silly reasons, but I cannot do anything about it right now). Theoretically allowing two different schemes should be relatively straight forward, but for some reason it is not and I do not know what else I could try to make it work (basically, I'm following this guide). My approach was to call AddJwtBearer twice in my setup with two different schemes

builder.Services.AddAuthentication()
    .AddJwtBearer(
        options =>
            {
                // ...
            })
    .AddJwtBearer(
        AuthenticationDefaults.CustomScheme, 
        options =>
            {
                options.Challenge = AuthenticationDefaults.CustomScheme;
                // ...
            });

Furthermore I added

builder.Services.AddAuthorization(
    options =>
        {
            var customPolicy = new AuthorizationPolicyBuilder(AuthenticationDefaults.CustomScheme)
                .RequireAuthenticatedUser()
                .Build();

            var azureAdPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser()
                .Build();
            
            options.AddPolicy(AuthenticationDefaults.CustomScheme, customPolicy);
            options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, azureAdPolicy);

            options.DefaultPolicy = customPolicy;
        });

to my setup and

[Authorize(AuthenticationSchemes = AuthenticationDefaults.CustomScheme)]

to my controller method.

Anyway, authorization with CustomScheme does not work. The scheme/policy seems to be functional (theoretical), for if I pass the Authorization header with my custom scheme, the WWW-Authenticate header matches my type, but I always get a 401 Unauthorized. When I add

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 

to any controller method and pass a valid token for the configuration, I get a 200 OK. I've checked the configuration for validating the tokens two and three times, but it seems okay. To me, the implementation seems broken, but then online people claim that something very similar worked out for them. Am I missing something?

Paul Kertscher
  • 9,416
  • 5
  • 32
  • 57
  • 1
    did you tried giving policy instead of AuthenticationSchemes as mentioned in the link ? – CodingMytra Sep 06 '22 at 09:08
  • @CodingMytra Yeah, I have given it a shot. – Paul Kertscher Sep 06 '22 at 10:26
  • as I understand both are jwt based auth, then why to go for this pain of writing this much code. if you remove `options.Challenge = AuthenticationDefaults.CustomScheme;` line from your code then it will work fine and without any additional code. – CodingMytra Sep 06 '22 at 11:13

1 Answers1

0

Obviously - at least as far as I can tell - the documentation is wrong. Or there is a bug, which renders the documentation effectively wrong. The code the JwtBearerHandler uses to obtain the token from the Authorization header is


if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
    token = authorization.Substring("Bearer ".Length).Trim();
}

// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
    return AuthenticateResult.NoResult(); 
}

So obviously it does not take a different authentication scheme into account, which seems quite a bit odd, for the AddJwtBearer extension method does allow a custom scheme to be passed (see my original code or the linked documentation). Anyway, if the OnMessageReceived event resolves a token and assigns the MessageReceivedContext.Token, the JwtBearerHandler will use this token:

// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);

// event can set the token
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
    return messageReceivedContext.Result;
}

// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;

if (string.IsNullOrEmpty(token))
// ...

So I ended up installing a handler that looks for my custom authentication scheme and assigns the token if the scheme was used:

.AddJwtBearer(
AuthenticationDefaults.CustomScheme,
options =>
    {
        options.Challenge = AuthenticationDefaults.CustomScheme;

        /* options */

        options.Events = new JwtBearerEvents
                             {
                                 OnMessageReceived = context =>
                                     {
                                         if (TryGetToken(
                                                 context.Request,
                                                 AuthenticationDefaults.CustomScheme,
                                                 out string? token))
                                         {
                                             context.Token = token;
                                         }

                                         return Task.CompletedTask;
                                     }
                             };


    });
    
// ...
    
bool TryGetToken(HttpRequest httpRequest, string authenticationScheme, out string? token)
{
    var authorization = httpRequest.Headers?.Authorization;

    if (authorization is { Count: >= 1 } 
        && authorization.Value[0].StartsWith(authenticationScheme))
    {
        token = authorization.Value[0].Substring(authenticationScheme.Length)
            .Trim();
        return true;
    }

    token = null;
    return false;
}

(From the JwtBearerHandler code.)

Paul Kertscher
  • 9,416
  • 5
  • 32
  • 57