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() {