Azure-activedirectory-identitymodel-extensions-for-dotnet: JwtSecurityTokenHandler.CreateJwtSecurityToken renaming Claim Type Names.

Created on 31 Jul 2018  路  8Comments  路  Source: AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet

_From @TobyMosque on July 30, 2018 23:16_

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\": \"[email protected]\", \"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\": \"[email protected]\", \"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

_Copied from original issue: aspnet/Security#1829_

Customer reported P1

All 8 comments

@TobyMosque @blowdart is the issue that encrypting seems to fail?
Can you provide IdentityModel error logs or exception?

the Encryption and Decryption works as expected. but when I try to create the Token using the method JwtSecurityTokenHandler.CreateJwtSecurityToken the type name are modified.:

for exemple, if i try to create a token with the follows claims.:

var id = "my_awesome_id"
var role = "my_awesome_role"

var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
    new Claim(ClaimTypes.Role, x)
};
// output claims.:
// [0] { Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Value = "my_awesome_role" }
// [1] { Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Value = "my_awesome_role" }

using the method JwtSecurityTokenHandler.CreateJwtSecurityToken or JwtSecurityTokenHandler.CreateToken(new SecurityTokenDescriptor

var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(
    issuer: domain, // domain => string/url
    audience: domain, // domain => string/url
    subject: new ClaimsIdentity(claims),
    notBefore: DateTime.Now,
    expires: expires,
    issuedAt: DateTime.Now,
    signingCredentials: signing,
    encryptingCredentials: encrypting);

// output token.Claims.:
// [0] { Type = "nameidentifier", Value = "my_awesome_role" }
// [1] { Type = "role", Value = "my_awesome_role" }
// [2] ...

as u can see the output of the claims and the token.Claims are different. because of that, Identity don't work as expected, the token is valid, the user had access to methods with AuthorizeAttribute, but the Identity don't know who is logged or what claims the user had.

but when i create the token using the constructor of the JwtSecurityToken

var handler = new JwtSecurityTokenHandler();
var token = new JwtSecurityToken(
    issuer: domain,
    audience: domain,
    claims: claims,
    notBefore: DateTime.Now,
    expires: expires,
    signingCredentials: signing);

// output token.Claims.:
// [0] { Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Value = "my_awesome_role" }
// [1] { Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Value = 
// [2] ...

as you can see, the claim types didn't get renamed, so Identity works has expected. but JwtSecurityToken don't support Encryption.

@TobyMosque the issue here is the JwtSecurityTokenHandler performs outbound 'type' mapping.
You can control this per instance:

JwtSecurityTokenHandler.OutboundClaimTypeMap.Clear();

per process

JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

@TobyMosque I am going to close this, if this doesn't work for you, please reopen.

@brentschmaltz thx, that worked to me, but that behavior is documented somewhere?

Make sure you set RoleClaimType and NameClaimType claim names for token validation to the same values you have in your token - role and sub:

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

    // Set your values, 
    // By default values are https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/the-role-of-claims
    // for NameClaimType - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
    // for RoleClaimType - "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
    NameClaimType = "sub",
    RoleClaimType = "role"
};

This just wasted an hour of my life. Why is there a global static singleton configuration again, that ASP.NET Core sets implicitly, and is then inherited by anything created by me, unless I explicitly clear it?

Was this page helpful?
0 / 5 - 0 ratings