Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Westinm/claims mapping #2082

Merged
merged 1 commit into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`private const _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);
brentschmaltz marked this conversation as resolved.
Show resolved Hide resolved

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically don't declare mutable static fields. This should be a property instead.

https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/field

his interpretation immediately implies that all fields must be private.

We exclude constant and static read-only fields from this strict restriction, because such fields, almost by definition, are never required to change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the above 2 static fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out, Eric. I will leave the default values, but include properties to get/set the private members.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want to make these properties as well. Exposing public static fields isn't recommended.


/// <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)
brentschmaltz marked this conversation as resolved.
Show resolved Hide resolved
_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
westin-m marked this conversation as resolved.
Show resolved Hide resolved
{
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