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

JwtSecurityTokenHandler.CreateJwtSecurityToken renaming Claim Type Names. #1829

Closed TobyMosque closed 6 years ago

TobyMosque commented 6 years ago

I'm tring to secure my web tokens using Aes256CbcHmacSha512, but when i create the SecurityToken calling JwtSecurityTokenHandler.CreateToken Method (SecurityTokenDescriptor), the claims' type names are updated.

Because of that, the role-based authorization didn't work as expected (AuthorizationAttribute.Roles didn't work and I can't get the Current User).

AppSettings.Keys.cs

public static class Keys
{
    public static string SigningKey { get; }
    public static string DecryptionKey { get; }
    public static string JwtIssuer { get; } = "https://sample.placebo.com";
    public static string JwtAudience { get; } = "https://sample.placebo.com";

    static Keys()
    {
        var rng = RandomNumberGenerator.Create();
        var signingKey = new byte[64];
        var decryptionKey = new byte[32];
        rng.GetBytes(signingKey);
        rng.GetBytes(decryptionKey);

        Keys.SigningKey = Convert.ToBase64String(signingKey);
        Keys.DecryptionKey = Convert.ToBase64String(decryptionKey);
    }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    var authBuilder = services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    });

    authBuilder.AddJwtBearer(cfg =>
    {
        cfg.RequireHttpsMetadata = false;
        cfg.SaveToken = true;

        var binarySigningKey = Convert.FromBase64String(AppSettings.Keys.SigningKey);
        var binaryDecryptionKey = Convert.FromBase64String(AppSettings.Keys.DecryptionKey);

        cfg.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = AppSettings.Keys.JwtIssuer,
            ValidAudience = AppSettings.Keys.JwtAudience,
            IssuerSigningKey = new SymmetricSecurityKey(binarySigningKey),
            TokenDecryptionKey = new SymmetricSecurityKey(binaryDecryptionKey),
            ClockSkew = TimeSpan.Zero 
        };
    });
    ...
}

AuthController.cs

[ApiController]
[Route("api/[controller]/[action]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<IdentityUser<Guid>> _userManager;
    private readonly SignInManager<IdentityUser<Guid>> _signInManager;

    public AuthController(UserManager<IdentityUser<Guid>> userManager, SignInManager<IdentityUser<Guid>> signInManager)
    {
        this._userManager = userManager;
        this._signInManager = signInManager;
    }

    // GET api/values
    [HttpPost]
    public async Task<ActionResult> CreateAdmin([FromBody]UsuarioModel model)
    {
        var usuario = new IdentityUser<Guid> { Id = Guid.NewGuid(), UserName = model.Logon, Email = model.Logon };
        var result = await this._userManager.CreateAsync(usuario, model.Password);
        if (!result.Succeeded)
            return BadRequest(result.Errors);
        await this._userManager.AddToRoleAsync(usuario, "Admin");
        return Ok();

    }

    // GET api/values/5
    [HttpPost]
    public async Task<ActionResult> Logon([FromBody]UsuarioModel model)
    {
        var user = await _userManager.FindByEmailAsync(model.Logon);
        if (user == null)
            return NotFound();
        var result = await _signInManager.PasswordSignInAsync(user, model.Password, true, false);
        if (!result.Succeeded)
            return BadRequest();

        var binSigning = Convert.FromBase64String(AppSettings.Keys.SigningKey);
        var binEncrypting = Convert.FromBase64String(AppSettings.Keys.DecryptionKey);

        var claims = new ClaimsIdentity();
        claims.AddClaims(new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
        });

        var roles = await _userManager.GetRolesAsync(user);
        claims.AddClaims(roles.Select(role => new Claim(ClaimTypes.Role, role)));

        var keySigning = new SymmetricSecurityKey(binSigning);
        var signing = new SigningCredentials(keySigning, SecurityAlgorithms.HmacSha256);

        var keyEncrypting = new SymmetricSecurityKey(binEncrypting);
        var encrypting = new EncryptingCredentials(keyEncrypting, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);

        var expires = DateTime.Now.AddDays(30);
        var handler = new JwtSecurityTokenHandler();

        var token = handler.CreateToken(new SecurityTokenDescriptor
        {
            Issuer = AppSettings.Keys.JwtIssuer,
            Audience = AppSettings.Keys.JwtAudience,
            Subject = claims,
            NotBefore = DateTime.Now,
            Expires = expires,
            IssuedAt = DateTime.Now,
            SigningCredentials = signing,
            EncryptingCredentials = encrypting
        });
        return Ok(handler.WriteToken(token));
    }

    [HttpGet]
    [Authorize(Roles = "Admin")]
    public ActionResult IsAdmin()
    {
        return Ok();
    }

    [HttpGet]
    [Authorize]
    public async Task<ActionResult> GetRoles()
    {
        var user = await this._userManager.GetUserAsync(User);
        var roles = await this._userManager.GetRolesAsync(user);
        return Ok(roles);
    }
}

Reproducing the problem.:

Creating the User.:

Request

curl -X POST "https://localhost:5001/api/Auth/CreateAdmin" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"logon\": \"admin@placebo.ocom\", \"password\": \"Placebo$512\"}"

