diff --git a/src/System.IdentityModel.Tokens.Jwt/ClaimTypeMapping.cs b/src/Microsoft.IdentityModel.JsonWebTokens/ClaimTypeMapping.cs
similarity index 96%
rename from src/System.IdentityModel.Tokens.Jwt/ClaimTypeMapping.cs
rename to src/Microsoft.IdentityModel.JsonWebTokens/ClaimTypeMapping.cs
index 20904928cc..e2b26d0ba2 100644
--- a/src/System.IdentityModel.Tokens.Jwt/ClaimTypeMapping.cs
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/ClaimTypeMapping.cs
@@ -1,19 +1,16 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Security.Claims;
-namespace System.IdentityModel.Tokens.Jwt
+namespace Microsoft.IdentityModel.JsonWebTokens
{
- ///
- /// Defines the inbound and outbound mapping for claim claim types from jwt to .net claim
- ///
internal static class ClaimTypeMapping
{
// This is the short to long mapping.
- // key is the long claim type
- // value is the short claim type
+ // key is the long claim type
+ // value is the short claim type
private static Dictionary shortToLongClaimTypeMapping = new Dictionary
{
{ JwtRegisteredClaimNames.Actort, ClaimTypes.Actor },
diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs b/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs
index 0a687655d2..d916f9995e 100644
--- a/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs
@@ -12,6 +12,11 @@
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Previously released as non-static", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(System.String,Microsoft.IdentityModel.Tokens.EncryptingCredentials,System.Collections.Generic.IDictionary{System.String,System.Object})~System.String")]
[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Previously released as visible field", Scope = "member", Target = "~F:Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.RegexJws")]
[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Previously released as visible field", Scope = "member", Target = "~F:Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.RegexJwe")]
+[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Breaking change", Scope = "member", Target = "~F:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DefaultInboundClaimTypeMap")]
+[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Breaking change", Scope = "member", Target = "~F:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DefaultMapInboundClaims")]
+[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Breaking change", Scope = "member", Target = "~F:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DefaultOutboundClaimTypeMap")]
+[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Breaking change", Scope = "member", Target = "~P:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.InboundClaimTypeMap")]
+[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Breaking change", Scope = "member", Target = "~P:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.OutboundClaimTypeMap")]
[assembly: SuppressMessage("Design", "CA1052:Static holder types should be Static or NotInheritable", Justification = "Previously released as non-static/inheritable", Scope = "type", Target = "~T:Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as Try method", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.TryGetPayloadValue``1(System.String,``0@)~System.Boolean")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as Try method", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.TryGetHeaderValue``1(System.String,``0@)~System.Boolean")]
diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
index bf1c3fbadf..7506976e02 100644
--- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
@@ -23,6 +23,21 @@ namespace Microsoft.IdentityModel.JsonWebTokens
///
public class JsonWebTokenHandler : TokenHandler
{
+ private IDictionary _inboundClaimTypeMap;
+ private const string _namespace = "http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties";
+ private static string _shortClaimType = _namespace + "/ShortTypeName";
+ private bool _mapInboundClaims = DefaultMapInboundClaims;
+
+ ///
+ /// Default claim type mapping for inbound claims.
+ ///
+ public static IDictionary DefaultInboundClaimTypeMap = new Dictionary(ClaimTypeMapping.InboundClaimTypeMap);
+
+ ///
+ /// Default value for the flag that determines whether or not the InboundClaimTypeMap is used.
+ ///
+ public static bool DefaultMapInboundClaims = false;
+
///
/// Gets the Base64Url encoded string representation of the following JWT header:
/// { , }.
@@ -30,6 +45,17 @@ public class JsonWebTokenHandler : TokenHandler
/// The Base64Url encoded string representation of the unsigned JWT header.
public const string Base64UrlEncodedUnsignedJWSHeader = "eyJhbGciOiJub25lIn0";
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JsonWebTokenHandler()
+ {
+ if (_mapInboundClaims)
+ _inboundClaimTypeMap = new Dictionary(DefaultInboundClaimTypeMap);
+ else
+ _inboundClaimTypeMap = new Dictionary();
+ }
+
///
/// Gets the type of the .
///
@@ -39,6 +65,64 @@ public Type TokenType
get { return typeof(JsonWebToken); }
}
+ ///
+ /// Gets or sets the property name of the will contain the original JSON claim 'name' if a mapping occurred when the (s) were created.
+ ///
+ /// If .IsNullOrWhiteSpace('value') is true.
+ public static string ShortClaimTypeProperty
+ {
+ get
+ {
+ return _shortClaimType;
+ }
+
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ throw LogHelper.LogArgumentNullException(nameof(value));
+
+ _shortClaimType = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the property which is used when determining whether or not to map claim types that are extracted when validating a .
+ /// If this is set to true, the is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs.
+ /// The default value is false.
+ ///
+ public bool MapInboundClaims
+ {
+ get
+ {
+ return _mapInboundClaims;
+ }
+ set
+ {
+ if(!_mapInboundClaims && value && _inboundClaimTypeMap.Count == 0)
+ _inboundClaimTypeMap = new Dictionary(DefaultInboundClaimTypeMap);
+ _mapInboundClaims = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the which is used when setting the for claims in the extracted when validating a .
+ /// The is set to the JSON claim 'name' after translating using this mapping.
+ /// The default value is ClaimTypeMapping.InboundClaimTypeMap.
+ ///
+ /// 'value' is null.
+ public IDictionary InboundClaimTypeMap
+ {
+ get
+ {
+ return _inboundClaimTypeMap;
+ }
+
+ set
+ {
+ _inboundClaimTypeMap = value ?? throw LogHelper.LogArgumentNullException(nameof(value));
+ }
+ }
+
internal static IDictionary AddCtyClaimDefaultValue(IDictionary additionalClaims, bool setDefaultCtyClaim)
{
if (!setDefaultCtyClaim)
@@ -680,9 +764,62 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, Tok
if (string.IsNullOrWhiteSpace(issuer))
issuer = GetActualIssuer(jwtToken);
+ if (MapInboundClaims)
+ return CreateClaimsIdentityWithMapping(jwtToken, validationParameters, issuer);
+
return CreateClaimsIdentityPrivate(jwtToken, validationParameters, issuer);
}
+ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer)
+ {
+ _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters));
+
+ ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer);
+ foreach (Claim jwtClaim in jwtToken.Claims)
+ {
+ bool wasMapped = _inboundClaimTypeMap.TryGetValue(jwtClaim.Type, out string claimType);
+
+ if (!wasMapped)
+ claimType = jwtClaim.Type;
+
+ if (claimType == ClaimTypes.Actor)
+ {
+ if (identity.Actor != null)
+ throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(
+ LogMessages.IDX14112,
+ LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort),
+ jwtClaim.Value)));
+
+ if (CanReadToken(jwtClaim.Value))
+ {
+ JsonWebToken actor = ReadToken(jwtClaim.Value) as JsonWebToken;
+ identity.Actor = CreateClaimsIdentity(actor, validationParameters);
+ }
+ }
+
+ if (wasMapped)
+ {
+ Claim claim = new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, issuer, issuer, identity);
+ if (jwtClaim.Properties.Count > 0)
+ {
+ foreach (var kv in jwtClaim.Properties)
+ {
+ claim.Properties[kv.Key] = kv.Value;
+ }
+ }
+
+ claim.Properties[ShortClaimTypeProperty] = jwtClaim.Type;
+ identity.AddClaim(claim);
+ }
+ else
+ {
+ identity.AddClaim(jwtClaim);
+ }
+ }
+
+ return identity;
+ }
+
internal override ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken securityToken, TokenValidationParameters tokenValidationParameters, string issuer)
{
return CreateClaimsIdentity(securityToken as JsonWebToken, tokenValidationParameters, issuer);
diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs
index 7b24360058..2e0ab2e469 100644
--- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs
@@ -5,11 +5,9 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
-using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
-using System.Threading;
using Microsoft.IdentityModel.Json.Linq;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
@@ -51,7 +49,7 @@ public class JwtTokenUtilities
///
/// String to be signed
/// The that contain crypto specs used to sign the token.
- /// The bse64urlendcoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).
+ /// The base 64 url encoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).
/// 'input' or 'signingCredentials' is null.
public static string CreateEncodedSignature(string input, SigningCredentials signingCredentials)
{
@@ -83,7 +81,7 @@ public static string CreateEncodedSignature(string input, SigningCredentials sig
/// String to be signed
/// The that contain crypto specs used to sign the token.
/// should the be cached.
- /// The bse64urlendcoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).
+ /// The base 64 url encoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).
/// or is null.
public static string CreateEncodedSignature(string input, SigningCredentials signingCredentials, bool cacheProvider)
{
diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs
index 87bfc6ec53..d015c9ecf8 100644
--- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs
+++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Security.Claims;
@@ -39,7 +38,7 @@ public class JwtSecurityTokenHandler : SecurityTokenHandler
///
/// Default claim type mapping for inbound claims.
///
- public static IDictionary DefaultInboundClaimTypeMap = ClaimTypeMapping.InboundClaimTypeMap;
+ public static IDictionary DefaultInboundClaimTypeMap = new Dictionary(ClaimTypeMapping.InboundClaimTypeMap);
///
/// Default value for the flag that determines whether or not the InboundClaimTypeMap is used.
@@ -49,7 +48,7 @@ public class JwtSecurityTokenHandler : SecurityTokenHandler
///
/// Default claim type mapping for outbound claims.
///
- public static IDictionary DefaultOutboundClaimTypeMap = ClaimTypeMapping.OutboundClaimTypeMap;
+ public static IDictionary DefaultOutboundClaimTypeMap = new Dictionary(ClaimTypeMapping.OutboundClaimTypeMap);
///
/// Default claim type filter list.
diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs
index 1357f6cff2..dac7cb8971 100644
--- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs
+++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs
@@ -11,6 +11,7 @@
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography;
+using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Json;
@@ -2575,6 +2576,114 @@ public void ValidateJWE(JwtTheoryData theoryData)
TestUtilities.AssertFailIfErrors(context);
}
+ // Test creates a JWT with every mapped claim and then checks that the result of validation from the
+ // JwtSecurityTokenHandler and JsonWebTokenHandler are the same, both in the mapped and unmapped case.
+ [Fact]
+ public async Task ValidateJsonWebTokenClaimMapping()
+ {
+ var jsonWebTokenHandler = new JsonWebTokenHandler() { MapInboundClaims = false };
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Subject = new ClaimsIdentity(Default.PayloadAllShortClaims),
+ SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2,
+ EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.DefaultX509Key_2048, SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes128CbcHmacSha256),
+ };
+
+ var accessToken = jsonWebTokenHandler.CreateToken(tokenDescriptor);
+
+ var validationParameters = new TokenValidationParameters
+ {
+ ValidAudience = Default.Audience,
+ ValidIssuer = Default.Issuer,
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key,
+ TokenDecryptionKey = KeyingMaterial.DefaultX509Key_2048,
+ AlgorithmValidator = ValidationDelegates.AlgorithmValidatorBuilder(true),
+ RequireExpirationTime = false,
+ };
+
+ JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler() { MapInboundClaims = false };
+
+ TokenValidationResult jsonValidationResult = await jsonWebTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
+ TokenValidationResult jwtValidationResult = await jwtSecurityTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
+
+ var context = new CompareContext
+ {
+ PropertiesToIgnoreWhenComparing = new Dictionary>
+ {
+ { typeof(TokenValidationResult), new List { "SecurityToken", "TokenType" } }
+ }
+ };
+
+ if(jsonValidationResult.IsValid && jwtValidationResult.IsValid)
+ {
+ if(!IdentityComparer.AreEqual(jsonValidationResult, jwtValidationResult, context))
+ {
+ context.AddDiff("jsonValidationResult.IsValid && jwtValidationResult.IsValid, Validation results are not equal");
+ }
+ }
+
+ jsonWebTokenHandler.MapInboundClaims = true;
+ jwtSecurityTokenHandler.MapInboundClaims = true;
+
+ jsonValidationResult = await jsonWebTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
+ jwtValidationResult = await jwtSecurityTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
+
+ if (jsonValidationResult.IsValid && jwtValidationResult.IsValid)
+ {
+ if (!IdentityComparer.AreEqual(jsonValidationResult, jwtValidationResult, context))
+ {
+ context.AddDiff("jsonValidationResult.IsValid && jwtValidationResult.IsValid, Validation results are not equal");
+ }
+ }
+
+ TestUtilities.AssertFailIfErrors(context);
+ }
+
+ // Test shows if the JwtSecurityTokenHandler has mapping OFF and
+ // the JsonWebTokenHandler has mapping ON,the claims are different.
+ [Fact]
+ public async Task ValidateDifferentClaimsBetweenHandlers()
+ {
+ var jsonWebTokenHandler = new JsonWebTokenHandler() { MapInboundClaims = true };
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Subject = new ClaimsIdentity(Default.PayloadAllShortClaims),
+ SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2,
+ EncryptingCredentials = new EncryptingCredentials(KeyingMaterial.DefaultX509Key_2048, SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes128CbcHmacSha256),
+ };
+
+ var accessToken = jsonWebTokenHandler.CreateToken(tokenDescriptor);
+
+ var validationParameters = new TokenValidationParameters
+ {
+ ValidAudience = Default.Audience,
+ ValidIssuer = Default.Issuer,
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key,
+ TokenDecryptionKey = KeyingMaterial.DefaultX509Key_2048,
+ AlgorithmValidator = ValidationDelegates.AlgorithmValidatorBuilder(true),
+ RequireExpirationTime = false,
+ };
+
+ JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler() { MapInboundClaims = false };
+
+ TokenValidationResult jsonValidationResult = await jsonWebTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
+ TokenValidationResult jwtValidationResult = await jwtSecurityTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
+
+ var context = new CompareContext();
+
+ if (jsonValidationResult.IsValid && jwtValidationResult.IsValid)
+ {
+ if (IdentityComparer.AreEqual(jsonValidationResult.Claims, jwtValidationResult.Claims, CompareContext.Default))
+ {
+ context.AddDiff("jsonValidationResult.IsValid && jwtValidationResult.IsValid, Claims between validation results are equal");
+ }
+ }
+
+ TestUtilities.AssertFailIfErrors(context);
+ }
+
[Theory, MemberData(nameof(ValidateJweTestCases))]
public async Task ValidateJWEAsync(JwtTheoryData theoryData)
{
diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JwtTokenUtilitiesTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JwtTokenUtilitiesTests.cs
index 37affa1515..2ebdd1e923 100644
--- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JwtTokenUtilitiesTests.cs
+++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JwtTokenUtilitiesTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.TestUtils;
@@ -13,6 +14,26 @@ namespace Microsoft.IdentityModel.JsonWebTokens.Tests
{
public class JwtTokenUtilitiesTests
{
+ [Fact]
+ public void ClaimTypeMappingIsIndependent()
+ {
+ // Each handler should have its own instance of the ClaimTypeMap
+ var jwtClaimsMapping = JwtSecurityTokenHandler.DefaultInboundClaimTypeMap;
+ var jsonClaimsMapping = JsonWebTokenHandler.DefaultInboundClaimTypeMap;
+
+ Assert.NotEmpty(jwtClaimsMapping);
+ Assert.NotEmpty(jsonClaimsMapping);
+
+ Assert.Equal(jwtClaimsMapping, jsonClaimsMapping);
+
+ // Clearing one should not affect the other
+ jwtClaimsMapping.Clear();
+
+ Assert.Empty(jwtClaimsMapping);
+ Assert.NotEmpty(jsonClaimsMapping);
+
+ }
+
[Fact]
public void ResolveTokenSigningKey()
{