0

I am trying to create a custom authentication handler that will require the Bearer JWT in the body of an HTTP request, but I'd prefer not to create a whole new custom authorization. Unfortunately, the only thing I can do is read the HTTP request body, get the token from there and put it in the Authorization header of the request.

Is there a different, more efficient way to do it? All I managed is to find the default JwtBearerHandler implementation on GitHub but when I make some modifications, it can't read the principal properly.

Startup.cs:

services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = true;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        RequireExpirationTime = true,
        ClockSkew = TimeSpan.FromSeconds(30)
    };

    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = ctx =>
        {
            if (ctx.Exception.GetType() == typeof(SecurityTokenExpiredException))
            {
                ctx.Response.Headers.Add("Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
});
public class AuthHandler : JwtBearerHandler
{
    private readonly IRepositoryEvonaUser _repositoryUser;
    private OpenIdConnectConfiguration _configuration;

    public AuthHandler(IOptionsMonitor<JwtBearerOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        IDataProtectionProvider dataProtection,
        ISystemClock clock,
        IRepositoryUser repositoryUser,
        OpenIdConnectConfiguration configuration
        )
        : base(options, logger, encoder, dataProtection, clock)
    {
        _repositoryUser = repositoryUser;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        string token = null;
        try
        {
            var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);

            await Events.MessageReceived(messageReceivedContext);
            if (messageReceivedContext.Result != null)
            {
                return messageReceivedContext.Result;
            }
            token = messageReceivedContext.Token;

            if (string.IsNullOrEmpty(token))
            {
                Request.EnableBuffering();
                using (var reader = new StreamReader(Request.Body, Encoding.UTF8, true, 10, true))
                {
                    var jsonBody = reader.ReadToEnd();
                    var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
                    if (body != null)
                    {
                        token = body.Token;
                    }
                    Request.Body.Position = 0;
                }

                if (string.IsNullOrEmpty(token))
                {
                    return AuthenticateResult.NoResult();
                }
            }


            if (_configuration == null && Options.ConfigurationManager != null)
            {
                _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
            }

            var validationParameters = Options.TokenValidationParameters.Clone();
            if (_configuration != null)
            {
                var issuers = new[] { _configuration.Issuer };
                validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
            }

            List<Exception> validationFailures = null;
            SecurityToken validatedToken;

            foreach (var validator in Options.SecurityTokenValidators)
            {
                if (validator.CanReadToken(token))
                {
                    ClaimsPrincipal principal; // it can't find this
                    try
                    {
                        principal = validator.ValidateToken(token, validationParameters, out validatedToken);
                    }
                    catch (Exception ex)
                    {

                        if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
                            && ex is SecurityTokenSignatureKeyNotFoundException)
                        {
                            Options.ConfigurationManager.RequestRefresh();
                        }

                        if (validationFailures == null)
                        {
                            validationFailures = new List<Exception>(1);
                        }
                        validationFailures.Add(ex);
                        continue;
                    }

                    var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
                    {
                        Principal = principal,
                        SecurityToken = validatedToken
                    };

                    await Events.TokenValidated(tokenValidatedContext);
                    if (tokenValidatedContext.Result != null)
                    {
                        return tokenValidatedContext.Result;
                    }

                    if (Options.SaveToken)
                    {
                        tokenValidatedContext.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken { Name = "access_token", Value = token }
                        });
                    }

                    tokenValidatedContext.Success();
                    return tokenValidatedContext.Result;
                }
            }

            if (validationFailures != null)
            {
                var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
                {
                    Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
                };

                await Events.AuthenticationFailed(authenticationFailedContext);
                if (authenticationFailedContext.Result != null)
                {
                    return authenticationFailedContext.Result;
                }

                return AuthenticateResult.Fail(authenticationFailedContext.Exception);
            }

            return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
        }
        catch (Exception ex)
        {

            var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
            {
                Exception = ex
            };

            await Events.AuthenticationFailed(authenticationFailedContext);
            if (authenticationFailedContext.Result != null)
            {
                return authenticationFailedContext.Result;
            }

            throw;
        }
    }
}

Or, is there a way to just tell the application to expect a JWT in the HTTP request body? I am well aware that the token should be sent in the request header instead of body, but I am interested into seeing if (and if so, how) this can be implemented.

I also tried this:

OnMessageReceived = ctx =>
{
       ctx.Request.EnableBuffering();
       using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 10, true))
       {
              var jsonBody = reader.ReadToEnd();
              var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
              if (body != null)
              {
                     ctx.Token = body.Token;
                     ctx.Request.Body.Position = 0;
              }
       }
       return Task.CompletedTask;
}
TheDoomDestroyer
  • 2,434
  • 4
  • 24
  • 45

2 Answers2

1

By default , AddJwtBearer will get token from request header , you should write your logic to read token from request body and validate the token . That means no such configuration to "tell" middleware to read token form request body .

If token is sent in request body , you need to read the request body in middleware and put token in header before the jwt middleware reaches. Or read the request body in one of the jwt bearer middleware's event , for example , OnMessageReceived event , read token in request body and at last set token like : context.Token = token; . Here is code sample for reading request body in middleware .

Nan Yu
  • 26,101
  • 9
  • 68
  • 148
  • It's actually pretty crazy, because I had originally tried setting `context.Token = token;` in my `OnMessageReceived` event, but it didn't do anything. However, after I tried it today after your suggestion, it works perfectly fine. – TheDoomDestroyer Oct 16 '19 at 14:44
  • I updated the original bode with the code that didn't work. It's pretty much the same as the one that works. Any ideas why that could be so? – TheDoomDestroyer Oct 16 '19 at 15:10
  • Not sure , you may debug into the `OnMessageReceived ` event from the original code to confirm what is the problem . But sometimes , yes , you will find the project "suddenly" works ....:) – Nan Yu Oct 17 '19 at 01:15
0

I'll mark @Nan Yu's answer as the correct one, but I'll post my final code nonetheless. What I essentially did was revert back to the default JwtBearerHandler and use JwtBearerOptions and JwtBearerEvents's OnMessageReceived event to get the token value from HTTP request's body.

They all reside in the Microsoft.AspNetCore.Authentication.JwtBearer namespace.

services
    .AddAuthentication(auth =>
    {
        auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = true;
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            RequireExpirationTime = true,
            ClockSkew = TimeSpan.Zero
        };

        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = ctx =>
            {
                if (ctx.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    ctx.Response.Headers.Add("Token-Expired", "true");
                }
                return Task.CompletedTask;
            },
            OnMessageReceived = ctx =>
            {
                ctx.Request.EnableBuffering();
                using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 1024, true))
                {
                    var jsonBody = reader.ReadToEnd();
                    var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
                    ctx.Request.Body.Position = 0;
                    if (body != null)
                    {
                        ctx.Token = body.Token;
                    }
                }
                return Task.CompletedTask;
            }
        };
    });
TheDoomDestroyer
  • 2,434
  • 4
  • 24
  • 45