Response Headers - HttpStatus 200

access-control-allow-origin: * 
 content-length: 0 
 date: Mon, 30 Jul 2018 22:34:32 GMT 
 server: Kestrel 

Autheticating

Request

curl -X POST "https://localhost:5001/api/Auth/Logon" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"logon\": \"admin@placebo.ocom\", \"password\": \"Placebo$512\" }"

Response Body

"eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwidHlwIjoiSldUIn0.V9QoFJE3HGRLIGNAMnqUwLDhoIwRkM4oBJWOdR7swnx-D80ttcWkxWLZvJlLo2mrjOOqz2w6fxr7kpOGVpxOa9sak0XirDYe.JA0T9RjdxsEdbGAhM-FoDA.JxdItRfs7OSfl96dT-rd9kuoH0FW2lnPc2Aa3uXvtJNqW0XgrG6GwoRv_biIAglhfa-1FWHwPbSnEmD8H0TmdX_Z6d1oWyRivbgy8Bjyh1DWVIRm7hq4C0sRCDWNHjQlGt0dVipkcNM48gPGpBG3DiBCheyzA9-W5gp_mipqTgzZ1qAh1HYdHu0K4A4lZqCjjlQr0UJ2FBVzcj4FkbaH0id4t9aK1vYwak8QU3i3ZS6ZOZ9HIeWTXgUcV4j6lEDyYyAPLoAeoRt049c9tp5o3gegWjoywCjuaYBmpvCQGTJxxMghMpXLml0gvjMCb_Ra1agV7AMhdLrEdXG8yDnJH7f5C_JOH2SAH8RqgnYVTQigjLAzXCZ9zGX700Y92RwEp_w0_TTXRPfHW5yJTQNHyZXwVqjAK7OTB1AYneZNsP7gzud127hp6GydoS0xQXtbOZjALJCFNmwjRoUCTYALCdD8aKXTtUi7sJpqpsTMobM1b2HFq_WGn7GJhZTHohyzZCGLKIxbJ5t6HpAUtMpa0Eajo6bRahanl9FYM7D94sEXdCriclvlOraHHOFzQX8t.NO3pMir6w4Uv2qcMt-oA4gITip_PFgdkZdC5kgeIe-o"

Response Headers - HttpStatus 200

access-control-allow-origin: * 
 cache-control: no-cache 
 content-type: application/json; charset=utf-8 
 date: Mon, 30 Jul 2018 22:41:34 GMT 
 expires: Thu, 01 Jan 1970 00:00:00 GMT 
 pragma: no-cache 
 server: Kestrel 
 transfer-encoding: chunked 

ClaimIdentity

claimidentity

SecurityToken

securitytoken

Getting Roles

Request

curl -X GET "https://localhost:5001/api/Auth/GetRoles" -H "accept: application/json" -H "Authorization: Bearer eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwidHlwIjoiSldUIn0.V9QoFJE3HGRLIGNAMnqUwLDhoIwRkM4oBJWOdR7swnx-D80ttcWkxWLZvJlLo2mrjOOqz2w6fxr7kpOGVpxOa9sak0XirDYe.JA0T9RjdxsEdbGAhM-FoDA.JxdItRfs7OSfl96dT-rd9kuoH0FW2lnPc2Aa3uXvtJNqW0XgrG6GwoRv_biIAglhfa-1FWHwPbSnEmD8H0TmdX_Z6d1oWyRivbgy8Bjyh1DWVIRm7hq4C0sRCDWNHjQlGt0dVipkcNM48gPGpBG3DiBCheyzA9-W5gp_mipqTgzZ1qAh1HYdHu0K4A4lZqCjjlQr0UJ2FBVzcj4FkbaH0id4t9aK1vYwak8QU3i3ZS6ZOZ9HIeWTXgUcV4j6lEDyYyAPLoAeoRt049c9tp5o3gegWjoywCjuaYBmpvCQGTJxxMghMpXLml0gvjMCb_Ra1agV7AMhdLrEdXG8yDnJH7f5C_JOH2SAH8RqgnYVTQigjLAzXCZ9zGX700Y92RwEp_w0_TTXRPfHW5yJTQNHyZXwVqjAK7OTB1AYneZNsP7gzud127hp6GydoS0xQXtbOZjALJCFNmwjRoUCTYALCdD8aKXTtUi7sJpqpsTMobM1b2HFq_WGn7GJhZTHohyzZCGLKIxbJ5t6HpAUtMpa0Eajo6bRahanl9FYM7D94sEXdCriclvlOraHHOFzQX8t.NO3pMir6w4Uv2qcMt-oA4gITip_PFgdkZdC5kgeIe-o"

Response Headers - HttpStatus 500

content-type: text/html; charset=utf-8 
 date: Mon, 30 Jul 2018 22:54:32 GMT 
 server: Kestrel 
 transfer-encoding: chunked

Retriving Current User

getroles

Claims on Current Context

tokenvalidated

Is Admin

Request

