GeoDev
GeoDev

Reputation: 33

Web Api Bearer JWT Token Authentication with Api Key fails on successive calls after successful token authentication

I have created an MVC Core API that authenticates users with an api key. On successful authentication it sends back a JWT token which they use for any subsequent requests. I can successfully get authenticated with a valid api key and get a token as a response back. Then i can use this token to make a request but the next request fails. In my real application the consumer is an MVC Core site and until now i hadn't noticed this issue because in every mvc controller action i was calling one api action but now that i have the need to call two api actions one after the other on the same mvc action the second one fails and i cannot understand why.

I have reproduced my issue in a sample web api and console application.

This is the code for the MVC Core API endpoint validating the api key and generating the jwt token:

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using PureHub.Services.Models.Valid8.Authentication;

namespace BugApiJwt.Controllers
{
    [Authorize]
    [Route("v1/[controller]")]
    public class AuthenticationController : ControllerBase
    {
        [AllowAnonymous]
        [HttpPost("[action]")]
        public virtual async Task<IActionResult> Token([FromBody] ApiLoginRequest model)
        {
            if (model != null)
            {
                if (model.ApiKey == "VdPfwrL+mpRHKgzAIm9js7e/J9AbJshoPgv1nIZiat22R")
                {
                    var claims = new List<Claim>
                    {
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
                        new Claim(JwtRegisteredClaimNames.Iat,
                            new DateTimeOffset(DateTime.UtcNow).ToUniversalTime().ToUnixTimeSeconds().ToString(),
                            ClaimValueTypes.Integer64)
                    };

                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("FTTaIMmkh3awD/4JF0iHgAfNiB6/C/gFeDdrKU/4YG1ZK36o16Ja4wLO+1Qft6yd+heHPRB2uQqXd76p5bXXPQ=="));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                    var token = new JwtSecurityToken(
                        issuer: "http://localhost:58393/",
                        audience: "http://localhost:58393/",
                        claims: claims,
                        expires: DateTime.UtcNow.AddMinutes(30),
                        signingCredentials: creds);

                    return Ok(new ApiLoginResponse
                    {
                        Token = new JwtSecurityTokenHandler().WriteToken(token),
                        Expiration = token.ValidTo
                    });
                }
            }

            return BadRequest();
        }
    }
 }

This is the protected resource:

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BugApiJwt.Controllers
{
    [Authorize]
    [Route("v1/values")]
    public class ValuesController : Controller
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new[] { "value1", "value2" };
        }

        [HttpGet("{id}")]
        public string Get(int id)
        {
            return $"You said: {id}";
        }
    }
}

And this is my startup:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace BugApiJwt
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;

                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidIssuer = "http://localhost:58393/",
                        ValidAudience = "http://localhost:58393/",
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("FTTaIMmkh3awD/4JF0iHgAfNiB6/C/gFeDdrKU/4YG1ZK36o16Ja4wLO+1Qft6yd+heHPRB2uQqXd76p5bXXPQ==")),
                    };
                });
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseAuthentication();
            app.UseMvc();
        }
    }
}

And this is the console application i'm testing it with:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace BugApiJwt.Console
{
    public class Program
    {
        private const string ApiKey = "VdPfwrL+mpRHKgzAIm9js7e/J9AbJshoPgv1nIZiat22R";
        private const string BaseAddress = "http://localhost:58393/";
        private static HttpClient _client = new HttpClient();
        private static string _realToken = string.Empty;

        private static void Main()
        {
            _client = new HttpClient
            {
                BaseAddress = new Uri(BaseAddress)
            };

            _client.DefaultRequestHeaders.Accept.Clear();
            _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            // Works
            System.Console.WriteLine("Call GetOne");
            var getOne = Get().GetAwaiter().GetResult();
            System.Console.WriteLine(getOne);

            // Fails
            System.Console.WriteLine("Call GetAll");
            var getTwo = GetAll().GetAwaiter().GetResult();
            System.Console.WriteLine(getTwo);

            System.Console.WriteLine("All Finished. Press Enter to exit");
            System.Console.ReadLine();
        }

        private static async Task<string> GetAuthenticationToken()
        {
            const string resource = "v1/authentication/token";

            if (!string.IsNullOrEmpty(_realToken)){return _realToken;}

            var loginRequest = new ApiLoginRequest{ApiKey = ApiKey};

            var httpResponseMessage = await _client.PostAsync(resource, ObjectToJsonContent(loginRequest)).ConfigureAwait(false);

            if (httpResponseMessage.IsSuccessStatusCode)
            {
                var content = await httpResponseMessage.Content.ReadAsStringAsync();

                var obj = JsonConvert.DeserializeObject<ApiLoginResponse>(content);

                _realToken = obj.Token;

                return obj.Token;
            }

            throw new Exception("Token is null");
        }
        public static async Task<string> Get()
        {
            var resource = "v1/values/1";
            var token = await GetAuthenticationToken();

            _client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");

            var httpResponseMessage = await _client.GetAsync(resource);

            System.Console.WriteLine(httpResponseMessage.RequestMessage.Headers.Authorization);
            System.Console.WriteLine(httpResponseMessage.Headers.WwwAuthenticate);

            var content = await httpResponseMessage.Content.ReadAsStringAsync();

            return content;
        }
        public static async Task<string> GetAll()
        {
            var resource = "v1/values";
            var token = await GetAuthenticationToken();

            _client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");

            var httpResponseMessage = await _client.GetAsync(resource);

            System.Console.WriteLine(httpResponseMessage.RequestMessage.Headers.Authorization);
            System.Console.WriteLine(httpResponseMessage.Headers.WwwAuthenticate);

            var content = await httpResponseMessage.Content.ReadAsStringAsync();

            return content;
        }
        private static StringContent ObjectToJsonContent<T>(T objectToPost) where T : class, new()
        {
            var tJson = JsonConvert.SerializeObject(objectToPost,
                Formatting.Indented, new JsonSerializerSettings
                {
                    NullValueHandling = NullValueHandling.Ignore,
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                });

            return new StringContent(tJson, Encoding.UTF8, "application/json");
        }

    }
    public class ApiLoginRequest
    {
        public string ApiKey { get; set; }
    }
    public class ApiLoginResponse
    {
        public string Token { get; set; }

        public DateTime Expiration { get; set; }
    }
}

Any help on why the second call fails?

The error message shown in the web api output window is:

Bearer was not authenticated. Failure message: No SecurityTokenValidator available for token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyNmFiNjQzYjFjOTM0MzYwYjI4NDAxMzZjNDIxOTBlZSIsImlhdCI6MTUxMDA2NDg0MywiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiR2xvYmFsSWQiOiI2NjVjYWEzYjYxYmY0MWRmOGIzMTVhODY5YzQzMmJkYyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJuYmYiOjE1MTAwNjQ4NDMsImV4cCI6MTUxMDA2NjY0MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0NDM2MCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQzNjAifQ.wJ86Ut2dmbDRDCNXU2kWXeQ1pQGkiVtUx7oSyJIZMzc

Upvotes: 1

Views: 1567

Answers (1)

GeoDev
GeoDev

Reputation: 33

It doesn't work because this piece of code TryAddWithoutValidation("Authorization", $"Bearer {token}"); adds the token on top of what is already in the authorization header without clearing it first. As a result successive calls add the bearer string with the token in the header which already contains the bearer token.

Upvotes: 1

Related Questions