Skip to content

Commit

Permalink
Add ClaimsMapping to JsonWebTokenHandler
Browse files Browse the repository at this point in the history
JsonWebTokenHandler now has opt-in claims mapping
  • Loading branch information
westin-m authored and brentschmaltz committed May 24, 2023
1 parent 02518f7 commit 8e7f07e
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Defines the inbound and outbound mapping for claim claim types from jwt to .net claim
/// </summary>
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<string, string> shortToLongClaimTypeMapping = new Dictionary<string, string>
{
{ JwtRegisteredClaimNames.Actort, ClaimTypes.Actor },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
137 changes: 137 additions & 0 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,39 @@ namespace Microsoft.IdentityModel.JsonWebTokens
/// </summary>
public class JsonWebTokenHandler : TokenHandler
{
private IDictionary<string, string> _inboundClaimTypeMap;
private const string _namespace = "http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties";
private static string _shortClaimType = _namespace + "/ShortTypeName";
private bool _mapInboundClaims = DefaultMapInboundClaims;

/// <summary>
/// Default claim type mapping for inbound claims.
/// </summary>
public static IDictionary<string, string> DefaultInboundClaimTypeMap = new Dictionary<string, string>(ClaimTypeMapping.InboundClaimTypeMap);

/// <summary>
/// Default value for the flag that determines whether or not the InboundClaimTypeMap is used.
/// </summary>
public static bool DefaultMapInboundClaims = false;

/// <summary>
/// Gets the Base64Url encoded string representation of the following JWT header:
/// { <see cref="JwtHeaderParameterNames.Alg"/>, <see cref="SecurityAlgorithms.None"/> }.
/// </summary>
/// <return>The Base64Url encoded string representation of the unsigned JWT header.</return>
public const string Base64UrlEncodedUnsignedJWSHeader = "eyJhbGciOiJub25lIn0";

/// <summary>
/// Initializes a new instance of the <see cref="JsonWebTokenHandler"/> class.
/// </summary>
public JsonWebTokenHandler()
{
if (_mapInboundClaims)
_inboundClaimTypeMap = new Dictionary<string, string>(DefaultInboundClaimTypeMap);
else
_inboundClaimTypeMap = new Dictionary<string, string>();
}

/// <summary>
/// Gets the type of the <see cref="JsonWebToken"/>.
/// </summary>
Expand All @@ -39,6 +65,64 @@ public Type TokenType
get { return typeof(JsonWebToken); }
}

/// <summary>
/// Gets or sets the property name of <see cref="Claim.Properties"/> the will contain the original JSON claim 'name' if a mapping occurred when the <see cref="Claim"/>(s) were created.
/// </summary>
/// <exception cref="ArgumentException">If <see cref="string"/>.IsNullOrWhiteSpace('value') is true.</exception>
public static string ShortClaimTypeProperty
{
get
{
return _shortClaimType;
}

set
{
if (string.IsNullOrWhiteSpace(value))
throw LogHelper.LogArgumentNullException(nameof(value));

_shortClaimType = value;
}
}

/// <summary>
/// Gets or sets the <see cref="MapInboundClaims"/> property which is used when determining whether or not to map claim types that are extracted when validating a <see cref="JsonWebToken"/>.
/// <para>If this is set to true, the <see cref="Claim.Type"/> is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs.</para>
/// <para>The default value is false.</para>
/// </summary>
public bool MapInboundClaims
{
get
{
return _mapInboundClaims;
}
set
{
if(!_mapInboundClaims && value && _inboundClaimTypeMap.Count == 0)
_inboundClaimTypeMap = new Dictionary<string, string>(DefaultInboundClaimTypeMap);
_mapInboundClaims = value;
}
}

/// <summary>
/// Gets or sets the <see cref="InboundClaimTypeMap"/> which is used when setting the <see cref="Claim.Type"/> for claims in the <see cref="ClaimsPrincipal"/> extracted when validating a <see cref="JsonWebToken"/>.
/// <para>The <see cref="Claim.Type"/> is set to the JSON claim 'name' after translating using this mapping.</para>
/// <para>The default value is ClaimTypeMapping.InboundClaimTypeMap.</para>
/// </summary>
/// <exception cref="ArgumentNullException">'value' is null.</exception>
public IDictionary<string, string> InboundClaimTypeMap
{
get
{
return _inboundClaimTypeMap;
}

set
{
_inboundClaimTypeMap = value ?? throw LogHelper.LogArgumentNullException(nameof(value));
}
}

internal static IDictionary<string, object> AddCtyClaimDefaultValue(IDictionary<string, object> additionalClaims, bool setDefaultCtyClaim)
{
if (!setDefaultCtyClaim)
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,7 +49,7 @@ public class JwtTokenUtilities
/// </summary>
/// <param name="input">String to be signed</param>
/// <param name="signingCredentials">The <see cref="SigningCredentials"/> that contain crypto specs used to sign the token.</param>
/// <returns>The bse64urlendcoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).</returns>
/// <returns>The base 64 url encoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).</returns>
/// <exception cref="ArgumentNullException">'input' or 'signingCredentials' is null.</exception>
public static string CreateEncodedSignature(string input, SigningCredentials signingCredentials)
{
Expand Down Expand Up @@ -83,7 +81,7 @@ public static string CreateEncodedSignature(string input, SigningCredentials sig
/// <param name="input">String to be signed</param>
/// <param name="signingCredentials">The <see cref="SigningCredentials"/> that contain crypto specs used to sign the token.</param>
/// <param name="cacheProvider">should the <see cref="SignatureProvider"/> be cached.</param>
/// <returns>The bse64urlendcoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).</returns>
/// <returns>The base 64 url encoded signature over the bytes obtained from UTF8Encoding.GetBytes( 'input' ).</returns>
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="signingCredentials"/> is null.</exception>
public static string CreateEncodedSignature(string input, SigningCredentials signingCredentials, bool cacheProvider)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,7 +38,7 @@ public class JwtSecurityTokenHandler : SecurityTokenHandler
/// <summary>
/// Default claim type mapping for inbound claims.
/// </summary>
public static IDictionary<string, string> DefaultInboundClaimTypeMap = ClaimTypeMapping.InboundClaimTypeMap;
public static IDictionary<string, string> DefaultInboundClaimTypeMap = new Dictionary<string, string>(ClaimTypeMapping.InboundClaimTypeMap);

/// <summary>
/// Default value for the flag that determines whether or not the InboundClaimTypeMap is used.
Expand All @@ -49,7 +48,7 @@ public class JwtSecurityTokenHandler : SecurityTokenHandler
/// <summary>
/// Default claim type mapping for outbound claims.
/// </summary>
public static IDictionary<string, string> DefaultOutboundClaimTypeMap = ClaimTypeMapping.OutboundClaimTypeMap;
public static IDictionary<string, string> DefaultOutboundClaimTypeMap = new Dictionary<string, string>(ClaimTypeMapping.OutboundClaimTypeMap);

/// <summary>
/// Default claim type filter list.
Expand Down
Loading

0 comments on commit 8e7f07e

Please sign in to comment.