curl -X GET "https://localhost:5001/api/Auth/IsAdmin" -H "accept: application/json" -H "Authorization: Bearer eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwidHlwIjoiSldUIn0.V9QoFJE3HGRLIGNAMnqUwLDhoIwRkM4oBJWOdR7swnx-D80ttcWkxWLZvJlLo2mrjOOqz2w6fxr7kpOGVpxOa9sak0XirDYe.JA0T9RjdxsEdbGAhM-FoDA.JxdItRfs7OSfl96dT-rd9kuoH0FW2lnPc2Aa3uXvtJNqW0XgrG6GwoRv_biIAglhfa-1FWHwPbSnEmD8H0TmdX_Z6d1oWyRivbgy8Bjyh1DWVIRm7hq4C0sRCDWNHjQlGt0dVipkcNM48gPGpBG3DiBCheyzA9-W5gp_mipqTgzZ1qAh1HYdHu0K4A4lZqCjjlQr0UJ2FBVzcj4FkbaH0id4t9aK1vYwak8QU3i3ZS6ZOZ9HIeWTXgUcV4j6lEDyYyAPLoAeoRt049c9tp5o3gegWjoywCjuaYBmpvCQGTJxxMghMpXLml0gvjMCb_Ra1agV7AMhdLrEdXG8yDnJH7f5C_JOH2SAH8RqgnYVTQigjLAzXCZ9zGX700Y92RwEp_w0_TTXRPfHW5yJTQNHyZXwVqjAK7OTB1AYneZNsP7gzud127hp6GydoS0xQXtbOZjALJCFNmwjRoUCTYALCdD8aKXTtUi7sJpqpsTMobM1b2HFq_WGn7GJhZTHohyzZCGLKIxbJ5t6HpAUtMpa0Eajo6bRahanl9FYM7D94sEXdCriclvlOraHHOFzQX8t.NO3pMir6w4Uv2qcMt-oA4gITip_PFgdkZdC5kgeIe-o"

Response Headers - HttpStatus 403

 content-length: 0 
 date: Mon, 30 Jul 2018 22:58:49 GMT 
 server: Kestrel 

That behivior didn't happen when i create a SecurityToken withouth an EncryptingCredentials.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    var authBuilder = services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    });

    authBuilder.AddJwtBearer(cfg =>
    {
        cfg.RequireHttpsMetadata = false;
        cfg.SaveToken = true;

        var binarySigningKey = Convert.FromBase64String(AppSettings.Keys.SigningKey);
        // var binaryDecryptionKey = Convert.FromBase64String(AppSettings.Keys.DecryptionKey);

        cfg.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = AppSettings.Keys.JwtIssuer,
            ValidAudience = AppSettings.Keys.JwtAudience,
            IssuerSigningKey = new SymmetricSecurityKey(binarySigningKey),
            // TokenDecryptionKey = new SymmetricSecurityKey(binaryDecryptionKey),
            ClockSkew = TimeSpan.Zero 
        };
    });
    ...
}

AuthController.cs

[ApiController]
[Route("api/[controller]/[action]")]
public class AuthController : ControllerBase
{
    ...
    // GET api/values/5
    [HttpPost]
    public async Task<ActionResult> Logon([FromBody]UsuarioModel model)
    {
        var user = await _userManager.FindByEmailAsync(model.Logon);
        if (user == null)
            return NotFound();
        var result = await _signInManager.PasswordSignInAsync(user, model.Password, true, false);
        if (!result.Succeeded)
            return BadRequest();

        var binSigning = Convert.FromBase64String(AppSettings.Keys.SigningKey);
        // var binEncrypting = Convert.FromBase64String(AppSettings.Keys.DecryptionKey);

        var claims = new ClaimsIdentity();
        claims.AddClaims(new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
        });

        var roles = await _userManager.GetRolesAsync(user);
        claims.AddClaims(roles.Select(role => new Claim(ClaimTypes.Role, role)));

        var keySigning = new SymmetricSecurityKey(binSigning);
        var signing = new SigningCredentials(keySigning, SecurityAlgorithms.HmacSha256);

        // var keyEncrypting = new SymmetricSecurityKey(binEncrypting);
        // var encrypting = new EncryptingCredentials(keyEncrypting, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);

        var expires = DateTime.Now.AddDays(30);
        var handler = new JwtSecurityTokenHandler();

        var token = new JwtSecurityToken(
            issuer: AppSettings.Keys.JwtIssuer,
            audience: AppSettings.Keys.JwtAudience,
            claims: claims.Claims,
            notBefore: DateTime.Now,
            expires: expires,
            signingCredentials: signing);

        // var token = handler.CreateToken(new SecurityTokenDescriptor
        // {
        //     Issuer = AppSettings.Keys.JwtIssuer,
        //     Audience = AppSettings.Keys.JwtAudience,
        //     Subject = claims,
        //     NotBefore = DateTime.Now,
        //     Expires = expires,
        //     IssuedAt = DateTime.Now,
        //     SigningCredentials = signing,
        //     EncryptingCredentials = encrypting
        // });
        return Ok(handler.WriteToken(token));
    }
    ...
}

SecurityToken

securitytoken

Token Validated

tokenvalidated

Current User

getroles

blowdart commented 6 years ago

This issue was moved to AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#982