Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding more flexibility to Jwt[Bearer]Handler #18320

Closed
gpomykala opened this issue Jan 13, 2020 · 4 comments
Closed

Consider adding more flexibility to Jwt[Bearer]Handler #18320

gpomykala opened this issue Jan 13, 2020 · 4 comments
Labels

Comments

@gpomykala
Copy link

@gpomykala gpomykala commented Jan 13, 2020

I was looking for a way to add an alternative authentication scheme to aspnetcore app:

  • it does use non-standard keyword (please note JwtBearerHandler has 'Bearer' hardcoded and does not respect user-defined challenge keywords)
  • it does use different strategy of issuing and validating tokens

Since JwtBearerHandler insists to look for "Bearer" token regardless of keyword set in Options.Challenge I was forced to add my own JWT handler.
It might be beneficial to add more flexibility in the handler shipped with aspnetcore to enable custom auth scheme scenarios without resorting to adding custom handlers.

Steps to reproduce:

  1. Define a JWT authentication with custom Challenge keyword
services.AddAuthentication("CustomScheme")
    .AddJwtBearer(options =>
     {
         options.Challenge = "CustomScheme";              
     });
  1. call API with custom auth header
curl -X GET "https://host:port/api/action" -H "Authorization: CustomScheme [token]"

  1. Observe that challenge response wrongly indicates "CustomScheme" keyword while in fact JwtBearerHandler expects "Bearer" keyword
 www-authenticate: CustomScheme 

Possible fix:
Replace

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

with

string tokenPrefix = $"{Options.Challenge} ";
if (authorization.StartsWith(tokenPrefix, StringComparison.OrdinalIgnoreCase))
{
    token = authorization.Substring(tokenPrefix.Length).Trim();
}

Environment info:

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.1.100
 Commit:    cd82f021f4

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.14393
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.1.100\

Host (useful for support):
  Version: 3.1.0
  Commit:  65f04fb6db

.NET Core SDKs installed:
  2.1.402 [C:\Program Files\dotnet\sdk]
  2.1.602 [C:\Program Files\dotnet\sdk]
  2.1.604 [C:\Program Files\dotnet\sdk]
  2.1.701 [C:\Program Files\dotnet\sdk]
  2.1.801 [C:\Program Files\dotnet\sdk]
  2.2.202 [C:\Program Files\dotnet\sdk]
  2.2.204 [C:\Program Files\dotnet\sdk]
  2.2.301 [C:\Program Files\dotnet\sdk]
  2.2.401 [C:\Program Files\dotnet\sdk]
  3.0.100 [C:\Program Files\dotnet\sdk]
  3.1.100 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
@Kahbazi

This comment has been minimized.

Copy link
Contributor

@Kahbazi Kahbazi commented Jan 13, 2020

Hi @gpomykala . You can achieve this by handling the OnMessageReceived event and extract the token whichever you want.

// 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;

@gpomykala

This comment has been minimized.

Copy link
Author

@gpomykala gpomykala commented Jan 14, 2020

It is correct that the workaround mentioned above may be used to assign anything from the request as a token, however it comes with multiple problems:

  • the scheme known by auth middleware is still called Bearer - an attempt to reference the custom scheme name leads to runtime errors:
            services.AddAuthentication("Scheme1")
             .AddJwtBearer(options =>
             {
                 options.Challenge = "Scheme1";
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Scheme1")),
                     ValidateIssuer = false,
                     ValidateAudience = false
                 };
                 options.Events = new JwtBearerEvents
                 {
                     OnMessageReceived = context =>
                        {
                           // get custom token
                 };
             });

[Authorize(AuthenticationSchemes = "Scheme1")]
public class MyController : ControllerBase { }

Above code leads to:

InvalidOperationException: No authentication handler is registered for the scheme 'Scheme1'. The registered schemes are: Bearer. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("Scheme1",...)?

When we remove the explicit scheme name from the controller definition we get an another one:

[Authorize]
public class MyController : ControllerBase { }

InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

The only way it may work is that we pretend that we want to use "Bearer" scheme when in fact we fetch token from another scheme:

services.AddAuthentication("Bearer")
             .AddJwtBearer(options =>
             {
                 options.Challenge = "Bearer";
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
                     ValidateIssuer = false,
                     ValidateAudience = false
                 };
                 options.Events = new JwtBearerEvents
                 {
                     OnMessageReceived = context =>
                        {
                            if (context.Request.Headers.ContainsKey("Authorization"))
                            {
                                Microsoft.Extensions.Primitives.StringValues accessTokens = context.Request.Headers["Authorization"];
                                context.Token = accessTokens.First().Replace("Scheme1 ", "");
                            }
                            return Task.CompletedTask;
                        }
                 };
             });

However the workaround above is limited to one scheme only, it is not possible to register multiple authe schemes using the workaround above - the code below results in runtime exception:

services.AddAuthentication("Bearer")
             .AddJwtBearer(options =>
             {
                 options.Challenge = "Bearer";
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
                     ValidateIssuer = false,
                     ValidateAudience = false
                 };
                 options.Events = new JwtBearerEvents
                 {
                     OnMessageReceived = context =>
                        {
                            if (context.Request.Headers.ContainsKey("Authorization"))
                            {
                                Microsoft.Extensions.Primitives.StringValues accessTokens = context.Request.Headers["Authorization"];
                                context.Token = accessTokens.First().Replace("Scheme1 ", "");
                            }
                            return Task.CompletedTask;
                        }
                 };
             });

            services.AddAuthentication("Bearer")
            .AddJwtBearer(options =>
             {
                 options.Challenge = "Bearer";
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret+"2")),
                     ValidateIssuer = false,
                     ValidateAudience = false
                 };
                 options.Events = new JwtBearerEvents
                 {
                     OnMessageReceived = context =>
                     {
                         if (context.Request.Headers.ContainsKey("Authorization"))
                         {
                             Microsoft.Extensions.Primitives.StringValues accessTokens = context.Request.Headers["Authorization"];
                             context.Token = accessTokens.First().Replace("Scheme2 ", "");
                         }
                         return Task.CompletedTask;
                     }
                 };
             });

System.InvalidOperationException: 'Scheme already exists: Bearer'
@Kahbazi

This comment has been minimized.

Copy link
Contributor

@Kahbazi Kahbazi commented Jan 14, 2020

You need to set the authentication scheme with AddJwtBearer method.

services.AddAuthentication("Scheme1")
    .AddJwtBearer("Scheme1", options => ... );
@gpomykala

This comment has been minimized.

Copy link
Author

@gpomykala gpomykala commented Jan 14, 2020

Hello, it did the trick indeed. The only downside I observerved so far is that the OnMessageReceived handler is being called twice. Thanks for a piece of advice.

services.AddAuthentication("Scheme1")
    .AddJwtBearer("Scheme1", options =>
    {
        options.Challenge = "Scheme1";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
            ValidateIssuer = false,
            ValidateAudience = false
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                if (context.Request.Headers.ContainsKey("Authorization"))
                {
                    Microsoft.Extensions.Primitives.StringValues accessTokens = context.Request.Headers["Authorization"];
                    context.Token = accessTokens.First().Replace("Scheme1 ", "");
                }
                return Task.CompletedTask;
            }
        };
    });

services.AddAuthentication("Scheme2")
.AddJwtBearer("Scheme2", options =>
    {
        options.Challenge = "Scheme2";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret+"2")),
            ValidateIssuer = false,
            ValidateAudience = false
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                if (context.Request.Headers.ContainsKey("Authorization"))
                {
                    Microsoft.Extensions.Primitives.StringValues accessTokens = context.Request.Headers["Authorization"];
                    context.Token = accessTokens.First().Replace("Scheme2 ", "");
                }
                return Task.CompletedTask;
            }
        };
    });
@gpomykala gpomykala closed this Jan 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.