aspnet / Security

[Archived] Middleware for security and authorization of web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
1.27k stars 600 forks source link

JWT payload "unique_name" not mapped correctly #1910

Closed Abrynos closed 5 years ago

Abrynos commented 5 years ago

Currently i'm writing a project for university using JWT-Authentication. When creating JWTs in my TokenManager i add a claim with the type JwtRegisteredClaimNames.UniqueName which is just "unique_name" . In my Controllers now i try to get the unique name of the user sending the JWT with HttpContext?.User?.Claims?.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.UniqueName)?.Value which always returns null. So i made a foreach loop to just write all Claims to the console. Thing is: according to jwt.io the claim is saved (as intended) to unique_name

image

The JWT decoder puts that claim in a Claim-object, which' Type-property has the value http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name (an equivalent to System.Security.Claims.ClaimTypes.Name) as shown in the console output image

If this is intended behaviour i'd appreciate you explaining why or pointing to where i can find out :)

Here's the code of a test-project i checked with:

Program.cs

using System;
using System.Threading.Tasks;
using test.Kestrel;

namespace test {
    internal class Program {
        private static async Task Main(string[] args) {
            await TestKestrel.Start();

            Console.ReadLine();

            await TestKestrel.Stop();
        }
    }
}

TestKestrel.cs

using Microsoft.AspNetCore.Hosting;
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;

namespace test.Kestrel {
    internal static class TestKestrel {
        private static IWebHost KestrelWebHost;

        internal static async Task Start() {
            if (KestrelWebHost != null) {
                return;
            }

            IWebHostBuilder builder = new WebHostBuilder();

            builder.UseContentRoot(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location));
            builder.UseWebRoot(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "www"));

            builder.UseKestrel(options => options.ListenLocalhost(8080));

            builder.UseStartup<Startup>();

            IWebHost kestrelWebHost = builder.Build();

            try {
                await kestrelWebHost.StartAsync().ConfigureAwait(false);
            }
            catch (Exception e) {
                kestrelWebHost.Dispose();
                return;
            }

            KestrelWebHost = kestrelWebHost;
        }

        internal static async Task Stop() {
            if (KestrelWebHost == null) {
                return;
            }

            await KestrelWebHost.StopAsync().ConfigureAwait(false);
            KestrelWebHost.Dispose();
            KestrelWebHost = null;
        }
    }
}

Startup.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using System;
using System.IdentityModel.Tokens.Jwt;

namespace test.Kestrel {
    internal sealed class Startup {
        private readonly IConfiguration Configuration;

        public Startup(IConfiguration configuration) => Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));

        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            if (app == null || env == null) {
                return;
            }

            app.UseAuthentication();

            app.UseResponseCompression();

            app.UseMvcWithDefaultRoute();

            app.UseDefaultFiles();
            app.UseStaticFiles();
        }

        public void ConfigureServices(IServiceCollection services) {
            if (services == null) {
                return;
            }

            AuthenticationBuilder auth = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);

            auth.AddJwtBearer(options => {
                options.TokenValidationParameters = new TokenValidationParameters {
                    ValidateIssuer = true,
                    ValidIssuer = "testIssuer",

                    ValidateAudience = true,
                    ValidAudience = "testIssuer",

                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = TokenManager.EncryptionKey,

                    RequireExpirationTime = true,
                    ValidateLifetime = true
                };
            });

            services.AddAuthorization(opts => {
                opts.AddPolicy("AccessToken", policy => {
                    policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
                    policy.RequireClaim(JwtRegisteredClaimNames.Typ, TokenManager.TokenTypes.Access.ToString());
                    policy.RequireClaim(JwtRegisteredClaimNames.UniqueName);
                });

                opts.AddPolicy("RefreshToken", policy => {
                    policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
                    policy.RequireClaim(JwtRegisteredClaimNames.Typ, TokenManager.TokenTypes.Refresh.ToString());
                    policy.RequireClaim(JwtRegisteredClaimNames.UniqueName);
                });
            });

            services.AddResponseCompression();

            IMvcCoreBuilder mvc = services.AddMvcCore();

            mvc.SetCompatibilityVersion(CompatibilityVersion.Latest);

            mvc.AddFormatterMappings();

            mvc.AddAuthorization();

            mvc.AddJsonFormatters();

            // Fix default contract resolver to use original names and not a camel-case
            // Also add debugging aid while we are at it
            mvc.AddJsonOptions(
                options => {
                    options.SerializerSettings.ContractResolver = new DefaultContractResolver();
                }
            );
        }
    }
}

TestController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Security.Claims;

namespace test.Kestrel.Controllers {
    [ApiController]
    [Route("Api")]
    public sealed class TestController : ControllerBase {
        [HttpGet]
        [AllowAnonymous]
        [Route("GetJWT")]
        public ActionResult<string> GetJWT() => Ok(TokenManager.GenerateAccessToken("myTestID"));

        [HttpGet]
        [Route("TestJWT")]
        public ActionResult TestJWT() {
            foreach(Claim c in HttpContext.User.Claims) {
                Console.WriteLine(c.Type + " = " + c.Value);
            }
            return Ok();
        }
    }
}

TokenManager.cs

using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace test {
    internal static class TokenManager {
        internal static SymmetricSecurityKey EncryptionKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes("gepQP532lwHZBG5qXJ0R"));
        private static SigningCredentials SigningCredentials => new SigningCredentials(EncryptionKey, SecurityAlgorithms.HmacSha512);

        private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new JwtSecurityTokenHandler();

        internal static string GenerateAccessToken(string userID) {
            if (string.IsNullOrEmpty(userID)) {
                return null;
            }

            return GenerateToken(userID, DateTime.Now.AddMinutes(10), TokenTypes.Access);
        }

        internal static string GenerateRefreshToken(string userID) {
            if (string.IsNullOrEmpty(userID)) {
                return null;
            }

            return GenerateToken(userID, DateTime.Now.AddHours(1), TokenTypes.Refresh);
        }

        private static string GenerateToken(string userID, DateTime expirationTime, TokenTypes tokenType) {
            try {
                return JwtSecurityTokenHandler.WriteToken(
                    new JwtSecurityToken(
                        issuer: "testIssuer",
                        audience: "testIssuer",
                        expires: expirationTime,
                        claims: new[] {
                            new Claim(JwtRegisteredClaimNames.UniqueName, userID),
                            new Claim(JwtRegisteredClaimNames.Typ, tokenType.ToString())
                        },
                        signingCredentials: SigningCredentials
                    )
                );
                ClaimTypes.Name
            }
            catch (Exception e) {
                Console.WriteLine(e.StackTrace);
                return null;
            }
        }

        internal enum TokenTypes : byte {
            Invalid,
            Access,
            Refresh
        }
    }
}
Tratcher commented 5 years ago

That mapping is configured here: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/d2361e5dcd1abbf6d0ea441cdb2e7404166b122c/src/System.IdentityModel.Tokens.Jwt/ClaimTypeMapping.cs#L61

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/d2361e5dcd1abbf6d0ea441cdb2e7404166b122c/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs#L60

You can call JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() if you want to remove those.

Abrynos commented 5 years ago

Thanks for pointing it out, although it doesn't make sense having to do that in the first place IMHO