diff --git a/build/common.props b/build/common.props index 000c4f5136..001d7daab7 100644 --- a/build/common.props +++ b/build/common.props @@ -48,8 +48,4 @@ - - - - diff --git a/build/commonTest.props b/build/commonTest.props index ee3387d6d9..bdbdb21cfc 100644 --- a/build/commonTest.props +++ b/build/commonTest.props @@ -17,6 +17,7 @@ $(TestOnlyCoreTargets) $(DotNetCoreAppRuntimeVersion) false + 11 diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs b/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs index d916f9995e..aa1485358a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs @@ -34,7 +34,7 @@ [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateTokenAsync(System.String,Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,System.String,Microsoft.IdentityModel.Tokens.TokenValidationParameters)~System.Threading.Tasks.Task{Microsoft.IdentityModel.Tokens.TokenValidationResult}")] [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "Vendored component", Scope = "module")] [assembly: SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "It is used within a defined if condition", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.GetSecurityKey(Microsoft.IdentityModel.Tokens.EncryptingCredentials,Microsoft.IdentityModel.Tokens.CryptoProviderFactory,System.Collections.Generic.IDictionary{System.String,System.Object},System.Byte[]@)~Microsoft.IdentityModel.Tokens.SecurityKey")] -[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateTokenPrivate(System.String,Microsoft.IdentityModel.Tokens.SigningCredentials,Microsoft.IdentityModel.Tokens.EncryptingCredentials,System.String,System.Collections.Generic.IDictionary{System.String,System.Object},System.Collections.Generic.IDictionary{System.String,System.Object},System.String)~System.String")] +[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateToken(System.String,Microsoft.IdentityModel.Tokens.SigningCredentials,Microsoft.IdentityModel.Tokens.EncryptingCredentials,System.String,System.Collections.Generic.IDictionary{System.String,System.Object},System.Collections.Generic.IDictionary{System.String,System.Object},System.String)~System.String")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(System.String,Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "There are additional keys to check, the next one may be successful", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.GetContentEncryptionKeys(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~System.Collections.Generic.IEnumerable{Microsoft.IdentityModel.Tokens.SecurityKey}")] diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index cb8bf87678..67de58bc3c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Security.Claims; +using System.Text; using System.Text.Json; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; namespace Microsoft.IdentityModel.JsonWebTokens { @@ -18,121 +19,134 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// internal class JsonClaimSet { - internal static JsonClaimSet Empty { get; } = new JsonClaimSet("{}"u8.ToArray()); + internal const string ClassName = "Microsoft.IdentityModel.JsonWebTokens.JsonClaimSet"; + internal static JsonClaimSet Empty { get; } = new JsonClaimSet("{}"u8.ToArray()); + internal object _claimsLock = new(); + internal IDictionary _jsonClaims; private IList _claims; - internal JsonClaimSet(JsonDocument jsonDocument) + internal JsonClaimSet(IDictionary jsonClaims) { - // This method is assuming ownership of the JsonDocument, which is backed by one or more ArrayPool arrays. - // We need to dispose of it to avoid leaking arrays from the pool. To achieve that, we clone the root element, - // which will result in a new JsonElement being created that's not tied to the original and that's not backed by - // ArrayPool memory, after which point we can dispose of the original to return the array(s) to the pool. - RootElement = jsonDocument.RootElement.Clone(); - jsonDocument.Dispose(); + _jsonClaims = jsonClaims; } - - internal JsonClaimSet(byte[] jsonBytes) : this(JsonDocument.Parse(jsonBytes)) + internal JsonClaimSet(byte[] jsonUtf8Bytes) { + _jsonClaims = JwtTokenUtilities.CreateClaimsDictionary(jsonUtf8Bytes, jsonUtf8Bytes.Length); } - internal JsonClaimSet(string json) : this(JsonDocument.Parse(json)) + internal JsonClaimSet(string json) : this(Encoding.UTF8.GetBytes(json)) { } - internal JsonElement RootElement { get; } - internal IList Claims(string issuer) { - if (_claims != null) - return _claims; + if (_claims == null) + lock (_claimsLock) + _claims ??= CreateClaims(issuer); - _claims = CreateClaims(issuer); return _claims; } internal IList CreateClaims(string issuer) { IList claims = new List(); - foreach (JsonProperty property in RootElement.EnumerateObject()) + foreach (KeyValuePair kvp in _jsonClaims) + CreateClaimFromObject(claims, kvp.Key, kvp.Value, issuer); + + return claims; + } + + internal static void CreateClaimFromObject(IList claims, string claimType, object value, string issuer) + { + // Json.net recognized DateTime by default. + if (value is string str) + claims.Add(new Claim(claimType, str, JwtTokenUtilities.GetStringClaimValueType(str), issuer, issuer)); + else if (value is int i) + claims.Add(new Claim(claimType, i.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer32, issuer, issuer)); + else if (value is long l) + claims.Add(new Claim(claimType, l.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64, issuer, issuer)); + else if (value is bool b) + claims.Add(new Claim(claimType, b.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Boolean, issuer, issuer)); + else if (value is double d) + claims.Add(new Claim(claimType, d.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer)); + else if (value is DateTime dt) + claims.Add(new Claim(claimType, dt.ToString("o",CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); + else if (value is float f) + claims.Add(new Claim(claimType, f.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer)); + else if (value is decimal m) + claims.Add(new Claim(claimType, m.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer)); + else if (value is null) + claims.Add(new Claim(claimType, string.Empty, JsonClaimValueTypes.JsonNull, issuer, issuer)); + else if (value is IList ilist) { - if (property.Value.ValueKind == JsonValueKind.Array) + foreach (var item in ilist) + CreateClaimFromObject(claims, claimType, item, issuer); + } + else if (value is JsonElement j) + if (j.ValueKind == JsonValueKind.Array) { - foreach (JsonElement jsonElement in property.Value.EnumerateArray()) + foreach (JsonElement jsonElement in j.EnumerateArray()) { - Claim claim = CreateClaimFromJsonElement(property.Name, issuer, jsonElement); + Claim claim = CreateClaimFromJsonElement(claimType, issuer, jsonElement); if (claim != null) claims.Add(claim); } } else { - Claim claim = CreateClaimFromJsonElement(property.Name, issuer, property.Value); + Claim claim = CreateClaimFromJsonElement(claimType, issuer, j); if (claim != null) claims.Add(claim); } - } - - return claims; } - private static Claim CreateClaimFromJsonElement(string key, string issuer, JsonElement jsonElement) + internal static Claim CreateClaimFromJsonElement(string claimType, string issuer, JsonElement value) { // Json.net recognized DateTime by default. - if (jsonElement.ValueKind == JsonValueKind.String) + if (value.ValueKind == JsonValueKind.String) { - try - { - if (jsonElement.TryGetDateTime(out DateTime dateTimeValue)) - return new Claim(key, dateTimeValue.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer); - else - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.String, issuer, issuer); - } - catch(IndexOutOfRangeException) - { - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.String, issuer, issuer); - } + string claimValue = value.ToString(); + return new Claim(claimType, claimValue, JwtTokenUtilities.GetStringClaimValueType(claimValue), issuer, issuer); } - else if (jsonElement.ValueKind == JsonValueKind.Null) - return new Claim(key, string.Empty, JsonClaimValueTypes.JsonNull, issuer, issuer); - else if (jsonElement.ValueKind == JsonValueKind.Object) - return new Claim(key, jsonElement.ToString(), JsonClaimValueTypes.Json, issuer, issuer); - else if (jsonElement.ValueKind == JsonValueKind.False) - return new Claim(key, "false", ClaimValueTypes.Boolean, issuer, issuer); - else if (jsonElement.ValueKind == JsonValueKind.True) - return new Claim(key, "true", ClaimValueTypes.Boolean, issuer, issuer); - else if (jsonElement.ValueKind == JsonValueKind.Number) + else if (value.ValueKind == JsonValueKind.Null) + return new Claim(claimType, string.Empty, JsonClaimValueTypes.JsonNull, issuer, issuer); + else if (value.ValueKind == JsonValueKind.Object) + return new Claim(claimType, value.ToString(), JsonClaimValueTypes.Json, issuer, issuer); + else if (value.ValueKind == JsonValueKind.False) + return new Claim(claimType, "False", ClaimValueTypes.Boolean, issuer, issuer); + else if (value.ValueKind == JsonValueKind.True) + return new Claim(claimType, "True", ClaimValueTypes.Boolean, issuer, issuer); + else if (value.ValueKind == JsonValueKind.Number) { - if (jsonElement.TryGetInt16(out short _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.Integer, issuer, issuer); - else if (jsonElement.TryGetInt32(out int _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.Integer, issuer, issuer); - else if (jsonElement.TryGetInt64(out long _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.Integer64, issuer, issuer); - else if (jsonElement.TryGetDecimal(out decimal _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.Double, issuer, issuer); - else if (jsonElement.TryGetDouble(out double _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.Double, issuer, issuer); - else if (jsonElement.TryGetUInt32(out uint _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.UInteger32, issuer, issuer); - else if (jsonElement.TryGetUInt64(out ulong _)) - return new Claim(key, jsonElement.ToString(), ClaimValueTypes.UInteger64, issuer, issuer); + if (value.TryGetInt32(out int i)) + return new Claim(claimType, i.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer, issuer, issuer); + else if (value.TryGetInt64(out long l)) + return new Claim(claimType, l.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64, issuer, issuer); + else if (value.TryGetUInt32(out uint u)) + return new Claim(claimType, u.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.UInteger32, issuer, issuer); + else if (value.TryGetDouble(out double d)) + return new Claim(claimType, d.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer); + else if (value.TryGetDecimal(out decimal m)) + return new Claim(claimType, m.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer); + else if (value.TryGetUInt64(out ulong ul)) + return new Claim(claimType, ul.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.UInteger64, issuer, issuer); } - else if (jsonElement.ValueKind == JsonValueKind.Array) + else if (value.ValueKind == JsonValueKind.Array) { - return new Claim(key, jsonElement.ToString(), JsonClaimValueTypes.JsonArray, issuer, issuer); + return new Claim(claimType, value.ToString(), JsonClaimValueTypes.JsonArray, issuer, issuer); } return null; } - private static object CreateObjectFromJsonElement(JsonElement jsonElement) + internal static object CreateObjectFromJsonElement(JsonElement jsonElement) { if (jsonElement.ValueKind == JsonValueKind.Array) { int numberOfElements = 0; // is this an array of properties - foreach(JsonElement element in jsonElement.EnumerateArray()) + foreach (JsonElement element in jsonElement.EnumerateArray()) numberOfElements++; object[] objects = new object[numberOfElements]; @@ -180,67 +194,38 @@ private static object CreateObjectFromJsonElement(JsonElement jsonElement) internal Claim GetClaim(string key, string issuer) { if (key == null) - throw new ArgumentNullException(nameof(key)); - - if (!RootElement.TryGetProperty(key, out JsonElement jsonElement)) - throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14304, key))); - - return CreateClaimFromJsonElement(key, issuer, jsonElement); - } - - internal static string GetClaimValueType(object obj) - { - if (obj == null) - return JsonClaimValueTypes.JsonNull; - - var objType = obj.GetType(); - - if (objType == typeof(string)) - return ClaimValueTypes.String; - - if (objType == typeof(int)) - return ClaimValueTypes.Integer; - - if (objType == typeof(bool)) - return ClaimValueTypes.Boolean; - - if (objType == typeof(double)) - return ClaimValueTypes.Double; + throw LogHelper.LogArgumentNullException(nameof(key)); - if (objType == typeof(long)) + if (_jsonClaims.TryGetValue(key, out object _)) { - long l = (long)obj; - if (l >= int.MinValue && l <= int.MaxValue) - return ClaimValueTypes.Integer; - - return ClaimValueTypes.Integer64; + foreach (var claim in Claims(issuer)) + if (claim.Type == key) + return claim; } - if (objType == typeof(DateTime)) - return ClaimValueTypes.DateTime; - - return objType.ToString(); + throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14304, key))); } internal string GetStringValue(string key) { - if (RootElement.TryGetProperty(key, out JsonElement jsonElement) && jsonElement.ValueKind == JsonValueKind.String) - return jsonElement.GetString(); + if (_jsonClaims.TryGetValue(key, out object obj)) + return obj.ToString(); return string.Empty; } internal DateTime GetDateTime(string key) { - if (!RootElement.TryGetProperty(key, out JsonElement jsonElement)) + if (!_jsonClaims.TryGetValue(key, out object value)) return DateTime.MinValue; - return EpochTime.DateTime(Convert.ToInt64(Math.Truncate((double)ParseTimeValue(key, jsonElement)))); + return EpochTime.DateTime(Convert.ToInt64(Math.Truncate((double)GetValueAsLong(key, value)))); } internal T GetValue(string key) { - return GetValue(key, true, out bool _); + T retval = GetValue(key, true, out bool _); + return retval; } /// @@ -256,7 +241,8 @@ internal T GetValue(string key) /// internal T GetValue(string key, bool throwEx, out bool found) { - found = RootElement.TryGetProperty(key, out JsonElement jsonElement); + found = _jsonClaims.TryGetValue(key, out object obj); + if (!found) { if (throwEx) @@ -265,104 +251,145 @@ internal T GetValue(string key, bool throwEx, out bool found) return default; } - if (typeof(T) == typeof(JsonElement)) - return (T)(object)jsonElement; + if (obj == null) + if (typeof(T) == typeof(object) || typeof(T).IsClass || Nullable.GetUnderlyingType(typeof(T)) != null) + { + return (T)(object)null; + } + else + { + found = false; + return default; + } + + Type objType = obj.GetType(); + + if (typeof(T) == objType) + return (T)(obj); + + if (typeof(T) == typeof(object)) + return (T)obj; - try + if (typeof(T) == typeof(string)) + return (T)((object)obj.ToString()); + + if (typeof(T) == typeof(IList)) { - if (jsonElement.ValueKind == JsonValueKind.Null) + List list = new(); + if (obj is IList iList) { - if (typeof(T) == typeof(object) || typeof(T).IsClass || Nullable.GetUnderlyingType(typeof(T)) != null) - return (T)(object)null; - else - { - found = false; - return default; - } + foreach (object item in iList) + list.Add(item?.ToString()); + + return (T)((object)list); } else { - if (typeof(T) == typeof(JObject)) - return (T)(object)(JObject.Parse(jsonElement.ToString())); + list.Add(obj.ToString()); + } + return (T)(object)list; + } - if (typeof(T) == typeof(JArray)) - return (T)(object)(JArray.Parse(jsonElement.ToString())); + if (typeof(T) == typeof(int) && int.TryParse(obj.ToString(), out int i)) + return (T)(object)i; - if (typeof(T) == typeof(object)) - return (T)CreateObjectFromJsonElement(jsonElement); + if (typeof(T) == typeof(long) && long.TryParse(obj.ToString(), out long l)) + return (T)(object)l; - if (typeof(T) == typeof(object[])) - { - if (jsonElement.ValueKind == JsonValueKind.Array) - { - int numberOfElements = 0; - // is this an array of properties - foreach (JsonElement element in jsonElement.EnumerateArray()) - numberOfElements++; - - object[] objects = new object[numberOfElements]; - - int index = 0; - foreach (JsonElement element in jsonElement.EnumerateArray()) - objects[index++] = CreateObjectFromJsonElement(element); - - return (T)(object)objects; - } - else - { - object[] objects = new object[1]; - objects[0] = CreateObjectFromJsonElement(jsonElement); - return (T)(object)objects; - } - } + if (typeof(T) == typeof(double) && double.TryParse(obj.ToString(), out double d)) + return (T)(object)d; - if (typeof(T) == typeof(string)) - return (T)(jsonElement.ToString() as object); + if (typeof(T) == typeof(DateTime) && DateTime.TryParse(obj.ToString(), out DateTime dt)) + return (T)(object)dt; - if (jsonElement.ValueKind == JsonValueKind.String) - { - if (typeof(T) == typeof(long) && long.TryParse(jsonElement.ToString(), out long lresult)) - return (T)(object)lresult; + if (typeof(T) == typeof(uint) && uint.TryParse(obj.ToString(), out uint u)) + return (T)(object)u; - if (typeof(T) == typeof(int) && int.TryParse(jsonElement.ToString(), out int iresult)) - return (T)(object)iresult; + if (typeof(T) == typeof(float) && float.TryParse(obj.ToString(), out float f)) + return (T)(object)f; - if (typeof(T) == typeof(DateTime)) - if (DateTime.TryParse(jsonElement.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime dateTime)) - return (T)(object)dateTime; - else - return JsonSerializer.Deserialize(jsonElement.GetRawText()); + if (typeof(T) == typeof(decimal) && decimal.TryParse(obj.ToString(), out decimal m)) + return (T)(object)m; - if (typeof(T) == typeof(double) && double.TryParse(jsonElement.ToString(), out double dresult)) - return (T)(object)dresult; + if (typeof(T) == typeof(IList)) + { + List list = new(); + if (obj is IList items) + { + foreach (object item in items) + list.Add(item); + } + else + { + list.Add(obj); + } + return (T)((object)list); + } - if (typeof(T) == typeof(float) && float.TryParse(jsonElement.ToString(), out float fresult)) - return (T)(object)fresult; + if (typeof(T) == typeof(int[])) + { + int[] ints; + if (obj is IList ilist) + { + ints = new int[ilist.Count]; + int index = 0; + foreach (object item in ilist) + { + if (typeof(int) == item.GetType()) + ints[index++] = (int)item; } - return JsonSerializer.Deserialize(jsonElement.GetRawText()); + // all items must be int + if (index == ilist.Count) + return (T)(object)(int[])ints; + } + else if (objType == typeof(int)) + { + ints = new int[]{(int)obj}; + return (T)(object)(int[])ints; } } - catch (Exception ex) + + if (typeof(T) == typeof(object[])) { - found = false; - if (throwEx) - throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14305, key, typeof(T), jsonElement.ValueKind, jsonElement.GetRawText()), ex)); + object[] objects; + if (obj is IList ilist) + { + objects = new object[ilist.Count]; + int index = 0; + foreach (object item in ilist) + objects[index++] = item; + } + else + { + objects = new object[] { obj }; + } + + return (T)(object)(object[])objects; } + + found = false; + if (throwEx) + throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14305, key, typeof(T), objType, obj.ToString()))); + return default; } internal bool TryGetClaim(string key, string issuer, out Claim claim) { - if (!RootElement.TryGetProperty(key, out JsonElement jsonElement)) - { - claim = null; + claim = null; + if (!_jsonClaims.TryGetValue(key, out object value)) return false; - } - claim = CreateClaimFromJsonElement(key, issuer, jsonElement); - return true; + foreach (Claim c in Claims(issuer)) + if (c.Type == key) + { + claim = c; + return true; + } + + return false; } /// @@ -383,40 +410,51 @@ internal bool TryGetValue(string key, out T value) internal bool HasClaim(string claimName) { - return RootElement.TryGetProperty(claimName, out _); + return _jsonClaims.TryGetValue(claimName, out _); } - private static long ParseTimeValue(string claimName, JsonElement jsonElement) + private static long GetValueAsLong(string claimName, object obj) { - if (jsonElement.ValueKind == JsonValueKind.Number) - { - if (jsonElement.TryGetInt64(out long retValLong)) - return retValLong; + if (obj is int i) + return (long)i; - if (jsonElement.TryGetDouble(out double retValDouble)) - return (long)retValDouble; + if (obj is long) + return (long)(obj); - if (jsonElement.TryGetInt32(out int retValInt)) - return retValInt; + if (obj is double d) + return (long)d; - if (jsonElement.TryGetDecimal(out decimal retValDecimal)) - return (long)retValDecimal; - } + if (obj is uint u) + return (long)u; - if (jsonElement.ValueKind == JsonValueKind.String) + if (obj is float f) + return (long)f; + + if (obj is decimal m) + return (long) m; + + if (obj is string str) { - string str = jsonElement.GetString(); - if (long.TryParse(str, out long resultLong)) - return resultLong; + if (int.TryParse(str, out int ii)) + return (long)ii; + + if (long.TryParse(str, out long l)) + return l; + + if (double.TryParse(str, out double dd)) + return (long)dd; + + if (uint.TryParse(str, out uint uu)) + return (long)uu; - if (float.TryParse(str, out float resultFloat)) - return (long)resultFloat; + if (float.TryParse(str, out float ff)) + return (long)ff; - if (double.TryParse(str, out double resultDouble)) - return (long)resultDouble; + if (decimal.TryParse(str, out decimal mm)) + return (long)mm; } - throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX14300, claimName, jsonElement.ToString(), typeof(long)))); + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX14300, claimName, obj?.ToString(), typeof(long)))); } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonClaimValueTypes.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonClaimValueTypes.cs index 4e32ec0a70..796da985db 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonClaimValueTypes.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonClaimValueTypes.cs @@ -1,29 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Security.Claims; + namespace Microsoft.IdentityModel.JsonWebTokens { /// - /// Constants for Json Web tokens. + /// Constants that indicate how the should be evaluated. /// public static class JsonClaimValueTypes { /// - /// A URI that represents the JSON XML data type. + /// A value that indicates the is a Json object. /// - /// When mapping json to .Net Claim(s), if the value was not a string (or an enumeration of strings), the ClaimValue will serialized using the current JSON serializer, a property will be added with the .Net type and the ClaimTypeValue will be set to 'JsonClaimValueType'. + /// When creating a from Json to if the value was not a simple type {String, Null, True, False, Number} + /// then will contain the Json value. If the Json was a JsonObject, the will be set to "JSON". public const string Json = "JSON"; /// - /// A URI that represents the JSON array XML data type. + /// A value that indicates the is a Json object. /// - /// When mapping json to .Net Claim(s), if the value was not a string (or an enumeration of strings), the ClaimValue will serialized using the current JSON serializer, a property will be added with the .Net type and the ClaimTypeValue will be set to 'JsonClaimValueType'. + /// When creating a from Json to if the value was not a simple type {String, Null, True, False, Number} + /// then will contain the Json value. If the Json was a JsonArray, the will be set to "JSON_ARRAY". public const string JsonArray = "JSON_ARRAY"; /// - /// A URI that represents the JSON null data type + /// A value that indicates the is Json null. /// - /// When mapping json to .Net Claim(s), we use empty string to represent the claim value and set the ClaimValueType to JsonNull + /// When creating a the cannot be null. The the Json value was nil, then the + /// will be set to and the will be set to "JSON_NULL". public const string JsonNull = "JSON_NULL"; } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 7ad4cf9155..7540345a7a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -5,11 +5,9 @@ using System.Collections.Generic; using System.Security.Claims; using System.Text; -using System.Text.Json; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; - namespace Microsoft.IdentityModel.JsonWebTokens { /// @@ -17,6 +15,7 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// public class JsonWebToken : SecurityToken { + internal object _audienceLock = new(); private ClaimsIdentity _claimsIdentity; private bool _wasClaimsIdentitySet; @@ -428,7 +427,7 @@ private void ReadToken(string encodedJson) IsSigned = !(Dot2 + 1 == encodedJson.Length); try { - Header = new JsonClaimSet(JwtTokenUtilities.GetJsonDocumentFromBase64UrlEncodedString(encodedJson, 0, Dot1)); + Header = new JsonClaimSet(JwtTokenUtilities.ParseJsonBytes(encodedJson, 0, Dot1)); } catch (Exception ex) { @@ -437,7 +436,7 @@ private void ReadToken(string encodedJson) try { - Payload = new JsonClaimSet(JwtTokenUtilities.GetJsonDocumentFromBase64UrlEncodedString(encodedJson, Dot1 + 1, Dot2 - Dot1 - 1)); + Payload = new JsonClaimSet(JwtTokenUtilities.ParseJsonBytes(encodedJson, Dot1 + 1, Dot2 - Dot1 - 1)); } catch (Exception ex) { @@ -613,18 +612,16 @@ public IEnumerable Audiences { if (_audiences == null) { - _audiences = new List(); - - if (Payload.TryGetValue(JwtRegisteredClaimNames.Aud, out JsonElement audiences)) + lock (_audiences) { - if (audiences.ValueKind == JsonValueKind.String) + if (_audiences == null) { - _audiences.Add(audiences.GetString()); - } - else if (audiences.ValueKind == JsonValueKind.Array) - { - foreach (JsonElement jsonElement in audiences.EnumerateArray()) - _audiences.Add(jsonElement.ToString()); + _audiences = new List(); + if (Payload.TryGetValue(JwtRegisteredClaimNames.Aud, out IList audiences)) + { + foreach (string str in audiences) + _audiences.Add(str); + } } } } @@ -633,11 +630,6 @@ public IEnumerable Audiences } } - internal override IEnumerable CreateClaims(string issuer) - { - return Payload.CreateClaims(issuer); - } - /// /// Gets a where each claim in the JWT { name, value } is returned as a . /// diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 07c8b9af88..9c3d925fbe 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -3,17 +3,19 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Security.Claims; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace Microsoft.IdentityModel.JsonWebTokens @@ -168,7 +170,7 @@ public virtual bool CanReadToken(string token) // Count the number of segments, which is the number of periods + 1. We can stop when we've encountered // more segments than the maximum we know how to handle. int pos = 0; - int segmentCount = 1; // TODO: Use MemoryExtensions.Count in .NET 8 + int segmentCount = 1; while (segmentCount <= JwtConstants.MaxJwtSegmentCount && ((pos = token.IndexOf('.', pos)) >= 0)) { pos++; @@ -198,59 +200,6 @@ public virtual bool CanValidateToken get { return true; } } - private static JObject CreateDefaultJWEHeader(EncryptingCredentials encryptingCredentials, string compressionAlgorithm, string tokenType) - { - var header = new JObject(); - header.Add(JwtHeaderParameterNames.Alg, encryptingCredentials.Alg); - header.Add(JwtHeaderParameterNames.Enc, encryptingCredentials.Enc); - - if (!string.IsNullOrEmpty(encryptingCredentials.Key.KeyId)) - header.Add(JwtHeaderParameterNames.Kid, encryptingCredentials.Key.KeyId); - - if (!string.IsNullOrEmpty(compressionAlgorithm)) - header.Add(JwtHeaderParameterNames.Zip, compressionAlgorithm); - - if (string.IsNullOrEmpty(tokenType)) - header.Add(JwtHeaderParameterNames.Typ, JwtConstants.HeaderType); - else - header.Add(JwtHeaderParameterNames.Typ, tokenType); - - return header; - } - - private static JObject CreateDefaultJWSHeader(SigningCredentials signingCredentials, string tokenType) - { - JObject header = null; - - if (signingCredentials == null) - { - header = new JObject() - { - {JwtHeaderParameterNames.Alg, SecurityAlgorithms.None } - }; - } - else - { - header = new JObject() - { - { JwtHeaderParameterNames.Alg, signingCredentials.Algorithm } - }; - - if (signingCredentials.Key.KeyId != null) - header.Add(JwtHeaderParameterNames.Kid, signingCredentials.Key.KeyId); - - if (signingCredentials.Key is X509SecurityKey x509SecurityKey) - header[JwtHeaderParameterNames.X5t] = x509SecurityKey.X5t; - } - - if (string.IsNullOrEmpty(tokenType)) - header.Add(JwtHeaderParameterNames.Typ, JwtConstants.HeaderType); - else - header.Add(JwtHeaderParameterNames.Typ, tokenType); - - return header; - } - /// /// Creates an unsigned JWS (Json Web Signature). /// @@ -262,7 +211,7 @@ public virtual string CreateToken(string payload) if (string.IsNullOrEmpty(payload)) throw LogHelper.LogArgumentNullException(nameof(payload)); - return CreateTokenPrivate(payload, null, null, null, null, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), null, null, null, null, null, null); } /// @@ -281,7 +230,7 @@ public virtual string CreateToken(string payload, IDictionary ad if (additionalHeaderClaims == null) throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); - return CreateTokenPrivate(payload, null, null, null, additionalHeaderClaims, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), null, null, null, additionalHeaderClaims, null, null); } /// @@ -300,7 +249,7 @@ public virtual string CreateToken(string payload, SigningCredentials signingCred if (signingCredentials == null) throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); - return CreateTokenPrivate(payload, signingCredentials, null, null, null, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), signingCredentials, null, null, null, null, null); } /// @@ -327,7 +276,7 @@ public virtual string CreateToken(string payload, SigningCredentials signingCred if (additionalHeaderClaims == null) throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); - return CreateTokenPrivate(payload, signingCredentials, null, null, additionalHeaderClaims, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), signingCredentials, null, null, additionalHeaderClaims, null, null); } /// @@ -337,28 +286,191 @@ public virtual string CreateToken(string payload, SigningCredentials signingCred /// A JWS in Compact Serialization Format. public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) { - if (tokenDescriptor == null) - throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor)); + _ = tokenDescriptor ?? throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor)); if (LogHelper.IsEnabled(EventLogLevel.Warning)) { if ((tokenDescriptor.Subject == null || !tokenDescriptor.Subject.Claims.Any()) && (tokenDescriptor.Claims == null || !tokenDescriptor.Claims.Any())) - LogHelper.LogWarning(LogMessages.IDX14114, LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor)), LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor.Subject)), LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor.Claims))); + LogHelper.LogWarning( + LogMessages.IDX14114, LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor)), LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor.Subject)), LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor.Claims))); } - JObject payload; - if (tokenDescriptor.Subject != null) - payload = JObject.FromObject(TokenUtilities.CreateDictionaryFromClaims(tokenDescriptor.Subject.Claims)); - else - payload = new JObject(); + return CreateToken( + WritePayload(tokenDescriptor), + tokenDescriptor.SigningCredentials, + tokenDescriptor.EncryptingCredentials, + tokenDescriptor.CompressionAlgorithm, + tokenDescriptor.AdditionalHeaderClaims, + tokenDescriptor.AdditionalInnerHeaderClaims, + tokenDescriptor.TokenType); + } + + internal static byte[] WriteJwsHeader( + SigningCredentials signingCredentials, + string tokenType, + IDictionary jwsHeaderClaims, + IDictionary jweHeaderClaims) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + Utf8JsonWriter writer = null; + try + { + writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartObject(); + + if (signingCredentials == null) + { + writer.WriteString(JwtHeaderUtf8Bytes.Alg, SecurityAlgorithms.None); + } + else + { + writer.WriteString(JwtHeaderUtf8Bytes.Alg, signingCredentials.Algorithm); + if (signingCredentials.Key.KeyId != null) + writer.WriteString(JwtHeaderUtf8Bytes.Kid, signingCredentials.Key.KeyId); + + if (signingCredentials.Key is X509SecurityKey x509SecurityKey) + writer.WriteString(JwtHeaderUtf8Bytes.X5t, x509SecurityKey.X5t); + } + + bool useJwsHeaderClaims = jwsHeaderClaims != null && jwsHeaderClaims.Count > 0; + bool typeWritten = false; + + // Priority is jwsHeaderClaims, jweHeaderClaims, default + if (jweHeaderClaims != null && jweHeaderClaims.Count > 0) + { + foreach (KeyValuePair kvp in jweHeaderClaims) + { + if (useJwsHeaderClaims && jwsHeaderClaims.ContainsKey(kvp.Key)) + continue; + + JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); + if (!typeWritten && kvp.Key.Equals(JwtHeaderParameterNames.Typ, StringComparison.Ordinal)) + typeWritten = true; + } + } + + if (useJwsHeaderClaims) + { + foreach (KeyValuePair kvp in jwsHeaderClaims) + { + JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); + if (!typeWritten && kvp.Key.Equals(JwtHeaderParameterNames.Typ, StringComparison.Ordinal)) + typeWritten = true; + } + } + + if (!typeWritten) + writer.WriteString(JwtHeaderUtf8Bytes.Typ, string.IsNullOrEmpty(tokenType) ? JwtConstants.HeaderType : tokenType); + + writer.WriteEndObject(); + writer.Flush(); + + return memoryStream.ToArray(); + } + finally + { + writer?.Dispose(); + } + } + } + + internal static byte[] WriteJweHeader( + EncryptingCredentials encryptingCredentials, + string compressionAlgorithm, + string tokenType, + IDictionary jweHeaderClaims) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + Utf8JsonWriter writer = null; + try + { + writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartObject(); + + writer.WriteString(JwtHeaderUtf8Bytes.Alg, encryptingCredentials.Alg); + writer.WriteString(JwtHeaderUtf8Bytes.Enc, encryptingCredentials.Enc); + + if (encryptingCredentials.Key.KeyId != null) + writer.WriteString(JwtHeaderUtf8Bytes.Kid, encryptingCredentials.Key.KeyId); + + if (!string.IsNullOrEmpty(compressionAlgorithm)) + writer.WriteString(JwtHeaderUtf8Bytes.Zip, compressionAlgorithm); + + bool typeWritten = false; + bool ctyWritten = !encryptingCredentials.SetDefaultCtyClaim; + + // Current 6x Priority is jweHeaderClaims, type, cty + if (jweHeaderClaims != null && jweHeaderClaims.Count > 0) + { + foreach (KeyValuePair kvp in jweHeaderClaims) + { + JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); + if (!typeWritten && kvp.Key.Equals(JwtHeaderParameterNames.Typ, StringComparison.Ordinal)) + typeWritten = true; + else if (!ctyWritten && kvp.Key.Equals(JwtHeaderParameterNames.Cty, StringComparison.Ordinal)) + ctyWritten = true; + } + } + + if (!typeWritten) + writer.WriteString(JwtHeaderUtf8Bytes.Typ, string.IsNullOrEmpty(tokenType) ? JwtConstants.HeaderType : tokenType); + + if (!ctyWritten) + writer.WriteString(JwtHeaderUtf8Bytes.Cty, JwtConstants.HeaderType); + + writer.WriteEndObject(); + writer.Flush(); + + return memoryStream.ToArray(); + } + finally + { + writer?.Dispose(); + } + } + } + + internal byte[] WritePayload(SecurityTokenDescriptor tokenDescriptor) + { + bool audienceSet = !string.IsNullOrEmpty(tokenDescriptor.Audience); + bool issuerSet = !string.IsNullOrEmpty(tokenDescriptor.Issuer); + + IDictionary payload = TokenUtilities.CreateDictionaryFromClaims(tokenDescriptor.Subject?.Claims, tokenDescriptor, audienceSet, issuerSet); + + // Duplicates are resolved according to the following priority: + // SecurityTokenDescriptor.{Audience, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims - // If a key is present in both tokenDescriptor.Subject.Claims and tokenDescriptor.Claims, the value present in tokenDescriptor.Claims is the - // one that takes precedence and will remain after the merge. Key comparison is case sensitive. if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) - payload.Merge(JObject.FromObject(tokenDescriptor.Claims), new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); + { + foreach (var kvp in tokenDescriptor.Claims) + { + if (audienceSet && kvp.Key.Equals("aud", StringComparison.Ordinal)) + continue; + + if (issuerSet && kvp.Key.Equals("iss", StringComparison.Ordinal)) + continue; + + if (tokenDescriptor.Expires.HasValue && kvp.Key.Equals("exp", StringComparison.Ordinal)) + continue; + + if (tokenDescriptor.IssuedAt.HasValue && kvp.Key.Equals("iat", StringComparison.Ordinal)) + continue; - if (tokenDescriptor.Audience != null) + if (tokenDescriptor.NotBefore.HasValue && kvp.Key.Equals("nbf", StringComparison.Ordinal)) + continue; + + payload[kvp.Key] = kvp.Value; + } + } + + bool expiresSet = false; + bool nbfSet = false; + bool iatSet = false; + + if (audienceSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Aud)) LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Audience)))); @@ -366,20 +478,21 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) payload[JwtRegisteredClaimNames.Aud] = tokenDescriptor.Audience; } - if (tokenDescriptor.Expires.HasValue) + if (issuerSet) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Exp)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Iss)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Issuer)))); - payload[JwtRegisteredClaimNames.Exp] = EpochTime.GetIntDate(tokenDescriptor.Expires.Value); + payload[JwtRegisteredClaimNames.Iss] = tokenDescriptor.Issuer; } - if (tokenDescriptor.Issuer != null) + if (tokenDescriptor.Expires.HasValue) { - if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Iss)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Issuer)))); + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Exp)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); - payload[JwtRegisteredClaimNames.Iss] = tokenDescriptor.Issuer; + payload[JwtRegisteredClaimNames.Exp] = EpochTime.GetIntDate(tokenDescriptor.Expires.Value); + expiresSet = true; } if (tokenDescriptor.IssuedAt.HasValue) @@ -388,6 +501,7 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.IssuedAt)))); payload[JwtRegisteredClaimNames.Iat] = EpochTime.GetIntDate(tokenDescriptor.IssuedAt.Value); + iatSet = true; } if (tokenDescriptor.NotBefore.HasValue) @@ -396,16 +510,59 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.NotBefore)))); payload[JwtRegisteredClaimNames.Nbf] = EpochTime.GetIntDate(tokenDescriptor.NotBefore.Value); + nbfSet = true; } - return CreateTokenPrivate( - payload.ToString(Formatting.None), - tokenDescriptor.SigningCredentials, - tokenDescriptor.EncryptingCredentials, - tokenDescriptor.CompressionAlgorithm, - tokenDescriptor.AdditionalHeaderClaims, - tokenDescriptor.AdditionalInnerHeaderClaims, - tokenDescriptor.TokenType); + // by default we set these three properties only if they haven't been set. + if (SetDefaultTimesOnTokenCreation) + { + long now = EpochTime.GetIntDate(DateTime.UtcNow); + + if (!expiresSet && !payload.ContainsKey(JwtRegisteredClaimNames.Exp)) + payload.Add(JwtRegisteredClaimNames.Exp, now + TokenLifetimeInMinutes * 60); + + if (!iatSet && !payload.ContainsKey(JwtRegisteredClaimNames.Iat)) + payload.Add(JwtRegisteredClaimNames.Iat, now); + + if (!nbfSet && !payload.ContainsKey(JwtRegisteredClaimNames.Nbf)) + payload.Add(JwtRegisteredClaimNames.Nbf, now); + } + + using (MemoryStream memoryStream = new()) + { + Utf8JsonWriter writer = null; + try + { + writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartObject(); + + foreach (KeyValuePair kvp in payload) + { + if (kvp.Value is IList l) + { + writer.WriteStartArray(kvp.Key); + + foreach (object obj in l) + JsonPrimitives.WriteObjectValue(ref writer, obj); + + writer.WriteEndArray(); + } + else + { + JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); + } + } + + writer.WriteEndObject(); + writer.Flush(); + + return memoryStream.ToArray(); + } + finally + { + writer?.Dispose(); + } + } } /// @@ -422,7 +579,7 @@ public virtual string CreateToken(string payload, EncryptingCredentials encrypti if (encryptingCredentials == null) throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); - return CreateTokenPrivate(payload, null, encryptingCredentials, null, null, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), null, encryptingCredentials, null, null, null, null); } /// @@ -449,7 +606,7 @@ public virtual string CreateToken(string payload, EncryptingCredentials encrypti if (additionalHeaderClaims == null) throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); - return CreateTokenPrivate(payload, null, encryptingCredentials, null, additionalHeaderClaims, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), null, encryptingCredentials, null, additionalHeaderClaims, null, null); } /// @@ -473,7 +630,7 @@ public virtual string CreateToken(string payload, SigningCredentials signingCred if (encryptingCredentials == null) throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); - return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, null, null, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), signingCredentials, encryptingCredentials, null, null, null, null); } /// @@ -509,7 +666,7 @@ public virtual string CreateToken( if (additionalHeaderClaims == null) throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); - return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, null, additionalHeaderClaims, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), signingCredentials, encryptingCredentials, null, additionalHeaderClaims, null, null); } /// @@ -530,7 +687,7 @@ public virtual string CreateToken(string payload, EncryptingCredentials encrypti if (string.IsNullOrEmpty(compressionAlgorithm)) throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); - return CreateTokenPrivate(payload, null, encryptingCredentials, compressionAlgorithm, null, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), null, encryptingCredentials, compressionAlgorithm, null, null, null); } /// @@ -559,7 +716,7 @@ public virtual string CreateToken(string payload, SigningCredentials signingCred if (string.IsNullOrEmpty(compressionAlgorithm)) throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); - return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, compressionAlgorithm, null, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), signingCredentials, encryptingCredentials, compressionAlgorithm, null, null, null); } /// @@ -606,8 +763,8 @@ public virtual string CreateToken( if (additionalInnerHeaderClaims == null) throw LogHelper.LogArgumentNullException(nameof(additionalInnerHeaderClaims)); - return CreateTokenPrivate( - payload, + return CreateToken( + Encoding.UTF8.GetBytes(payload), signingCredentials, encryptingCredentials, compressionAlgorithm, @@ -655,11 +812,12 @@ public virtual string CreateToken( if (additionalHeaderClaims == null) throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); - return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, compressionAlgorithm, additionalHeaderClaims, null, null); + return CreateToken(Encoding.UTF8.GetBytes(payload), signingCredentials, encryptingCredentials, compressionAlgorithm, additionalHeaderClaims, null, null); } - private string CreateTokenPrivate( - string payload, + internal static string CreateToken + ( + byte[] payloadBytes, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, string compressionAlgorithm, @@ -667,76 +825,38 @@ private string CreateTokenPrivate( IDictionary additionalInnerHeaderClaims, string tokenType) { + // TODO - Create one Span to write everything into and avoid moving between string -> Utf8Bytes -> string + string -> Utf8bytes. + // The Header, Payload, Message, etc. + // Avoid creating and writing to MemoryStreams and then calling ToArray(); + // Rent ArrayPools + // Would like to use, but the following is internal. + // Will see how hard it is to use the code. + // ArrayBufferWriter buffer = new System.Buffers.ArrayBufferWriter(); + // C:\github\dotnet\runtime\src\libraries\Common\src\System\Buffers\ArrayBufferWriter.cs + // If we can't use ArrayBufferWriter, we can make use our own pool in tokens. + // A possibility is to use our internal pool: sealed class DisposableObjectPool where T : class, IDisposable + // When creating an Encrypted Token, pass in a Span, representing the inner token. + if (additionalHeaderClaims?.Count > 0 && additionalHeaderClaims.Keys.Intersect(JwtTokenUtilities.DefaultHeaderParameters, StringComparer.OrdinalIgnoreCase).Any()) throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(LogMessages.IDX14116, LogHelper.MarkAsNonPII(nameof(additionalHeaderClaims)), LogHelper.MarkAsNonPII(string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters))))); if (additionalInnerHeaderClaims?.Count > 0 && additionalInnerHeaderClaims.Keys.Intersect(JwtTokenUtilities.DefaultHeaderParameters, StringComparer.OrdinalIgnoreCase).Any()) throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(LogMessages.IDX14116, nameof(additionalInnerHeaderClaims), string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters)))); - var header = CreateDefaultJWSHeader(signingCredentials, tokenType); - - if (encryptingCredentials == null && additionalHeaderClaims != null && additionalHeaderClaims.Count > 0) - header.Merge(JObject.FromObject(additionalHeaderClaims)); - - if (additionalInnerHeaderClaims != null && additionalInnerHeaderClaims.Count > 0) - header.Merge(JObject.FromObject(additionalInnerHeaderClaims)); - - var rawHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(header.ToString(Formatting.None))); - JObject jsonPayload = null; - try - { - if (SetDefaultTimesOnTokenCreation) - { - jsonPayload = JObject.Parse(payload); - if (jsonPayload != null) - { - var now = EpochTime.GetIntDate(DateTime.UtcNow); - if (!jsonPayload.TryGetValue(JwtRegisteredClaimNames.Exp, out _)) - jsonPayload.Add(JwtRegisteredClaimNames.Exp, now + TokenLifetimeInMinutes * 60); - - if (!jsonPayload.TryGetValue(JwtRegisteredClaimNames.Iat, out _)) - jsonPayload.Add(JwtRegisteredClaimNames.Iat, now); - - if (!jsonPayload.TryGetValue(JwtRegisteredClaimNames.Nbf, out _)) - jsonPayload.Add(JwtRegisteredClaimNames.Nbf, now); - } - } - } - catch(Exception ex) - { - if (LogHelper.IsEnabled(EventLogLevel.Error)) - LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(LogMessages.IDX14307, ex, payload))); - } + byte[] headerBytes = WriteJwsHeader(signingCredentials, tokenType, additionalInnerHeaderClaims, encryptingCredentials == null ? additionalHeaderClaims : null); - payload = jsonPayload != null ? jsonPayload.ToString(Formatting.None) : payload; - var rawPayload = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(payload)); - var message = rawHeader + "." + rawPayload; + string header = Base64UrlEncoder.Encode(headerBytes); + string message = header + "." + Base64UrlEncoder.Encode(payloadBytes); var rawSignature = signingCredentials == null ? string.Empty : JwtTokenUtilities.CreateEncodedSignature(message, signingCredentials); if (encryptingCredentials != null) - { - additionalHeaderClaims = AddCtyClaimDefaultValue(additionalHeaderClaims, encryptingCredentials.SetDefaultCtyClaim); - - return EncryptTokenPrivate(message + "." + rawSignature, encryptingCredentials, compressionAlgorithm, additionalHeaderClaims, tokenType); - } + return EncryptToken(Encoding.UTF8.GetBytes(message + "." + rawSignature), encryptingCredentials, compressionAlgorithm, additionalHeaderClaims, tokenType); return message + "." + rawSignature; } - /// - /// Compress a JWT token string. - /// - /// - /// - /// if is null. - /// if is null. - /// if the compression algorithm is not supported. - /// Compressed JWT token bytes. - private static byte[] CompressToken(string token, string compressionAlgorithm) + internal static byte[] CompressToken(byte[] utf8Bytes, string compressionAlgorithm) { - if (token == null) - throw LogHelper.LogArgumentNullException(nameof(token)); - if (string.IsNullOrEmpty(compressionAlgorithm)) throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); @@ -745,7 +865,7 @@ private static byte[] CompressToken(string token, string compressionAlgorithm) var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(compressionAlgorithm); - return compressionProvider.Compress(Encoding.UTF8.GetBytes(token)) ?? throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10680, LogHelper.MarkAsNonPII(compressionAlgorithm)))); + return compressionProvider.Compress(utf8Bytes) ?? throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10680, LogHelper.MarkAsNonPII(compressionAlgorithm)))); } private static StringComparison GetStringComparisonRuleIf509(SecurityKey securityKey) => (securityKey is X509SecurityKey) @@ -1031,7 +1151,10 @@ public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCred /// if compression using 'algorithm' fails. /// if encryption fails using the (algorithm), pair. /// if not using one of the supported content encryption key (CEK) algorithms: 128, 384 or 512 AesCbcHmac (this applies in the case of key wrap only, not direct encryption). - public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCredentials, string algorithm, IDictionary additionalHeaderClaims) + public string EncryptToken(string innerJwt, + EncryptingCredentials encryptingCredentials, + string algorithm, + IDictionary additionalHeaderClaims) { if (string.IsNullOrEmpty(innerJwt)) throw LogHelper.LogArgumentNullException(nameof(innerJwt)); @@ -1048,31 +1171,47 @@ public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCred return EncryptTokenPrivate(innerJwt, encryptingCredentials, algorithm, additionalHeaderClaims, null); } - private static string EncryptTokenPrivate(string innerJwt, EncryptingCredentials encryptingCredentials, string compressionAlgorithm, IDictionary additionalHeaderClaims, string tokenType) + private static string EncryptTokenPrivate( + string innerJwt, + EncryptingCredentials encryptingCredentials, + string compressionAlgorithm, + IDictionary additionalHeaderClaims, + string tokenType) { - var cryptoProviderFactory = encryptingCredentials.CryptoProviderFactory ?? encryptingCredentials.Key.CryptoProviderFactory; + return (EncryptToken( + Encoding.UTF8.GetBytes(innerJwt), + encryptingCredentials, + compressionAlgorithm, + additionalHeaderClaims, + tokenType)); + } + + internal static string EncryptToken( + byte[] innerTokenUtf8Bytes, + EncryptingCredentials encryptingCredentials, + string compressionAlgorithm, + IDictionary additionalHeaderClaims, + string tokenType) + { + CryptoProviderFactory cryptoProviderFactory = encryptingCredentials.CryptoProviderFactory ?? encryptingCredentials.Key.CryptoProviderFactory; if (cryptoProviderFactory == null) throw LogHelper.LogExceptionMessage(new ArgumentException(TokenLogMessages.IDX10620)); - byte[] wrappedKey = null; - SecurityKey securityKey = JwtTokenUtilities.GetSecurityKey(encryptingCredentials, cryptoProviderFactory, additionalHeaderClaims, out wrappedKey); + SecurityKey securityKey = JwtTokenUtilities.GetSecurityKey(encryptingCredentials, cryptoProviderFactory, additionalHeaderClaims, out byte[] wrappedKey); - using (var encryptionProvider = cryptoProviderFactory.CreateAuthenticatedEncryptionProvider(securityKey, encryptingCredentials.Enc)) + using (AuthenticatedEncryptionProvider encryptionProvider = cryptoProviderFactory.CreateAuthenticatedEncryptionProvider(securityKey, encryptingCredentials.Enc)) { if (encryptionProvider == null) throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogMessages.IDX14103)); - var header = CreateDefaultJWEHeader(encryptingCredentials, compressionAlgorithm, tokenType); - if (additionalHeaderClaims != null) - header.Merge(JObject.FromObject(additionalHeaderClaims)); - + byte[] jweHeader = WriteJweHeader(encryptingCredentials, compressionAlgorithm, tokenType, additionalHeaderClaims); byte[] plainText; if (!string.IsNullOrEmpty(compressionAlgorithm)) { try { - plainText = CompressToken(innerJwt, compressionAlgorithm); + plainText = CompressToken(innerTokenUtf8Bytes, compressionAlgorithm); } catch (Exception ex) { @@ -1081,15 +1220,17 @@ private static string EncryptTokenPrivate(string innerJwt, EncryptingCredentials } else { - plainText = Encoding.UTF8.GetBytes(innerJwt); + plainText = innerTokenUtf8Bytes; } try { - var rawHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(header.ToString(Formatting.None))); + string rawHeader = Base64UrlEncoder.Encode(jweHeader); + + //TODO - why isn't the result checked. var encryptionResult = encryptionProvider.Encrypt(plainText, Encoding.ASCII.GetBytes(rawHeader)); return JwtConstants.DirectKeyUseAlg.Equals(encryptingCredentials.Alg) ? - string.Join(".", rawHeader, string.Empty, Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)): + string.Join(".", rawHeader, string.Empty, Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)) : string.Join(".", rawHeader, Base64UrlEncoder.Encode(wrappedKey), Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)); } catch (Exception ex) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtHeaderParameterNames.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtHeaderParameterNames.cs index 532812257c..fa174f907f 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtHeaderParameterNames.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtHeaderParameterNames.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Text; + namespace Microsoft.IdentityModel.JsonWebTokens { /// @@ -8,11 +11,28 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// public struct JwtHeaderParameterNames { + // Please keep this alphabetical order + /// /// See: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 /// public const string Alg = "alg"; + /// + /// See: https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.1.2 + /// + public const string Apu = "apu"; + + /// + /// See: https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.1.3 + /// + public const string Apv = "apv"; + + /// + /// See: https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.1.1 + /// + public const string Epk = "epk"; + /// /// See: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.10 /// Also: https://datatracker.ietf.org/doc/html/rfc7519#section-5.2 @@ -69,20 +89,31 @@ public struct JwtHeaderParameterNames /// See: https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.3 /// public const string Zip = "zip"; + } - /// - /// See: https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.1.1 - /// - public const string Epk = "epk"; - - /// - /// See: https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.1.2 - /// - public const string Apu = "apu"; + /// + /// Parameter names for JsonWebToken header values as UTF8 bytes. + /// Used by UTF8JsonReader/Writer for performance gains. + /// + internal readonly struct JwtHeaderUtf8Bytes + { + // Please keep this alphabetical order - /// - /// See: https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.1.3 - /// - public const string Apv = "apv"; + public static ReadOnlySpan Alg =>"alg"u8; + public static ReadOnlySpan Apu =>"apu"u8; + public static ReadOnlySpan Apv =>"apv"u8; + public static ReadOnlySpan Cty =>"cty"u8; + public static ReadOnlySpan Enc =>"enc"u8; + public static ReadOnlySpan Epk =>"epk"u8; + public static ReadOnlySpan IV =>"iv"u8; + public static ReadOnlySpan Jku =>"jku"u8; + public static ReadOnlySpan Jwk =>"jwk"u8; + public static ReadOnlySpan Kid =>"kid"u8; + public static ReadOnlySpan Typ =>"typ"u8; + public static ReadOnlySpan X5c =>"x5c"u8; + public static ReadOnlySpan X5t =>"x5t"u8; + public static ReadOnlySpan X5u =>"x5u"u8; + public static ReadOnlySpan Zip =>"zip"u8; } + } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtRegisteredClaimNames.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtRegisteredClaimNames.cs index c75eadd724..75239d2b4b 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtRegisteredClaimNames.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtRegisteredClaimNames.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Text; + namespace Microsoft.IdentityModel.JsonWebTokens { /// @@ -10,6 +13,8 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// public struct JwtRegisteredClaimNames { + // Please keep in alphabetical order + /// /// public const string Actort = "actort"; @@ -150,4 +155,43 @@ public struct JwtRegisteredClaimNames /// public const string Website = "website"; } + + /// + /// Parameter names for JsonWebToken registered claim names in UTF8 bytes. + /// Used by UTF8JsonReader/Writer for performance gains. + /// + internal readonly struct JwtRegisteredClaimNamesUtf8Bytes + { + // Please keep in alphabetical order + + public static ReadOnlySpan Actort => "actort"u8; + public static ReadOnlySpan Acr => "acr"u8; + public static ReadOnlySpan Amr => "amr"u8; + public static ReadOnlySpan AtHash => "at_hash"u8; + public static ReadOnlySpan Aud => "aud"u8; + public static ReadOnlySpan AuthTime => "auth_time"u8; + public static ReadOnlySpan Azp => "azp"u8; + public static ReadOnlySpan Birthdate => "birthdate"u8; + public static ReadOnlySpan CHash => "c_hash"u8; + public static ReadOnlySpan Email => "email"u8; + public static ReadOnlySpan Exp => "exp"u8; + public static ReadOnlySpan Gender => "gender"u8; + public static ReadOnlySpan FamilyName => "family_name"u8; + public static ReadOnlySpan GivenName => "given_name"u8; + public static ReadOnlySpan Iat => "iat"u8; + public static ReadOnlySpan Iss => "iss"u8; + public static ReadOnlySpan Jti => "jti"u8; + public static ReadOnlySpan Name => "name"u8; + public static ReadOnlySpan NameId => "nameid"u8; + public static ReadOnlySpan Nonce => "nonce"u8; + public static ReadOnlySpan Nbf => "nbf"u8; + public static ReadOnlySpan PhoneNumber => "phone_number"u8; + public static ReadOnlySpan PhoneNumberVerified => "phone_number_verified"u8; + public static ReadOnlySpan Prn => "prn"u8; + public static ReadOnlySpan Sid => "sid"u8; + public static ReadOnlySpan Sub => "sub"u8; + public static ReadOnlySpan Typ => "typ"u8; + public static ReadOnlySpan UniqueName => "unique_name"u8; + public static ReadOnlySpan Website => "website"u8; + } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs index e3daea25e8..1fee751f1e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; -using System.IO; +using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -13,7 +13,8 @@ using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; +using Microsoft.IdentityModel.Tokens.Json; + using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace Microsoft.IdentityModel.JsonWebTokens @@ -85,7 +86,6 @@ public static string CreateEncodedSignature(string input, SigningCredentials sig /// or is null. public static string CreateEncodedSignature(string input, SigningCredentials signingCredentials, bool cacheProvider) { - // TODO create overload that takes a Span for the input if (input == null) throw LogHelper.LogArgumentNullException(nameof(input)); @@ -400,44 +400,6 @@ public static IEnumerable GetAllDecryptionKeys(TokenValidationParam } - /// - /// Gets the using the number of seconds from 1970-01-01T0:0:0Z (UTC) - /// - /// Claim in the payload that should map to an integer, float, or string. - /// The payload that contains the desired claim value. - /// If the claim is not found, the function returns: - /// - /// If the value of the claim cannot be parsed into a long. - /// The representation of a claim. - internal static DateTime GetDateTime(string key, JObject payload) - { - if (!payload.TryGetValue(key, out var jToken)) - return DateTime.MinValue; - - return EpochTime.DateTime(Convert.ToInt64(Math.Truncate(Convert.ToDouble(ParseTimeValue(jToken, key), CultureInfo.InvariantCulture)))); - } - - private static long ParseTimeValue(JToken jToken, string claimName) - { - if (jToken.Type == JTokenType.Integer || jToken.Type == JTokenType.Float) - { - return (long)jToken; - } - else if (jToken.Type == JTokenType.String) - { - if (long.TryParse((string)jToken, out long resultLong)) - return resultLong; - - if (float.TryParse((string)jToken, out float resultFloat)) - return (long)resultFloat; - - if (double.TryParse((string)jToken, out double resultDouble)) - return (long)resultDouble; - } - - throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX14300, LogHelper.MarkAsNonPII(claimName), jToken.ToString(), LogHelper.MarkAsNonPII(typeof(long))))); - } - internal static string SafeLogJwtToken(object obj) { if (obj == null) @@ -550,12 +512,61 @@ internal static IEnumerable ConcatSigningKeys(TokenValidationParame } } - internal static JsonDocument ParseDocument(byte[] bytes, int length) + // If a string is in IS8061 format, assume a DateTime is in UTC + internal static string GetStringClaimValueType(string str) { - using (MemoryStream memoryStream = new MemoryStream(bytes, 0, length)) + if (DateTime.TryParse(str, out DateTime dateTimeValue)) { - return JsonDocument.Parse(memoryStream); + string dtUniversal = dateTimeValue.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); + if (dtUniversal.Equals(str, StringComparison.Ordinal)) + return ClaimValueTypes.DateTime; + } + + return ClaimValueTypes.String; + } + + // TODO - we need to support to ways of accessing json values. + // In a System.Security.Claims.Claim, JsonObjects and JsonArrays will need to be serialized as Json, and the ClaimValueType set to JsonArray or Json. + // A C# type can also be returned, we will return a List representing the JsonArray or an object representing the JsonObject. + // This will allow for constant time lookup for both scenarios if we store the results in dictionaries. + + // We need a shared model for adding claims from object for JsonWebToken and JwtSecurityToken + // From getting ClaimValueTypes to setting object types + + internal static IDictionary CreateClaimsDictionary(byte[] bytes, int length) + { + string utf8 = Encoding.UTF8.GetString(bytes); + Dictionary claims = new(); + Span utf8Span = bytes; + Utf8JsonReader reader = new(utf8Span.Slice(0,length)); + + if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, false)) + throw LogHelper.LogExceptionMessage( + new JsonException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX11023, + LogHelper.MarkAsNonPII("JsonTokenType.StartObject"), + LogHelper.MarkAsNonPII(reader.TokenType), + LogHelper.MarkAsNonPII(JsonClaimSet.ClassName), + LogHelper.MarkAsNonPII(reader.TokenStartIndex), + LogHelper.MarkAsNonPII(reader.CurrentDepth), + LogHelper.MarkAsNonPII(reader.BytesConsumed)))); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string propertyName = reader.GetString(); + reader.Read(); + claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName); + } + else if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } }; + + return claims; } /// @@ -565,9 +576,9 @@ internal static JsonDocument ParseDocument(byte[] bytes, int length) /// /// /// - internal static JsonDocument GetJsonDocumentFromBase64UrlEncodedString(string rawString, int startIndex, int length) + internal static IDictionary ParseJsonBytes(string rawString, int startIndex, int length) { - return Base64UrlEncoding.Decode(rawString, startIndex, length, ParseDocument); + return Base64UrlEncoding.Decode>(rawString, startIndex, length, CreateClaimsDictionary); } } } diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs index 1eace64244..3901aa02c3 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs @@ -107,7 +107,7 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed)))); - while(JsonPrimitives.ReaderRead(ref reader)) + while(reader.Read()) { #region Check property name using ValueTextEquals // the config spec, https://datatracker.ietf.org/doc/html/rfc7517#section-4, does not require that we reject JSON with @@ -271,13 +271,13 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC else { #region case-insensitive - string propertyName = JsonPrimitives.GetPropertyName(ref reader, OpenIdConnectConfiguration.ClassName, true); + string propertyName = JsonPrimitives.ReadPropertyName(ref reader, OpenIdConnectConfiguration.ClassName, true); // fallback to checking property names as case insensitive // first check to see if the upper case property value is a valid property name if not add to AdditionalData, to avoid unnecessary string compares. if (!OpenIdProviderMetadataNamesUpperCase.Contains(propertyName.ToUpperInvariant())) { - config.AdditionalData[propertyName] = JsonPrimitives.GetUnknownProperty(ref reader); + config.AdditionalData[propertyName] = JsonPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, OpenIdConnectConfiguration.ClassName); } else { @@ -600,7 +600,7 @@ public static void Write(ref Utf8JsonWriter writer, OpenIdConnectConfiguration c JsonPrimitives.WriteStrings(ref writer, Utf8Bytes.UserInfoSigningAlgValuesSupported, config.UserInfoEndpointSigningAlgValuesSupported); if (config.AdditionalData.Count > 0) - JsonPrimitives.WriteAdditionalData(ref writer, config.AdditionalData); + JsonPrimitives.WriteObjects(ref writer, config.AdditionalData); writer.WriteEndObject(); } diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectMessage.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectMessage.cs index 7ed8fa69c5..da56e660f6 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectMessage.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectMessage.cs @@ -112,11 +112,11 @@ private void SetJsonParameters(string json) { Utf8JsonReader reader = new(Encoding.UTF8.GetBytes(json).AsSpan()); - while (JsonPrimitives.ReaderRead(ref reader)) + while (reader.Read()) { if (reader.TokenType == JsonTokenType.PropertyName) { - string propertyName = JsonPrimitives.GetPropertyName(ref reader, ClassName, true); + string propertyName = JsonPrimitives.ReadPropertyName(ref reader, ClassName, true); string propertyValue = null; if (reader.TokenType == JsonTokenType.String) propertyValue = JsonPrimitives.ReadString(ref reader, propertyName, ClassName); diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectParameterNames.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectParameterNames.cs index 623fd73ff1..eb99feb2e7 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectParameterNames.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectParameterNames.cs @@ -6,7 +6,7 @@ namespace Microsoft.IdentityModel.Protocols.OpenIdConnect { /// - /// Parameter names for OpenIdConnect. + /// Parameter names for OpenIdConnect Request/Response messages. /// public static class OpenIdConnectParameterNames { @@ -57,7 +57,8 @@ public static class OpenIdConnectParameterNames } /// - /// Parameter names for OpenIdConnect UTF8 bytes. + /// Parameter names for OpenIdConnect Request/Response messages as UTF8 bytes. + /// Used by UTF8JsonReader/Writer for performance gains. /// internal static class OpenIdConnectParameterUtf8Bytes { diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs index 4f2cb60bbb..eaef4e2319 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdProviderMetadataNames.cs @@ -6,7 +6,7 @@ namespace Microsoft.IdentityModel.Protocols.OpenIdConnect { /// - /// OpenIdProviderConfiguration MetadataName + /// OpenId Provider Metadata parameter names /// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata /// public static class OpenIdProviderMetadataNames @@ -62,7 +62,8 @@ public static class OpenIdProviderMetadataNames } /// - /// OpenIdProviderConfiguration MetadataName - UTF8Bytes + /// OpenId Provider Metadata parameter names as UTF8Bytes + /// Used by UTF8JsonReader/Writer for performance gains. /// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata /// internal static class OpenIdProviderMetadataUtf8Bytes diff --git a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/Cnf.cs b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/Cnf.cs new file mode 100644 index 0000000000..9ad6731bb1 --- /dev/null +++ b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/Cnf.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json; + +namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest +{ + /// + /// Represents the Cnf Claim + /// + internal class Cnf + { + internal const string ClassName = "Microsoft.IdentityModel.Protocols.SignedHttpRequest.Cnf"; + + public Cnf() { } + + public Cnf(string json) + { + if (string.IsNullOrEmpty(json)) + throw LogHelper.LogArgumentNullException(nameof(json)); + + Utf8JsonReader reader = new(Encoding.UTF8.GetBytes(json).AsSpan()); + if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, false)) + throw LogHelper.LogExceptionMessage( + new JsonException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX11023, + LogHelper.MarkAsNonPII("JsonTokenType.StartObject"), + LogHelper.MarkAsNonPII(reader.TokenType), + LogHelper.MarkAsNonPII(ClassName), + LogHelper.MarkAsNonPII(reader.TokenStartIndex), + LogHelper.MarkAsNonPII(reader.CurrentDepth), + LogHelper.MarkAsNonPII(reader.BytesConsumed)))); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals(ConfirmationClaimTypesUtf8Bytes.Jwk)) + { + reader.Read(); + JsonWebKey = JsonWebKeySerializer.Read(ref reader, new JsonWebKey()); + } + else if (reader.ValueTextEquals(ConfirmationClaimTypesUtf8Bytes.Kid)) + { + reader.Read(); + Kid = JsonSerializerPrimitives.ReadString(ref reader, ConfirmationClaimTypes.Kid, ClassName); + } + else if (reader.ValueTextEquals(ConfirmationClaimTypesUtf8Bytes.Jku)) + { + reader.Read(); + Jku = JsonSerializerPrimitives.ReadString(ref reader, ConfirmationClaimTypes.Kid, ClassName); + } + else if (reader.ValueTextEquals(ConfirmationClaimTypesUtf8Bytes.Jwe)) + { + reader.Read(); + Jwe = JsonSerializerPrimitives.ReadString(ref reader, ConfirmationClaimTypes.Jwe, ClassName); + } + } + else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) + break; + } + } + + [JsonPropertyName("kid")] + public string Kid { get; set; } + + [JsonPropertyName("jwe")] + public string Jwe { get; set; } + + [JsonPropertyName("jku")] + public string Jku { get; set; } + + [JsonPropertyName("jwk")] + public JsonWebKey JsonWebKey{ get; set; } + } +} diff --git a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/ConfirmationClaimTypes.cs b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/ConfirmationClaimTypes.cs index 81dd13cd25..a9f746353e 100644 --- a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/ConfirmationClaimTypes.cs +++ b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/ConfirmationClaimTypes.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; + namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest { /// @@ -34,4 +36,14 @@ public static class ConfirmationClaimTypes /// public const string Kid = "kid"; } + + internal static class ConfirmationClaimTypesUtf8Bytes + { + public static ReadOnlySpan Cnf => "cnf"u8; + public static ReadOnlySpan Jwk => "jwk"u8; + public static ReadOnlySpan Jwe => "jwe"u8; + public static ReadOnlySpan Jku => "jku"u8; + public static ReadOnlySpan Kid => "kid"u8; + } + } diff --git a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs index d393ebbdf1..12943f57df 100644 --- a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs +++ b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; @@ -16,7 +17,6 @@ using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest @@ -71,7 +71,6 @@ public string CreateSignedHttpRequest(SignedHttpRequestDescriptor signedHttpRequ string encodedPayload; using (MemoryStream memoryStream = new MemoryStream()) { - Utf8JsonWriter payloadWriter = null; try { @@ -200,7 +199,7 @@ internal void CreateHttpRequestPayload(ref Utf8JsonWriter writer, SignedHttpRequ AddCnfClaim(ref writer, signedHttpRequestDescriptor); if (signedHttpRequestDescriptor.AdditionalPayloadClaims != null && signedHttpRequestDescriptor.AdditionalPayloadClaims.Any()) - JsonPrimitives.WriteAdditionalData(ref writer, signedHttpRequestDescriptor.AdditionalPayloadClaims); + JsonPrimitives.WriteObjects(ref writer, signedHttpRequestDescriptor.AdditionalPayloadClaims); } /// @@ -831,20 +830,21 @@ internal virtual void ValidateQClaim(JsonWebToken signedHttpRequest, SignedHttpR if (httpRequestUri == null) throw LogHelper.LogArgumentNullException(nameof(httpRequestUri)); - if (!signedHttpRequest.TryGetPayloadValue(SignedHttpRequestClaimTypes.Q, out JArray qClaim) || qClaim == null) + if (!signedHttpRequest.TryGetPayloadValue(SignedHttpRequestClaimTypes.Q, out IList qClaim) || qClaim == null) throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidQClaimException(LogHelper.FormatInvariant(LogMessages.IDX23003, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.Q)))); httpRequestUri = EnsureAbsoluteUri(httpRequestUri); var sanitizedQueryParams = SanitizeQueryParams(httpRequestUri); - string qClaimBase64UrlEncodedHash = string.Empty; - string expectedBase64UrlEncodedHash = string.Empty; - List qClaimQueryParamNames; + string calculatedBase64UrlEncodedHash = string.Empty; + IList qClaimQueryParamNames; + try { // "q": [["queryParamName1", "queryParamName2",... "queryParamNameN"], "base64UrlEncodedHashValue"]] - qClaimQueryParamNames = qClaim[0].ToObject>(); - qClaimBase64UrlEncodedHash = qClaim[1].ToString(); + // deserialzed as IList with q[0] is an IList, q[1] an object + qClaimBase64UrlEncodedHash = (string)qClaim[1]; + qClaimQueryParamNames = qClaim[0] as IList; } catch (Exception e) { @@ -857,7 +857,7 @@ internal virtual void ValidateQClaim(JsonWebToken signedHttpRequest, SignedHttpR var firstQueryParam = true; foreach (var queryParamName in qClaimQueryParamNames) { - if (!sanitizedQueryParams.TryGetValue(queryParamName, out var queryParamsValue)) + if (!sanitizedQueryParams.TryGetValue((string)queryParamName, out string queryParamsValue)) { throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidQClaimException(LogHelper.FormatInvariant(LogMessages.IDX23028, LogHelper.MarkAsNonPII(queryParamName), LogHelper.MarkAsNonPII(string.Join(", ", sanitizedQueryParams.Select(x => x.Key)))))); } @@ -866,15 +866,15 @@ internal virtual void ValidateQClaim(JsonWebToken signedHttpRequest, SignedHttpR if (!firstQueryParam) stringBuffer.Append("&"); - stringBuffer.Append(queryParamName).Append('=').Append(queryParamsValue); + stringBuffer.Append((string)queryParamName).Append('=').Append(queryParamsValue); firstQueryParam = false; // remove the query param from the dictionary to mark it as covered. - sanitizedQueryParams.Remove(queryParamName); + sanitizedQueryParams.Remove((string)queryParamName); } } - expectedBase64UrlEncodedHash = CalculateBase64UrlEncodedHash(stringBuffer.ToString()); + calculatedBase64UrlEncodedHash = CalculateBase64UrlEncodedHash(stringBuffer.ToString()); } catch (Exception e) { @@ -884,8 +884,8 @@ internal virtual void ValidateQClaim(JsonWebToken signedHttpRequest, SignedHttpR if (!signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.AcceptUnsignedQueryParameters && sanitizedQueryParams.Any()) throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidQClaimException(LogHelper.FormatInvariant(LogMessages.IDX23029, LogHelper.MarkAsNonPII(string.Join(", ", sanitizedQueryParams.Select(x => x.Key)))))); - if (!string.Equals(expectedBase64UrlEncodedHash, qClaimBase64UrlEncodedHash)) - throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidQClaimException(LogHelper.FormatInvariant(LogMessages.IDX23011, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.Q), expectedBase64UrlEncodedHash, qClaimBase64UrlEncodedHash))); + if (!string.Equals(calculatedBase64UrlEncodedHash, qClaimBase64UrlEncodedHash)) + throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidQClaimException(LogHelper.FormatInvariant(LogMessages.IDX23011, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.Q), calculatedBase64UrlEncodedHash, qClaimBase64UrlEncodedHash))); } /// @@ -900,19 +900,20 @@ internal virtual void ValidateQClaim(JsonWebToken signedHttpRequest, SignedHttpR /// internal virtual void ValidateHClaim(JsonWebToken signedHttpRequest, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { - if (!signedHttpRequest.TryGetPayloadValue(SignedHttpRequestClaimTypes.H, out JArray hClaim) || hClaim == null) + if (!signedHttpRequest.TryGetPayloadValue(SignedHttpRequestClaimTypes.H, out IList hClaim) || hClaim == null) throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidHClaimException(LogHelper.FormatInvariant(LogMessages.IDX23003, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.H)))); var sanitizedHeaders = SanitizeHeaders(signedHttpRequestValidationContext.HttpRequestData.Headers); string hClaimBase64UrlEncodedHash = string.Empty; - string expectedBase64UrlEncodedHash = string.Empty; - List hClaimHeaderNames; + string calculatedBase64UrlEncodedHash = string.Empty; + IList hClaimHeaderNames; try { // "h": [["headerName1", "headerName2",... "headerNameN"], "base64UrlEncodedHashValue"]] - hClaimHeaderNames = hClaim[0].ToObject>(); - hClaimBase64UrlEncodedHash = hClaim[1].ToString(); + // deserialzed as IList with h[0] is an IList, h[1] an object + hClaimBase64UrlEncodedHash = (string)hClaim[1]; + hClaimHeaderNames = hClaim[0] as IList; } catch (Exception e) { @@ -925,7 +926,7 @@ internal virtual void ValidateHClaim(JsonWebToken signedHttpRequest, SignedHttpR var firstHeader = true; foreach (var headerName in hClaimHeaderNames) { - if (!sanitizedHeaders.TryGetValue(headerName, out var headerValue)) + if (!sanitizedHeaders.TryGetValue((string)headerName, out var headerValue)) { throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidHClaimException(LogHelper.FormatInvariant(LogMessages.IDX23027, LogHelper.MarkAsNonPII(headerName), LogHelper.MarkAsNonPII(string.Join(", ", sanitizedHeaders.Select(x => x.Key)))))); } @@ -938,11 +939,11 @@ internal virtual void ValidateHClaim(JsonWebToken signedHttpRequest, SignedHttpR firstHeader = false; // remove the header from the dictionary to mark it as covered. - sanitizedHeaders.Remove(headerName); + sanitizedHeaders.Remove((string)headerName); } } - expectedBase64UrlEncodedHash = CalculateBase64UrlEncodedHash(stringBuffer.ToString()); + calculatedBase64UrlEncodedHash = CalculateBase64UrlEncodedHash(stringBuffer.ToString()); } catch (Exception e) { @@ -952,8 +953,8 @@ internal virtual void ValidateHClaim(JsonWebToken signedHttpRequest, SignedHttpR if (!signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.AcceptUnsignedHeaders && sanitizedHeaders.Any()) throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidHClaimException(LogHelper.FormatInvariant(LogMessages.IDX23026, LogHelper.MarkAsNonPII(string.Join(", ", sanitizedHeaders.Select(x => x.Key)))))); - if (!string.Equals(expectedBase64UrlEncodedHash, hClaimBase64UrlEncodedHash)) - throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidHClaimException(LogHelper.FormatInvariant(LogMessages.IDX23011, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.H), expectedBase64UrlEncodedHash, hClaimBase64UrlEncodedHash))); + if (!string.Equals(calculatedBase64UrlEncodedHash, hClaimBase64UrlEncodedHash)) + throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidHClaimException(LogHelper.FormatInvariant(LogMessages.IDX23011, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.H), calculatedBase64UrlEncodedHash, hClaimBase64UrlEncodedHash))); } /// @@ -1020,7 +1021,7 @@ internal virtual async Task ResolvePopKeyAsync(JsonWebToken signedH /// An access token ("at") that was already validated during the SignedHttpRequest validation process. /// A structure that wraps parameters needed for SignedHttpRequest validation. /// JSON representation of the 'cnf' claim. - internal virtual JObject GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) + internal virtual Cnf GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { if (validatedAccessToken == null) throw LogHelper.LogArgumentNullException(nameof(validatedAccessToken)); @@ -1028,13 +1029,18 @@ internal virtual JObject GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonWe // use the decrypted jwt if the jwtValidatedAccessToken is encrypted. if (validatedAccessToken.InnerToken != null) validatedAccessToken = validatedAccessToken.InnerToken; - - if (validatedAccessToken.TryGetPayloadValue(ConfirmationClaimTypes.Cnf, out JObject cnf) && cnf != null) - return cnf; - else if (validatedAccessToken.TryGetPayloadValue(ConfirmationClaimTypes.Cnf, out string cnfString) && !string.IsNullOrEmpty(cnfString)) - return JObject.Parse(cnfString); - else - throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidCnfClaimException(LogHelper.FormatInvariant(LogMessages.IDX23003, LogHelper.MarkAsNonPII(ConfirmationClaimTypes.Cnf)))); + + try + { + if (validatedAccessToken.TryGetPayloadValue(ConfirmationClaimTypes.Cnf, out string cnf) && cnf != null) + return new Cnf(cnf); + } + catch(JsonException ex) + { + throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidCnfClaimException(LogHelper.FormatInvariant(LogMessages.IDX23003, LogHelper.MarkAsNonPII(ConfirmationClaimTypes.Cnf)), ex)); + } + + throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidCnfClaimException(LogHelper.FormatInvariant(LogMessages.IDX23003, LogHelper.MarkAsNonPII(ConfirmationClaimTypes.Cnf)))); } /// @@ -1047,19 +1053,19 @@ internal virtual JObject GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonWe /// Propagates notification that operations should be canceled. /// A resolved PoP . /// https://datatracker.ietf.org/doc/html/rfc7800#section-3.1 - internal virtual async Task ResolvePopKeyFromCnfClaimAsync(JObject cnf, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal virtual async Task ResolvePopKeyFromCnfClaimAsync(Cnf cnf, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { if (cnf == null) throw LogHelper.LogArgumentNullException(nameof(cnf)); - if (cnf.TryGetValue(ConfirmationClaimTypes.Jwk, StringComparison.Ordinal, out var jwk)) - return ResolvePopKeyFromJwk(jwk.ToString(), signedHttpRequestValidationContext); - else if (cnf.TryGetValue(ConfirmationClaimTypes.Jwe, StringComparison.Ordinal, out var jwe)) - return await ResolvePopKeyFromJweAsync(jwe.ToString(), signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); - else if (cnf.TryGetValue(ConfirmationClaimTypes.Jku, StringComparison.Ordinal, out var jku)) - return await ResolvePopKeyFromJkuAsync(jku.ToString(), cnf, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); - else if (cnf.TryGetValue(ConfirmationClaimTypes.Kid, StringComparison.Ordinal, out var kid)) - return await ResolvePopKeyFromKeyIdentifierAsync(kid.ToString(), signedHttpRequest, validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); + if (cnf.JsonWebKey != null) + return ResolvePopKeyFromJwk(cnf.JsonWebKey, signedHttpRequestValidationContext); + else if (!string.IsNullOrEmpty(cnf.Jwe)) + return await ResolvePopKeyFromJweAsync(cnf.Jwe, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); + else if (!string.IsNullOrEmpty(cnf.Jku)) + return await ResolvePopKeyFromJkuAsync(cnf.Jku, cnf, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); + else if (!string.IsNullOrEmpty(cnf.Kid)) + return await ResolvePopKeyFromKeyIdentifierAsync(cnf.Kid, signedHttpRequest, validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); else throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidCnfClaimException(LogHelper.FormatInvariant(LogMessages.IDX23014, cnf.ToString()))); } @@ -1067,15 +1073,12 @@ internal virtual async Task ResolvePopKeyFromCnfClaimAsync(JObject /// /// Resolves a PoP from the asymmetric representation of a PoP key. /// - /// An asymmetric representation of a PoP key (JSON). + /// The JsonWebKey to resolve. /// A structure that wraps parameters needed for SignedHttpRequest validation. /// A resolved PoP . - internal virtual SecurityKey ResolvePopKeyFromJwk(string jwk, SignedHttpRequestValidationContext signedHttpRequestValidationContext) + internal virtual SecurityKey ResolvePopKeyFromJwk(JsonWebKey jsonWebKey, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { - if (string.IsNullOrEmpty(jwk)) - throw LogHelper.LogArgumentNullException(nameof(jwk)); - - var jsonWebKey = new JsonWebKey(jwk); + _ = jsonWebKey ?? throw LogHelper.LogArgumentNullException(nameof(jsonWebKey)); if (JsonWebKeyConverter.TryConvertToSecurityKey(jsonWebKey, out var key)) { @@ -1112,7 +1115,7 @@ internal virtual async Task ResolvePopKeyFromJweAsync(string jwe, S /// A structure that wraps parameters needed for SignedHttpRequest validation. /// Propagates notification that operations should be canceled. /// A resolved PoP . - internal virtual async Task ResolvePopKeyFromJkuAsync(string jkuSetUrl, JObject cnf, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal virtual async Task ResolvePopKeyFromJkuAsync(string jkuSetUrl, Cnf cnf, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { var popKeys = await GetPopKeysFromJkuAsync(jkuSetUrl, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); @@ -1127,15 +1130,19 @@ internal virtual async Task ResolvePopKeyFromJkuAsync(string jkuSet // If there are multiple keys in the referenced JWK Set document, a "kid" member MUST also be included // with the referenced key's JWK also containing the same "kid" value. // https://datatracker.ietf.org/doc/html/rfc7800#section-3.5 - else if (cnf.TryGetValue(ConfirmationClaimTypes.Kid, StringComparison.Ordinal, out var kid)) + else if (!string.IsNullOrEmpty(cnf.Kid)) { foreach (var key in popKeys) { - if (string.Equals(key.KeyId, kid.ToString())) + if (string.Equals(key.KeyId, cnf.Kid)) return key; } - throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidPopKeyException(LogHelper.FormatInvariant(LogMessages.IDX23021, LogHelper.MarkAsNonPII(kid), string.Join(", ", popKeys.Select(x => x.KeyId ?? "Null"))))); + throw LogHelper.LogExceptionMessage( + new SignedHttpRequestInvalidPopKeyException( + LogHelper.FormatInvariant( + LogMessages.IDX23021, + LogHelper.MarkAsNonPII(cnf.Kid), string.Join(", ", popKeys.Select(x => x.KeyId ?? "Null"))))); } else { @@ -1189,8 +1196,10 @@ internal virtual async Task ResolvePopKeyFromKeyIdentifierAsync(str { if (signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.PopKeyResolverFromKeyIdAsync != null) return await signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.PopKeyResolverFromKeyIdAsync(kid, validatedAccessToken, signedHttpRequest, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); - else if (signedHttpRequest != null && signedHttpRequest.TryGetPayloadValue(ConfirmationClaimTypes.Cnf, out JObject signedHttpRequestCnf) && signedHttpRequestCnf != null) - return await ResolvePopKeyFromCnfReferenceAsync(kid, signedHttpRequestCnf, validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); + else if (signedHttpRequest != null && signedHttpRequest.TryGetPayloadValue(ConfirmationClaimTypes.Cnf, out string signedHttpRequestCnf) && signedHttpRequestCnf != null) + { + return await ResolvePopKeyFromCnfReferenceAsync(kid, new Cnf(signedHttpRequestCnf), validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); + } else throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidPopKeyException(LogHelper.FormatInvariant(LogMessages.IDX23023))); } @@ -1205,7 +1214,7 @@ internal virtual async Task ResolvePopKeyFromKeyIdentifierAsync(str /// Propagates notification that operations should be canceled. /// A resolved PoP . /// MUST match the base64url-encoded thumbprint of a JWK resolved from the . - internal virtual async Task ResolvePopKeyFromCnfReferenceAsync(string cnfReferenceId, JObject confirmationClaim, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal virtual async Task ResolvePopKeyFromCnfReferenceAsync(string cnfReferenceId, Cnf confirmationClaim, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { // resolve PoP key from the confirmation claim, but set signedHttpRequest to null to prevent recursion. var popKey = await ResolvePopKeyFromCnfClaimAsync(confirmationClaim, null, validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); diff --git a/src/Microsoft.IdentityModel.TestExtensions/Microsoft.IdentityModel.TestExtensions.csproj b/src/Microsoft.IdentityModel.TestExtensions/Microsoft.IdentityModel.TestExtensions.csproj index 2dfea0dd35..cab3c186d8 100644 --- a/src/Microsoft.IdentityModel.TestExtensions/Microsoft.IdentityModel.TestExtensions.csproj +++ b/src/Microsoft.IdentityModel.TestExtensions/Microsoft.IdentityModel.TestExtensions.csproj @@ -19,6 +19,10 @@ + + + + diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index 0e06564b6c..c6d41dbdf6 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Text; using System.Text.Encodings.Web; @@ -15,7 +16,18 @@ namespace Microsoft.IdentityModel.Tokens.Json { internal static class JsonSerializerPrimitives { - internal static Exception CreateJsonReaderException( + internal const int MaxDepth = 2; + + /// + /// Creates a JsonException that provides information on what went wrong + /// + /// the . + /// the type the reader was expecting to find. + /// the name of the type being read. + /// the property name being read. + /// inner exception if any. + /// + public static JsonException CreateJsonReaderException( ref Utf8JsonReader reader, string expectedType, string className, @@ -47,7 +59,7 @@ internal static Exception CreateJsonReaderException( innerException); } - internal static Exception CreateJsonReaderExceptionInvalidType(ref Utf8JsonReader reader, string expectedType, string className, string propertyName) + public static Exception CreateJsonReaderExceptionInvalidType(ref Utf8JsonReader reader, string expectedType, string className, string propertyName) { return new JsonException( LogHelper.FormatInvariant( @@ -61,111 +73,71 @@ internal static Exception CreateJsonReaderExceptionInvalidType(ref Utf8JsonReade LogHelper.MarkAsNonPII(reader.BytesConsumed))); } - public static JsonElement CreateJsonElement(string json) + public static JsonElement CreateJsonElement(IList strings) { - Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json).AsSpan()); + using (MemoryStream memoryStream = new()) + { + Utf8JsonWriter writer = null; + try + { + writer = new(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartArray(); + + foreach (string str in strings) + writer.WriteStringValue(str); + + writer.WriteEndArray(); + writer.Flush(); + + Utf8JsonReader reader = new(memoryStream.ToArray()); #if NET6_0_OR_GREATER - bool ret = JsonElement.TryParseValue(ref reader, out JsonElement? jsonElement); - return jsonElement.Value; + bool ret = JsonElement.TryParseValue(ref reader, out JsonElement? jsonElement); + return jsonElement.Value; #else - using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader)) - return jsonDocument.RootElement.Clone(); + using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader)) + return jsonDocument.RootElement.Clone(); #endif + } + finally + { + writer?.Dispose(); + } + } } - public static void WriteAsJsonElement(ref Utf8JsonWriter writer, string json) + public static JsonElement CreateJsonElement(string json) { Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json).AsSpan()); #if NET6_0_OR_GREATER - if (JsonElement.TryParseValue(ref reader, out JsonElement? jsonElement)) - jsonElement.Value.WriteTo(writer); + bool ret = JsonElement.TryParseValue(ref reader, out JsonElement? jsonElement); + return jsonElement.Value; #else using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader)) - jsonDocument.RootElement.WriteTo(writer); + return jsonDocument.RootElement.Clone(); #endif } - internal static string GetPropertyName(ref Utf8JsonReader reader, string className, bool advanceReader) - { - if (reader.TokenType == JsonTokenType.None) - ReaderRead(ref reader); - - if (reader.TokenType != JsonTokenType.PropertyName) - throw LogHelper.LogExceptionMessage(CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.PropertyName", string.Empty, className)); - - if (advanceReader) - { - string propertyName = reader.GetString(); - ReaderRead(ref reader); - return propertyName; - } - - return reader.GetString(); - } - - /// - /// This method is called when deserializing a known type where the JSON property does not map to a type property. - /// We put the object into a Dictionary[string, object]. - /// - /// - /// - internal static object GetUnknownProperty(ref Utf8JsonReader reader) - { - switch (reader.TokenType) - { - case JsonTokenType.False: - return false; - case JsonTokenType.Number: - return ReadNumber(ref reader); - case JsonTokenType.True: - return true; - case JsonTokenType.Null: - return null; - case JsonTokenType.String: - return reader.GetString(); - case JsonTokenType.StartObject: - case JsonTokenType.StartArray: - return ReadJsonElement(ref reader); - default: - // There is something broken here as this was called when the reader is pointing at a property. - // It must be a known Json type. - Debug.Assert(false, $"Utf8JsonReader.TokenType is not one of the expected types: False, Number, True, Null, String, StartArray, StartObject. Is: '{reader.TokenType}'."); - return null; - } - } - + #region Read internal static bool IsReaderAtTokenType(ref Utf8JsonReader reader, JsonTokenType tokenType, bool advanceReader) { if (reader.TokenType == JsonTokenType.None) - ReaderRead(ref reader); + reader.Read(); if (reader.TokenType != tokenType) return false; if (advanceReader) - ReaderRead(ref reader); + reader.Read(); return true; } - internal static bool ReaderRead(ref Utf8JsonReader reader) - { - try - { - return reader.Read(); - } - catch (JsonException ex) - { - throw new JsonException(ex.Message, ex); - } - } - internal static bool ReadBoolean(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) { if (read) - ReaderRead(ref reader); + reader.Read(); if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False) return reader.GetBoolean(); @@ -174,58 +146,10 @@ internal static bool ReadBoolean(ref Utf8JsonReader reader, string propertyName, CreateJsonReaderException(ref reader, "JsonTokenType.False or JsonTokenType.True", className, propertyName)); } - internal static IList ReadStrings(ref Utf8JsonReader reader, IList strings, string propertyName, string className, bool read = false) - { - if (read) - ReaderRead(ref reader); - - // returning null keeps the same logic as JsonSerialization.ReadObject - if (reader.TokenType == JsonTokenType.Null) - return null; - - if (!IsReaderAtTokenType(ref reader, JsonTokenType.StartArray, false)) - throw LogHelper.LogExceptionMessage( - CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - - while (ReaderRead(ref reader)) - { - if (IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, false)) - break; - - strings.Add(ReadString(ref reader, propertyName, className)); - } - - return strings; - } - - internal static ICollection ReadStrings(ref Utf8JsonReader reader, ICollection strings, string propertyName, string className, bool read = false) - { - if (read) - ReaderRead(ref reader); - - // returning null keeps the same logic as JsonSerialization.ReadObject - if (reader.TokenType == JsonTokenType.Null) - return null; - - if (!IsReaderAtTokenType(ref reader, JsonTokenType.StartArray, false)) - throw LogHelper.LogExceptionMessage( - CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - - while (ReaderRead(ref reader)) - { - if (IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, false)) - break; - - strings.Add(ReadString(ref reader, propertyName, className)); - } - - return strings; - } - internal static double ReadDouble(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) { if (read) - ReaderRead(ref reader); + reader.Read(); if (reader.TokenType == JsonTokenType.Number) { @@ -247,7 +171,7 @@ internal static double ReadDouble(ref Utf8JsonReader reader, string propertyName internal static int ReadInt(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) { if (read) - ReaderRead(ref reader); + reader.Read(); if (reader.TokenType == JsonTokenType.Number) { @@ -281,41 +205,73 @@ internal static JsonElement ReadJsonElement(ref Utf8JsonReader reader) #endif } - /// - /// Currently used by test code only - /// - /// - /// - /// - /// - /// - internal static IList ReadObjects(ref Utf8JsonReader reader, IList objects, string propertyName, string className) + internal static object ReadNumber(ref Utf8JsonReader reader) { - _ = objects ?? throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(objects))); + if (reader.TryGetInt32(out int i)) + return i; + else if (reader.TryGetInt64(out long l)) + return l; + else if (reader.TryGetDouble(out double d)) + return d; + else if (reader.TryGetUInt32(out uint u)) + return u; + else if (reader.TryGetUInt64(out ulong ul)) + return ul; + else if (reader.TryGetSingle(out float f)) + return f; + else if (reader.TryGetDecimal(out decimal m)) + return m; + + Debug.Assert(false, "expected to read a number, but none of the Utf8JsonReader.TryGet... methods returned true."); + + return ReadJsonElement(ref reader); + } + + internal static IList ReadArrayOfObjects(ref Utf8JsonReader reader, string propertyName, string className) + { // returning null keeps the same logic as JsonSerialization.ReadObject if (reader.TokenType == JsonTokenType.Null) return null; + List objects = new(); if (!IsReaderAtTokenType(ref reader, JsonTokenType.StartArray, false)) throw LogHelper.LogExceptionMessage( CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - while (ReaderRead(ref reader)) + while (reader.Read()) { - if (IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, true)) + if (IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, false)) break; - objects.Add(ReadJsonElement(ref reader)); - } + objects.Add(ReadPropertyValueAsObject(ref reader, propertyName, className)); + } return objects; } + internal static string ReadPropertyName(ref Utf8JsonReader reader, string className, bool advanceReader) + { + if (reader.TokenType == JsonTokenType.None) + reader.Read(); + + if (reader.TokenType != JsonTokenType.PropertyName) + throw LogHelper.LogExceptionMessage(CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.PropertyName", string.Empty, className)); + + if (advanceReader) + { + string propertyName = reader.GetString(); + reader.Read(); + return propertyName; + } + + return reader.GetString(); + } + internal static string ReadString(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) { if (read) - ReaderRead(ref reader); + reader.Read(); // returning null keeps the same logic as JsonSerialization.ReadObject if (reader.TokenType == JsonTokenType.Null) @@ -328,90 +284,233 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName return reader.GetString(); } + internal static object ReadStringAsObject(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) + { + if (read) + reader.Read(); + + // returning null keeps the same logic as JsonSerialization.ReadObject + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType != JsonTokenType.String) + throw LogHelper.LogExceptionMessage( + CreateJsonReaderException(ref reader, "JsonTokenType.String", className, propertyName)); + + string originalString = reader.GetString(); +#pragma warning disable CA1031 // Do not catch general exception types + try + { + // if (reader.TryGetDateTime(out DateTime dateTimeValue)) + // has thrown on escaped chars and empty chars + // try catch for safety + if (DateTime.TryParse(originalString, out DateTime dateTimeValue)) + { + dateTimeValue = dateTimeValue.ToUniversalTime(); + string dtUniversal = dateTimeValue.ToString("o", CultureInfo.InvariantCulture); + if (dtUniversal.Equals(originalString, StringComparison.Ordinal)) + return dateTimeValue; + } + } + catch(Exception) + { } +#pragma warning restore CA1031 // Do not catch general exception types + + return originalString; + } + + internal static ICollection ReadStrings( + ref Utf8JsonReader reader, + ICollection strings, + string propertyName, + string className, + bool read = false) + { + if (read) + reader.Read(); + + // returning null keeps the same logic as JsonSerialization.ReadObject + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (!IsReaderAtTokenType(ref reader, JsonTokenType.StartArray, false)) + throw LogHelper.LogExceptionMessage( + CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); + + while (reader.Read()) + { + if (IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, false)) + break; + + strings.Add(ReadString(ref reader, propertyName, className)); + } + + return strings; + } + + internal static IList ReadStrings( + ref Utf8JsonReader reader, + IList strings, + string propertyName, + string className, + bool read = false) + { + if (read) + reader.Read(); + + // returning null keeps the same logic as JsonSerialization.ReadObject + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (!IsReaderAtTokenType(ref reader, JsonTokenType.StartArray, false)) + throw LogHelper.LogExceptionMessage( + CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); + + while (reader.Read()) + { + if (IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, false)) + break; + + strings.Add(ReadString(ref reader, propertyName, className)); + } + + return strings; + } + /// - /// This method is only called when we are on a JsonTokenType.Number AND reading into AdditionalData which is an IDictionary[string, object]. - /// We have to make a choice of the type to return. + /// This method is called when deserializing a property value as an object. + /// Normally we put the object into a Dictionary[string, object]. /// /// - /// If possible a .net numerical type, otherwise a JsonElement. - internal static object ReadNumber(ref Utf8JsonReader reader) + /// + /// + /// + internal static object ReadPropertyValueAsObject(ref Utf8JsonReader reader, string propertyName, string className) { - // Assume reader is a Utf8JsonReader positioned at a JsonTokenType.Number - if (reader.TryGetInt32(out int i)) - return i; - else if (reader.TryGetInt64(out long l)) - return l; - else if (reader.TryGetUInt32(out uint u)) - return u; - else if (reader.TryGetSingle(out float f)) - return f; - else if (reader.TryGetDouble(out double d)) - return d; - else if (reader.TryGetDecimal(out decimal m)) - return m; + switch (reader.TokenType) + { + case JsonTokenType.False: + return false; + case JsonTokenType.Number: + return ReadNumber(ref reader); + case JsonTokenType.True: + return true; + case JsonTokenType.Null: + return null; + case JsonTokenType.String: + return ReadStringAsObject(ref reader, propertyName, className); + case JsonTokenType.StartObject: + return ReadJsonElement(ref reader); + case JsonTokenType.StartArray: + return ReadArrayOfObjects(ref reader, propertyName, className); + default: + // There is something broken here as this was called when the reader is pointing at a property. + // It must be a known Json type. + Debug.Assert(false, $"Utf8JsonReader.TokenType is not one of the expected types: False, Number, True, Null, String, StartArray, StartObject. Is: '{reader.TokenType}'."); + return null; + } + } + #endregion - Debug.Assert(false, "expected to read a number, but none of the Utf8JsonReader.TryGet... methods returned true."); + #region Write + public static void WriteAsJsonElement(ref Utf8JsonWriter writer, string json) + { + Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json).AsSpan()); - return ReadJsonElement(ref reader); +#if NET6_0_OR_GREATER + if (JsonElement.TryParseValue(ref reader, out JsonElement? jsonElement)) + jsonElement.Value.WriteTo(writer); +#else + using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader)) + jsonDocument.RootElement.WriteTo(writer); +#endif } - internal static void WriteAdditionalData(ref Utf8JsonWriter writer, IDictionary additionalData) + public static void WriteObjects(ref Utf8JsonWriter writer, IDictionary dictionary) { - if (additionalData.Count > 0) - { - foreach (KeyValuePair kvp in additionalData) - { - if (kvp.Value is string) - writer.WriteString(kvp.Key, kvp.Value as string); - else if (kvp.Value is int) - writer.WriteNumber(kvp.Key, (int)kvp.Value); - else if (kvp.Value is bool) - writer.WriteBoolean(kvp.Key, (bool)kvp.Value); - else if (kvp.Value is decimal) - writer.WriteNumber(kvp.Key, (decimal)kvp.Value); - else if (kvp.Value is double) - writer.WriteNumber(kvp.Key, (double)kvp.Value); - else if (kvp.Value is float) - writer.WriteNumber(kvp.Key, (float)kvp.Value); - else if (kvp.Value is long) - writer.WriteNumber(kvp.Key, (long)kvp.Value); - else if (kvp.Value is null) - writer.WriteNull(kvp.Key); - else if (kvp.Value is JsonElement element) - { - writer.WritePropertyName(kvp.Key); - element.WriteTo(writer); - } - else - { - writer.WriteString(kvp.Key, kvp.Value.ToString()); - } - } - } + if (dictionary?.Count > 0) + foreach (KeyValuePair kvp in dictionary) + WriteObject(ref writer, kvp.Key, kvp.Value); } - internal static void WriteObject(ref Utf8JsonWriter writer, string key, object obj) + /// + /// Writes an 'object' as a JsonProperty. + /// This was written to support what IdentityModel6x supported and is not meant to be a + /// general object serializer. + /// If a user needs to serialize a special value, then serialize the value into a JsonElement. + /// + /// + /// + /// + /// The current depth of recursive call for objects. + /// Maximum is 2. + public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj, int depth = 0) { - if (obj is string) - writer.WriteString(key, obj as string); - else if (obj is int) - writer.WriteNumber(key, (int)obj); - else if (obj is bool) - writer.WriteBoolean(key, (bool)obj); - else if (obj is decimal) - writer.WriteNumber(key, (decimal)obj); - else if (obj is double) - writer.WriteNumber(key, (double)obj); - else if (obj is float) - writer.WriteNumber(key, (float)obj); - else if (obj is long) - writer.WriteNumber(key, (long)obj); + if (obj is string str) + writer.WriteString(key, str); + else if (obj is DateTime dt) + writer.WriteString(key, dt.ToUniversalTime()); + else if (obj is int i) + writer.WriteNumber(key, i); + else if (obj is bool b) + writer.WriteBoolean(key, b); + else if (obj is decimal d) + writer.WriteNumber(key, d); + else if (obj is double dub) + writer.WriteNumber(key, dub); + else if (obj is float f) + writer.WriteNumber(key, f); + else if (obj is long l) + writer.WriteNumber(key, l); else if (obj is null) writer.WriteNull(key); - else if (obj is JsonElement) + else if (obj is List strs) + { + writer.WriteStartArray(key); + foreach (string item in strs) + writer.WriteStringValue(item); + + writer.WriteEndArray(); + } + else if (depth < MaxDepth && obj is List objs) + { + depth++; + writer.WriteStartArray(key); + foreach (object item in objs) + WriteObjectValue(ref writer, item, depth); + + writer.WriteEndArray(); + } + else if (obj is IDictionary idics) + { + writer.WriteStartObject(key); + foreach (KeyValuePair kvp in idics) + writer.WriteString(kvp.Key, kvp.Value); + + writer.WriteEndObject(); + } + else if (depth < MaxDepth && obj is IDictionary idic) + { + depth++; + writer.WriteStartObject(key); + foreach (KeyValuePair kvp in idic) + WriteObject(ref writer, kvp.Key, kvp.Value, depth); + + writer.WriteEndObject(); + } + else if (depth < MaxDepth && obj is Dictionary dic) + { + depth++; + writer.WriteStartObject(key); + foreach (KeyValuePair kvp in dic) + WriteObject(ref writer, kvp.Key, kvp.Value, depth); + + writer.WriteEndObject(); + } + else if (obj is JsonElement j) { writer.WritePropertyName(key); - ((JsonElement)obj).WriteTo(writer); + j.WriteTo(writer); } else { @@ -419,34 +518,83 @@ internal static void WriteObject(ref Utf8JsonWriter writer, string key, object o } } - internal static void WriteStrings(ref Utf8JsonWriter writer, ReadOnlySpan propertyName, IList strings) + /// + /// Writes values into an array. + /// Assumes the writer.StartArray() has been called. + /// + /// + /// + /// The current depth of recursive call for objects. + /// Maximum is 3. + public static void WriteObjectValue(ref Utf8JsonWriter writer, object obj, int depth = 0) + { + if (obj is string str) + writer.WriteStringValue(str); + else if (obj is DateTime dt) + writer.WriteStringValue(dt.ToUniversalTime()); + else if (obj is int i) + writer.WriteNumberValue(i); + else if (obj is bool b) + writer.WriteBooleanValue(b); + else if (obj is double d) + writer.WriteNumberValue((decimal)d); + else if (obj is decimal m) + writer.WriteNumberValue(m); + else if (obj is float f) + writer.WriteNumberValue(f); + else if (obj is long l) + writer.WriteNumberValue(l); + else if (obj is null) + writer.WriteNullValue(); + else if (obj is JsonElement j) + j.WriteTo(writer); + else if (obj is List strings) + { + writer.WriteStartArray(); + foreach (string strValue in strings) + writer.WriteStringValue(strValue); + + writer.WriteEndArray(); + } + else if (depth < MaxDepth && obj is List objs) + { + depth++; + writer.WriteStartArray(); + foreach (object item in objs) + WriteObjectValue(ref writer, item, depth); + + writer.WriteEndArray(); + } + else + writer.WriteStringValue(obj.ToString()); + } + + public static void WriteStrings(ref Utf8JsonWriter writer, ReadOnlySpan propertyName, IList strings) { - writer.WritePropertyName(propertyName); - writer.WriteStartArray(); + writer.WriteStartArray(propertyName); foreach (string str in strings) writer.WriteStringValue(str); writer.WriteEndArray(); } - internal static void WriteStrings(ref Utf8JsonWriter writer, ReadOnlySpan propertyName, ICollection strings) + public static void WriteStrings(ref Utf8JsonWriter writer, ReadOnlySpan propertyName, ICollection strings) { - writer.WritePropertyName(propertyName); - writer.WriteStartArray(); + writer.WriteStartArray(propertyName); foreach (string str in strings) writer.WriteStringValue(str); writer.WriteEndArray(); } - internal static void WriteStrings(ref Utf8JsonWriter writer, JsonEncodedText propertyName, IList strings) + public static void WriteStrings(ref Utf8JsonWriter writer, JsonEncodedText propertyName, IList strings) { - writer.WritePropertyName(propertyName); - writer.WriteStartArray(); + writer.WriteStartArray(propertyName); foreach (string str in strings) writer.WriteStringValue(str); writer.WriteEndArray(); } +#endregion } } diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs index 3312863869..8ff6c8a02e 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySerializer.cs @@ -80,7 +80,7 @@ public static JsonWebKey Read(ref Utf8JsonReader reader, JsonWebKey jsonWebKey) LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed)))); - while(JsonSerializerPrimitives.ReaderRead(ref reader)) + while(reader.Read()) { #region Check property name using ValueTextEquals // common names are tried first @@ -90,65 +90,30 @@ public static JsonWebKey Read(ref Utf8JsonReader reader, JsonWebKey jsonWebKey) if (reader.TokenType == JsonTokenType.PropertyName) { if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.K)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.K = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.K, JsonWebKey.ClassName); - } + jsonWebKey.K = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.K, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.E)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.E = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.E, JsonWebKey.ClassName); - } + jsonWebKey.E = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.E, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Kid)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Kid = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Kid, JsonWebKey.ClassName); - } + jsonWebKey.Kid = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Kid, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Kty)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Kty = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Kty, JsonWebKey.ClassName); - } + jsonWebKey.Kty = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Kty, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.N)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.N = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.N, JsonWebKey.ClassName); - } + jsonWebKey.N = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.N, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.X5c)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - JsonSerializerPrimitives.ReadStrings(ref reader, jsonWebKey.X5c, JsonWebKeyParameterNames.X5c, JsonWebKey.ClassName); - } + JsonSerializerPrimitives.ReadStrings(ref reader, jsonWebKey.X5c, JsonWebKeyParameterNames.X5c, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Alg)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Alg = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Alg, JsonWebKey.ClassName); - } + jsonWebKey.Alg = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Alg, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Crv)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Crv = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Crv, JsonWebKey.ClassName); - } + jsonWebKey.Crv = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Crv, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.D)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.D = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.D, JsonWebKey.ClassName); - } + jsonWebKey.D = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.D, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.DP)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.DP = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.DP, JsonWebKey.ClassName); - } + jsonWebKey.DP = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.DP, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.DQ)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.DQ = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.DQ, JsonWebKey.ClassName); - } + jsonWebKey.DQ = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.DQ, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.KeyOps)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); // the value can be null if the value is 'nill' - if (JsonSerializerPrimitives.ReadStrings(ref reader, jsonWebKey.KeyOps, JsonWebKeyParameterNames.KeyOps, JsonWebKey.ClassName) == null) + if (JsonSerializerPrimitives.ReadStrings(ref reader, jsonWebKey.KeyOps, JsonWebKeyParameterNames.KeyOps, JsonWebKey.ClassName, true) == null) { throw LogHelper.LogExceptionMessage( new ArgumentNullException( @@ -164,67 +129,36 @@ public static JsonWebKey Read(ref Utf8JsonReader reader, JsonWebKey jsonWebKey) LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed))))); } - } else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Oth)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - JsonSerializerPrimitives.ReadStrings(ref reader, jsonWebKey.Oth, JsonWebKeyParameterNames.Oth, JsonWebKey.ClassName); - } + JsonSerializerPrimitives.ReadStrings(ref reader, jsonWebKey.Oth, JsonWebKeyParameterNames.Oth, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.P)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.P = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.P, JsonWebKey.ClassName); - } + jsonWebKey.P = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.P, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Q)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Q = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Q, JsonWebKey.ClassName); - } + jsonWebKey.Q = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Q, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.QI)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.QI = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.QI, JsonWebKey.ClassName); - } + jsonWebKey.QI = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.QI, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Use)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Use = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Use, JsonWebKey.ClassName); - } + jsonWebKey.Use = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Use, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.X)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.X = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X, JsonWebKey.ClassName); - } + jsonWebKey.X = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.X5t)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.X5t = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X5t, JsonWebKey.ClassName); - } + jsonWebKey.X5t = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X5t, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.X5tS256)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.X5tS256 = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X5tS256, JsonWebKey.ClassName); - } + jsonWebKey.X5tS256 = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X5tS256, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.X5u)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.X5u = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X5u, JsonWebKey.ClassName); - } + jsonWebKey.X5u = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.X5u, JsonWebKey.ClassName, true); else if (reader.ValueTextEquals(JsonWebKeyParameterUtf8Bytes.Y)) - { - JsonSerializerPrimitives.ReaderRead(ref reader); - jsonWebKey.Y = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Y, JsonWebKey.ClassName); - } + jsonWebKey.Y = JsonSerializerPrimitives.ReadString(ref reader, JsonWebKeyParameterNames.Y, JsonWebKey.ClassName, true); #endregion else { #region case-insensitive // fallback to checking property names as case insensitive // first check to see if the upper case property value is a valid property name if not add to AdditionalData, to avoid unnecessary string compares. - string propertyName = JsonSerializerPrimitives.GetPropertyName(ref reader, JsonWebKey.ClassName, true); + string propertyName = JsonSerializerPrimitives.ReadPropertyName(ref reader, JsonWebKey.ClassName, true); if (!JsonWebKeyParameterNamesUpperCase.Contains(propertyName.ToUpperInvariant())) { - jsonWebKey.AdditionalData[propertyName] = JsonSerializerPrimitives.GetUnknownProperty(ref reader); + jsonWebKey.AdditionalData[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonWebKey.ClassName); } else { @@ -355,7 +289,8 @@ public static string Write(JsonWebKey jsonWebKey) writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); Write(ref writer, jsonWebKey); writer.Flush(); - return Encoding.UTF8.GetString(memoryStream.ToArray()); + + return Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); } finally { @@ -437,7 +372,7 @@ public static void Write(ref Utf8JsonWriter writer, JsonWebKey jsonWebKey) if (!string.IsNullOrEmpty(jsonWebKey.Y)) writer.WriteString(JsonWebKeyParameterUtf8Bytes.Y, jsonWebKey.Y); - JsonSerializerPrimitives.WriteAdditionalData(ref writer, jsonWebKey.AdditionalData); + JsonSerializerPrimitives.WriteObjects(ref writer, jsonWebKey.AdditionalData); writer.WriteEndObject(); } diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySetSerializer.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySetSerializer.cs index 6b0542f824..a9a976a56e 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySetSerializer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonWebKeySetSerializer.cs @@ -42,22 +42,22 @@ public static JsonWebKeySet Read(ref Utf8JsonReader reader, JsonWebKeySet jsonWe LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed)))); - while (JsonSerializerPrimitives.ReaderRead(ref reader)) + while (reader.Read()) { if (reader.TokenType == JsonTokenType.PropertyName) { if (reader.ValueTextEquals(_keysUtf8)) { - JsonSerializerPrimitives.ReaderRead(ref reader); + reader.Read(); ReadKeys(ref reader, jsonWebKeySet); } else { - string propertyName = JsonSerializerPrimitives.GetPropertyName(ref reader, JsonWebKey.ClassName, true); + string propertyName = JsonSerializerPrimitives.ReadPropertyName(ref reader, JsonWebKeySet.ClassName, true); if (propertyName.Equals(JsonWebKeyParameterNames.Keys, StringComparison.OrdinalIgnoreCase)) ReadKeys(ref reader, jsonWebKeySet); else - jsonWebKeySet.AdditionalData[propertyName] = JsonSerializerPrimitives.GetUnknownProperty(ref reader); + jsonWebKeySet.AdditionalData[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader,JsonWebKeyParameterNames.Keys, JsonWebKeySet.ClassName); } } else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, true)) @@ -79,7 +79,7 @@ public static void ReadKeys(ref Utf8JsonReader reader, JsonWebKeySet jsonWebKeyS JsonWebKeyParameterNames.KeyOps, JsonWebKeySet.ClassName)); - while (JsonSerializerPrimitives.ReaderRead(ref reader)) + while (reader.Read()) { if (reader.TokenType == JsonTokenType.StartObject) jsonWebKeySet.Keys.Add(JsonWebKeySerializer.Read(ref reader, new JsonWebKey())); @@ -98,11 +98,11 @@ public static string Write(JsonWebKeySet jsonWebKeySet) Utf8JsonWriter writer = null; try { - // writing strings without escaping is as we know this is a utf8 encoding writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); Write(ref writer, jsonWebKeySet); writer.Flush(); - return Encoding.UTF8.GetString(memoryStream.ToArray()); + + return Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); } finally { @@ -129,7 +129,7 @@ public static void Write(ref Utf8JsonWriter writer, JsonWebKeySet jsonWebKeySet) writer.WriteEndArray(); if (jsonWebKeySet.AdditionalData.Count > 0) - JsonSerializerPrimitives.WriteAdditionalData(ref writer, jsonWebKeySet.AdditionalData); + JsonSerializerPrimitives.WriteObjects(ref writer, jsonWebKeySet.AdditionalData); writer.WriteEndObject(); } diff --git a/src/Microsoft.IdentityModel.Tokens/JsonWebKeyParameterNames.cs b/src/Microsoft.IdentityModel.Tokens/JsonWebKeyParameterNames.cs index c5f051114b..48d2efc19d 100644 --- a/src/Microsoft.IdentityModel.Tokens/JsonWebKeyParameterNames.cs +++ b/src/Microsoft.IdentityModel.Tokens/JsonWebKeyParameterNames.cs @@ -7,6 +7,7 @@ namespace Microsoft.IdentityModel.Tokens { /// /// JsonWebKey parameter names + /// see: https://datatracker.ietf.org/doc/html/rfc7517 /// public static class JsonWebKeyParameterNames { @@ -37,7 +38,11 @@ public static class JsonWebKeyParameterNames #pragma warning restore 1591 } - internal static class JsonWebKeyParameterUtf8Bytes + /// + /// JsonWebKey parameter names as UTF8 bytes + /// Used by UTF8JsonReader/Writer for performance gains. + /// + internal readonly struct JsonWebKeyParameterUtf8Bytes { public static ReadOnlySpan Alg => "alg"u8; public static ReadOnlySpan Crv => "crv"u8; diff --git a/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs b/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs index 6171c226e4..8f75b45125 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs @@ -6,8 +6,9 @@ using System.Globalization; using System.Linq; using System.Security.Claims; -using Newtonsoft.Json.Linq; using Microsoft.IdentityModel.Logging; + +using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace Microsoft.IdentityModel.Tokens @@ -63,8 +64,11 @@ internal static IDictionary CreateDictionaryFromClaims(IEnumerab IList claimValues = existingValue as IList; if (claimValues == null) { - claimValues = new List(); - claimValues.Add(existingValue); + claimValues = new List + { + existingValue + }; + payload[jsonClaimType] = claimValues; } @@ -79,6 +83,69 @@ internal static IDictionary CreateDictionaryFromClaims(IEnumerab return payload; } + internal static IDictionary CreateDictionaryFromClaims( + IEnumerable claims, + SecurityTokenDescriptor tokenDescriptor, + bool audienceSet, + bool issuerSet) + { + var payload = new Dictionary(); + + if (claims == null) + return payload; + + bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; + + foreach (Claim claim in claims) + { + if (claim == null) + continue; + + // skipping these as they will be added once by the caller + // why add them if we are going to replace them later + if (checkClaims && tokenDescriptor.Claims.ContainsKey(claim.Type)) + continue; + + if (audienceSet && claim.Type.Equals("aud", StringComparison.Ordinal)) + continue; + + if (issuerSet && claim.Type.Equals("iss", StringComparison.Ordinal)) + continue; + + if (tokenDescriptor.Expires.HasValue && claim.Type.Equals("exp", StringComparison.Ordinal)) + continue; + + if (tokenDescriptor.IssuedAt.HasValue && claim.Type.Equals("iat", StringComparison.Ordinal)) + continue; + + if (tokenDescriptor.NotBefore.HasValue && claim.Type.Equals("nbf", StringComparison.Ordinal)) + continue; + + object jsonClaimValue = claim.ValueType.Equals(ClaimValueTypes.String) ? claim.Value : GetClaimValueUsingValueType(claim); + + // The enumeration is from ClaimsIdentity.Claims, there can be duplicates. + // When a duplicate is detected, we create a List and add both to a list. + // When the creating the JWT and a list is found, a JsonArray will be created. + if (payload.TryGetValue(claim.Type, out object existingValue)) + { + if (existingValue is not IList) + { + payload[claim.Type] = new List + { + existingValue, + jsonClaimValue + }; + } + } + else + { + payload[claim.Type] = jsonClaimValue; + } + } + + return payload; + } + internal static object GetClaimValueUsingValueType(Claim claim) { if (claim.ValueType == ClaimValueTypes.String) @@ -97,13 +164,13 @@ internal static object GetClaimValueUsingValueType(Claim claim) return longValue; if (claim.ValueType == ClaimValueTypes.DateTime && DateTime.TryParse(claim.Value, out DateTime dateTimeValue)) - return dateTimeValue; + return dateTimeValue.ToUniversalTime(); if (claim.ValueType == Json) - return JObject.Parse(claim.Value); + return JsonPrimitives.CreateJsonElement(claim.Value); if (claim.ValueType == JsonArray) - return JArray.Parse(claim.Value); + return JsonPrimitives.CreateJsonElement(claim.Value); if (claim.ValueType == JsonNull) return string.Empty; @@ -133,6 +200,7 @@ internal static IEnumerable GetAllSigningKeys(BaseConfiguration con yield return key; } + // TODO - do not use yield if (validationParameters is not null) { LogHelper.LogInformation(TokenLogMessages.IDX10243); diff --git a/src/System.IdentityModel.Tokens.Jwt/JsonClaimValueTypes.cs b/src/System.IdentityModel.Tokens.Jwt/JsonClaimValueTypes.cs index 05ee865a3c..a3a49e46fc 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JsonClaimValueTypes.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JsonClaimValueTypes.cs @@ -1,29 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Security.Claims; + namespace System.IdentityModel.Tokens.Jwt { /// - /// Constants for Json Web tokens. + /// Constants that indicate how the should be evaluated. /// public static class JsonClaimValueTypes { /// - /// A URI that represents the JSON XML data type. + /// A value that indicates the is a Json object. /// - /// When mapping json to .Net Claim(s), if the value was not a string (or an enumeration of strings), the ClaimValue will serialized using the current JSON serializer, a property will be added with the .Net type and the ClaimTypeValue will be set to 'JsonClaimValueType'. - public const string Json = Microsoft.IdentityModel.JsonWebTokens.JsonClaimValueTypes.Json; + /// When creating a from Json to if the value was not a simple type {String, Null, True, False, Number} + /// then will contain the Json value. If the Json was a JsonObject, the will be set to "JSON". + public const string Json = "JSON"; /// - /// A URI that represents the JSON array XML data type. + /// A value that indicates the is a Json object. /// - /// When mapping json to .Net Claim(s), if the value was not a string (or an enumeration of strings), the ClaimValue will serialized using the current JSON serializer, a property will be added with the .Net type and the ClaimTypeValue will be set to 'JsonClaimValueType'. - public const string JsonArray = Microsoft.IdentityModel.JsonWebTokens.JsonClaimValueTypes.JsonArray; + /// When creating a from Json to if the value was not a simple type {String, Null, True, False, Number} + /// then will contain the Json value. If the Json was a JsonArray, the will be set to "JSON_ARRAY". + public const string JsonArray = "JSON_ARRAY"; /// - /// A URI that represents the JSON null data type + /// A value that indicates the is Json null. /// - /// When mapping json to .Net Claim(s), we use empty string to represent the claim value and set the ClaimValueType to JsonNull - public const string JsonNull = Microsoft.IdentityModel.JsonWebTokens.JsonClaimValueTypes.JsonNull; + /// When creating a the cannot be null. The the Json value was nil, then the + /// will be set to and the will be set to "JSON_NULL". + public const string JsonNull = "JSON_NULL"; } } diff --git a/src/System.IdentityModel.Tokens.Jwt/JsonExtensions.cs b/src/System.IdentityModel.Tokens.Jwt/JsonExtensions.cs deleted file mode 100644 index b2d0496810..0000000000 --- a/src/System.IdentityModel.Tokens.Jwt/JsonExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.IdentityModel.Logging; -using Newtonsoft.Json; - -namespace System.IdentityModel.Tokens.Jwt -{ - /// - /// Delegate that can be set on to control serialization of objects into JSON. - /// - /// Object to serialize - /// The serialized object. - public delegate string Serializer(object obj); - - /// - /// Delegate that can be set on to control deserialization JSON into objects. - /// - /// JSON to deserialize. - /// Type expected. - /// The deserialized object. - public delegate object Deserializer(string obj, Type targetType); - - /// - /// Dictionary extensions for serializations - /// - public static class JsonExtensions - { - private static Serializer _serializer = JsonConvert.SerializeObject; - private static Deserializer _deserializer = JsonConvert.DeserializeObject; - - /// - /// Gets or sets a to use when serializing objects to JSON. - /// - /// If 'value' is null. - public static Serializer Serializer - { - get - { - return _serializer; - } - set - { - _serializer = value ?? throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(value))); - } - } - - /// - /// Gets or sets a to use when deserializing objects from JSON. - /// - /// If 'value' is null. - public static Deserializer Deserializer - { - get - { - return _deserializer; - } - set - { - _deserializer = value ?? throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(value))); - } - } - - /// - /// Serializes an object to JSON. - /// - /// The object to serialize - /// The object as JSON. - public static string SerializeToJson(object value) - { - return Serializer(value); - } - - /// - /// Deserialzes JSON into an instance of type T. - /// - /// The object type. - /// The JSON to deserialze. - /// A new instance of type T. - public static T DeserializeFromJson(string jsonString) where T : class - { - return Deserializer(jsonString, typeof(T)) as T; - } - - /// - /// Deserialzes JSON into an instance of . - /// - /// The JSON to deserialze. - /// A new instance . - public static JwtHeader DeserializeJwtHeader(string jsonString) - { - return Deserializer(jsonString, typeof(JwtHeader)) as JwtHeader; - } - - /// - /// Deserialzes JSON into an instance of . - /// - /// The JSON to deserialze. - /// A new instance . - public static JwtPayload DeserializeJwtPayload(string jsonString) - { - return Deserializer(jsonString, typeof(JwtPayload)) as JwtPayload; - } - } -} diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtHeader.cs b/src/System.IdentityModel.Tokens.Jwt/JwtHeader.cs index d0229ca6e4..2f8829cd6e 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtHeader.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtHeader.cs @@ -2,10 +2,16 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; + namespace System.IdentityModel.Tokens.Jwt { /// @@ -16,6 +22,8 @@ namespace System.IdentityModel.Tokens.Jwt [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable"), System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Serialize not really supported.")] public class JwtHeader : Dictionary { + internal string ClassName = "System.IdentityModel.Tokens.Jwt.JwtHeader"; + /// /// Initializes a new instance of the class. Default string comparer . /// @@ -24,6 +32,68 @@ public JwtHeader() { } + /// + /// Initializes a new instance of the class. Default string comparer . + /// + internal JwtHeader(string json) + { + _ = json ?? throw LogHelper.LogArgumentNullException(nameof(json)); + + Utf8JsonReader reader = new(Encoding.UTF8.GetBytes(json)); + + if (!JsonPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, false)) + throw LogHelper.LogExceptionMessage( + new JsonException( + LogHelper.FormatInvariant( + Microsoft.IdentityModel.Tokens.LogMessages.IDX11023, + LogHelper.MarkAsNonPII("JsonTokenType.StartObject"), + LogHelper.MarkAsNonPII(reader.TokenType), + LogHelper.MarkAsNonPII(ClassName), + LogHelper.MarkAsNonPII(reader.TokenStartIndex), + LogHelper.MarkAsNonPII(reader.CurrentDepth), + LogHelper.MarkAsNonPII(reader.BytesConsumed)))); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string propertyName = JsonPrimitives.ReadPropertyName(ref reader, ClassName, true); + object obj; + if (reader.TokenType == JsonTokenType.StartArray) + obj = JsonPrimitives.ReadArrayOfObjects(ref reader, propertyName, ClassName); + else + obj = JsonPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, ClassName); + + if (TryGetValue(propertyName, out object existingValue)) + { + if (existingValue is not IList claimValues) + { + claimValues = new List + { + existingValue + }; + + this[propertyName] = claimValues; + } + + if (obj is IList objectList) + { + foreach (object item in objectList) + claimValues.Add(item); + } + else + { + claimValues.Add(obj); + } + } + else + { + this[propertyName] = obj; + } + } + } + } + /// /// Initializes a new instance of . /// With the Header Parameters: @@ -314,32 +384,22 @@ public string X5t /// /// Base64url encoded JSON to deserialize. /// An instance of . - /// Use to customize JSON serialization. public static JwtHeader Base64UrlDeserialize(string base64UrlEncodedJsonString) { - return JsonExtensions.DeserializeJwtHeader(Base64UrlEncoder.Decode(base64UrlEncodedJsonString)); + _ = base64UrlEncodedJsonString ?? throw LogHelper.LogArgumentNullException(nameof(base64UrlEncodedJsonString)); + + return new JwtHeader(Base64UrlEncoder.Decode(base64UrlEncodedJsonString)); } /// /// Encodes this instance as Base64UrlEncoded JSON. /// /// Base64UrlEncoded JSON. - /// Use to customize JSON serialization. public virtual string Base64UrlEncode() { return Base64UrlEncoder.Encode(SerializeToJson()); } - /// - /// Deserialzes JSON into a instance. - /// - /// The JSON to deserialize. - /// An instance of . - /// Use to customize JSON serialization. - public static JwtHeader Deserialize(string jsonString) - { - return JsonExtensions.DeserializeJwtHeader(jsonString); - } /// /// Gets a standard claim from the header. /// A standard claim is either a string or a value of another type serialized in JSON format. @@ -356,7 +416,8 @@ internal string GetStandardClaim(string claimType) if (value is string str) return str; - return JsonExtensions.SerializeToJson(value); + // TODO - review dev + return string.Empty; } return null; @@ -392,10 +453,29 @@ internal void AddAdditionalClaims(IDictionary additionalHeaderCl /// Serializes this instance to JSON. /// /// This instance as JSON. - /// Use to customize JSON serialization. public virtual string SerializeToJson() { - return JsonExtensions.SerializeToJson(this as IDictionary); + // TODO - common method for JwtPayload and JwtHeader + using (MemoryStream memoryStream = new MemoryStream()) + { + Utf8JsonWriter writer = null; + + try + { + writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartObject(); + + JsonPrimitives.WriteObjects(ref writer, this); + + writer.WriteEndObject(); + writer.Flush(); + return Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); + } + finally + { + writer?.Dispose(); + } + } } } } diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs b/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs index 5be394e9e9..5503b1cff0 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs @@ -3,12 +3,17 @@ using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; + +using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; namespace System.IdentityModel.Tokens.Jwt { @@ -18,6 +23,8 @@ namespace System.IdentityModel.Tokens.Jwt [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable"), System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Serialize not really supported.")] public class JwtPayload : Dictionary { + internal string ClassName = "System.IdentityModel.Tokens.Jwt.JwtPayload"; + /// /// Initializes a new instance of the class with no claims. Default string comparer . /// Creates a empty @@ -27,6 +34,63 @@ public JwtPayload() { } + internal JwtPayload (string json) + { + Utf8JsonReader reader = new(Encoding.UTF8.GetBytes(json)); + + if (!JsonPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, false)) + throw LogHelper.LogExceptionMessage( + new JsonException( + LogHelper.FormatInvariant( + Microsoft.IdentityModel.Tokens.LogMessages.IDX11023, + LogHelper.MarkAsNonPII("JsonTokenType.StartObject"), + LogHelper.MarkAsNonPII(reader.TokenType), + LogHelper.MarkAsNonPII(ClassName), + LogHelper.MarkAsNonPII(reader.TokenStartIndex), + LogHelper.MarkAsNonPII(reader.CurrentDepth), + LogHelper.MarkAsNonPII(reader.BytesConsumed)))); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string propertyName = JsonPrimitives.ReadPropertyName(ref reader, ClassName, true); + object obj; + if (reader.TokenType == JsonTokenType.StartArray) + obj = JsonPrimitives.ReadArrayOfObjects(ref reader, propertyName, ClassName); + else + obj = JsonPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, ClassName); + + if (TryGetValue(propertyName, out object existingValue)) + { + if (existingValue is not IList claimValues) + { + claimValues = new List + { + existingValue + }; + + this[propertyName] = claimValues; + } + + if (obj is IList objectList) + { + foreach (object item in objectList) + claimValues.Add(item); + } + else + { + claimValues.Add(obj); + } + } + else + { + this[propertyName] = obj; + } + } + } + } + /// /// Initializes a new instance of the class with . Default string comparer . /// The claims to add. @@ -115,14 +179,14 @@ internal void AddFirstPriorityClaims(string issuer, string audience, DateTime? n throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, LogHelper.MarkAsNonPII(expires.Value), LogHelper.MarkAsNonPII(notBefore.Value)))); } - this[JwtRegisteredClaimNames.Nbf] = EpochTime.GetIntDate(notBefore.Value.ToUniversalTime()); + this[JwtRegisteredClaimNames.Nbf] = (int)EpochTime.GetIntDate(notBefore.Value.ToUniversalTime()); } - this[JwtRegisteredClaimNames.Exp] = EpochTime.GetIntDate(expires.Value.ToUniversalTime()); + this[JwtRegisteredClaimNames.Exp] = (int)EpochTime.GetIntDate(expires.Value.ToUniversalTime()); } if (issuedAt.HasValue) - this[JwtRegisteredClaimNames.Iat] = EpochTime.GetIntDate(issuedAt.Value.ToUniversalTime()); + this[JwtRegisteredClaimNames.Iat] = (int)EpochTime.GetIntDate(issuedAt.Value.ToUniversalTime()); if (!string.IsNullOrEmpty(issuer)) this[JwtRegisteredClaimNames.Iss] = issuer; @@ -215,7 +279,7 @@ public string CHash return this.GetStandardClaim(JwtRegisteredClaimNames.CHash); } } - + /// /// Gets the 'value' of the 'expiration' claim { exp, 'value' }. /// @@ -278,7 +342,7 @@ public string Nonce return this.GetStandardClaim(JwtRegisteredClaimNames.Nonce); } } - + /// /// Gets the 'value' of the 'subject' claim { sub, 'value' }. /// @@ -326,7 +390,7 @@ public DateTime IssuedAt return this.GetDateTime(JwtRegisteredClaimNames.Iat); } } - + /// /// Gets a for each JSON { name, value }. /// @@ -337,132 +401,68 @@ public virtual IEnumerable Claims get { List claims = new List(); - string issuer = this.Iss ?? ClaimsIdentity.DefaultIssuer; + string issuer = Iss ?? ClaimsIdentity.DefaultIssuer; - // there is some code redundancy here that was not factored as this is a high use method. Each identity received from the host will pass through here. foreach (KeyValuePair keyValuePair in this) { if (keyValuePair.Value == null) - { claims.Add(new Claim(keyValuePair.Key, string.Empty, JsonClaimValueTypes.JsonNull, issuer, issuer)); - continue; - } - var claimValue = keyValuePair.Value as string; - if (claimValue != null) - { - claims.Add(new Claim(keyValuePair.Key, claimValue, ClaimValueTypes.String, issuer, issuer)); - continue; - } + else if (keyValuePair.Value is string str) + claims.Add(new Claim(keyValuePair.Key, str, GetClaimValueType(str), issuer, issuer)); - var jtoken = keyValuePair.Value as JToken; - if (jtoken != null) - { - AddClaimsFromJToken(claims, keyValuePair.Key, jtoken, issuer); - continue; - } + else if (keyValuePair.Value is JsonElement j) + AddClaimsFromJsonElement(keyValuePair.Key, issuer, j, claims); // in this case, the payload was most likely never serialized. - var objects = keyValuePair.Value as IEnumerable; - if (objects != null) - { - foreach (var obj in objects) - { - claimValue = obj as string; - if (claimValue != null) - { - claims.Add(new Claim(keyValuePair.Key, claimValue, ClaimValueTypes.String, issuer, issuer)); - continue; - } - - jtoken = obj as JToken; - if (jtoken != null) - { - AddDefaultClaimFromJToken(claims, keyValuePair.Key, jtoken, issuer); - continue; - } - - // DateTime claims require special processing. JsonConvert.SerializeObject(obj) will result in "\"dateTimeValue\"". The quotes will be added. - if (obj is DateTime dateTimeValue) - claims.Add(new Claim(keyValuePair.Key, dateTimeValue.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); - else - claims.Add(new Claim(keyValuePair.Key, JsonConvert.SerializeObject(obj), GetClaimValueType(obj), issuer, issuer)); - } - - continue; - } + else if (keyValuePair.Value is IEnumerable objects) + AddListofObjects(keyValuePair.Key, objects, claims, issuer); - IDictionary dictionary = keyValuePair.Value as IDictionary; - if (dictionary != null) + else if (keyValuePair.Value is IDictionary dictionary) { foreach (var item in dictionary) - claims.Add(new Claim(keyValuePair.Key, "{" + item.Key + ":" + JsonConvert.SerializeObject(item.Value) + "}", GetClaimValueType(item.Value), issuer, issuer)); - - continue; + if (item.Value != null) + claims.Add(new Claim(keyValuePair.Key, "{" + item.Key + ":" + item.Value.ToString() + "}", GetClaimValueType(item.Value), issuer, issuer)); } - - // DateTime claims require special processing. JsonConvert.SerializeObject(keyValuePair.Value) will result in "\"dateTimeValue\"". The quotes will be added. - if (keyValuePair.Value is DateTime dateTime) - claims.Add(new Claim(keyValuePair.Key, dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); - else - claims.Add(new Claim(keyValuePair.Key, JsonConvert.SerializeObject(keyValuePair.Value), GetClaimValueType(keyValuePair.Value), issuer, issuer)); + else if (keyValuePair.Value is DateTime dateTime) + claims.Add(new Claim(keyValuePair.Key, dateTime.ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); + else if (keyValuePair.Value != null) + claims.Add(new Claim(keyValuePair.Key, keyValuePair.Value.ToString(), GetClaimValueType(keyValuePair.Value), issuer, issuer)); } return claims; } } - private static void AddClaimsFromJToken(List claims, string claimType, JToken jtoken, string issuer) + private void AddListofObjects(string key, IEnumerable objects, List claims, string issuer) { - if (jtoken.Type == JTokenType.Object) - { - claims.Add(new Claim(claimType, jtoken.ToString(Formatting.None), JsonClaimValueTypes.Json, issuer, issuer)); - } - else if (jtoken.Type == JTokenType.Array) - { - var jarray = jtoken as JArray; - foreach (var item in jarray) - { - switch (item.Type) - { - case JTokenType.Object: - claims.Add(new Claim(claimType, item.ToString(Formatting.None), JsonClaimValueTypes.Json, issuer, issuer)); - break; - - // only go one level deep on arrays. - case JTokenType.Array: - claims.Add(new Claim(claimType, item.ToString(Formatting.None), JsonClaimValueTypes.JsonArray, issuer, issuer)); - break; - - default: - AddDefaultClaimFromJToken(claims, claimType, item, issuer); - break; - } - } - } - else + foreach (var obj in objects) { - AddDefaultClaimFromJToken(claims, claimType, jtoken, issuer); + if (obj is string claimValue) + claims.Add(new Claim(key, claimValue, ClaimValueTypes.String, issuer, issuer)); + else if (obj is DateTime dateTimeValue) + claims.Add(new Claim(key, dateTimeValue.ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); + else if (obj is JsonElement jsonElement) + claims.Add(JsonClaimSet.CreateClaimFromJsonElement(key, issuer, jsonElement)); + else if (obj is IEnumerable innerObjects) + AddListofObjects(key, innerObjects, claims, issuer); + else + claims.Add(new Claim(key, obj.ToString(), GetClaimValueType(obj), issuer, issuer)); } } - private static void AddDefaultClaimFromJToken(List claims, string claimType, JToken jtoken, string issuer) + internal static void AddClaimsFromJsonElement(string claimType, string issuer, JsonElement jsonElement, List claims) { - JValue jvalue = jtoken as JValue; - if (jvalue != null) + // handle arrays to a single level + if (jsonElement.ValueKind == JsonValueKind.Array) { - // String is special because item.ToString(Formatting.None) will result in "/"string/"". The quotes will be added. - // Boolean needs item.ToString otherwise 'true' => 'True' - if (jvalue.Type == JTokenType.String) - claims.Add(new Claim(claimType, jvalue.Value.ToString(), ClaimValueTypes.String, issuer, issuer)); - // DateTime claims require special processing. jtoken.ToString(Formatting.None) will result in "\"dateTimeValue\"". The quotes will be added. - else if (jvalue.Value is DateTime dateTimeValue) - claims.Add(new Claim(claimType, dateTimeValue.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); - else - claims.Add(new Claim(claimType, jtoken.ToString(Formatting.None), GetClaimValueType(jvalue.Value), issuer, issuer)); + foreach (JsonElement element in jsonElement.EnumerateArray()) + claims.Add(JsonClaimSet.CreateClaimFromJsonElement(claimType, issuer, element)); } else - claims.Add(new Claim(claimType, jtoken.ToString(Formatting.None), GetClaimValueType(jtoken), issuer, issuer)); + { + claims.Add(JsonClaimSet.CreateClaimFromJsonElement(claimType, issuer, jsonElement)); + } } /// @@ -527,53 +527,44 @@ public void AddClaims(IEnumerable claims) /// Adds claims from dictionary. /// /// A dictionary of claims. - /// If a key is already present in target dictionary, its value is overridden by the value of the key in claimsCollection. + /// If a key is already present in target dictionary, its claimValue is overridden by the claimValue of the key in claimsCollection. internal void AddDictionaryClaims(IDictionary claimsCollection) { if (claimsCollection == null) throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(claimsCollection))); - foreach (string type in claimsCollection.Keys) - this[type] = claimsCollection[type]; + foreach (KeyValuePair kvp in claimsCollection) + this[kvp.Key] = kvp.Value; } - internal static string GetClaimValueType(object obj) + internal static string GetClaimValueType(object value) { - if (obj == null) + if (value == null) return JsonClaimValueTypes.JsonNull; - var objType = obj.GetType(); - - if (objType == typeof(string)) - return ClaimValueTypes.String; + Type objType = value.GetType(); - if (objType == typeof(int)) - return ClaimValueTypes.Integer; - - if (objType == typeof(bool)) + if (value is string str) + return JwtTokenUtilities.GetStringClaimValueType(str); + else if (objType == typeof(int)) + return ClaimValueTypes.Integer32; + else if (objType == typeof(long)) + return ClaimValueTypes.Integer64; + else if (objType == typeof(bool)) return ClaimValueTypes.Boolean; - - if (objType == typeof(double)) + else if (objType == typeof(double)) return ClaimValueTypes.Double; - - if (objType == typeof(long)) - { - long l = (long)obj; - if (l >= int.MinValue && l <= int.MaxValue) - return ClaimValueTypes.Integer; - - return ClaimValueTypes.Integer64; - } - - if (objType == typeof(DateTime)) + else if (objType == typeof(DateTime)) return ClaimValueTypes.DateTime; - - if (objType == typeof(JObject)) + else if (objType == typeof(float)) + return ClaimValueTypes.Double; + else if (objType == typeof(decimal)) + return ClaimValueTypes.Double; + else if (value is null) + return JsonClaimValueTypes.JsonNull; + else if (objType == typeof(JsonElement)) return JsonClaimValueTypes.Json; - if (objType == typeof(JArray)) - return JsonClaimValueTypes.JsonArray; - return objType.ToString(); } @@ -587,7 +578,7 @@ internal string GetStandardClaim(string claimType) if (value is string str) return str; - return JsonExtensions.SerializeToJson(value); + return string.Empty; } return null; @@ -595,65 +586,66 @@ internal string GetStandardClaim(string claimType) internal int? GetIntClaim(string claimType) { - int? retval = null; - - object value; - if (TryGetValue(claimType, out value)) + if (TryGetValue(claimType, out object claimValue)) { - IList claimValues = value as IList; - if (claimValues != null) + if (claimValue is IList objects) { - foreach (object obj in claimValues) + foreach (object obj in objects) { - retval = null; - if (obj == null) - { - continue; - } - - try - { - retval = Convert.ToInt32(Math.Truncate(Convert.ToDouble(obj, CultureInfo.InvariantCulture))); - } - catch (System.FormatException) - { - retval = null; - } - catch (System.InvalidCastException) - { - retval = null; - } - catch (OverflowException) - { - retval = null; - } - - if (retval != null) - { - return retval; - } + int i = default; + if (TryConvertToInt(obj, ref i)) + return i; } } else { - try - { - retval = Convert.ToInt32(Math.Truncate(Convert.ToDouble(value, CultureInfo.InvariantCulture))); - } - catch (System.FormatException) - { - retval = null; - } - catch (OverflowException) + int i = default; + if (TryConvertToInt(claimValue, ref i)) + return i; + } + } + + return null; + } + + private static bool TryConvertToInt(object value, ref int outVal) + { + outVal = default; + try + { + if (value is int i) + { + outVal = i; + return true; + } + + if (value is string str) + if (int.TryParse(str, out int result)) { - retval = null; + outVal = result; + return true; } - } - return retval; + + outVal = Convert.ToInt32(Math.Truncate(Convert.ToDouble(value, CultureInfo.InvariantCulture))); + return true; + } + catch (FormatException) + { + return false; + } + catch (OverflowException) + { + return false; + } + catch (InvalidCastException) + { + return false; } - return retval; +#pragma warning disable CS0162 // Unreachable code detected + return false; +#pragma warning restore CS0162 // Unreachable code detected } internal IList GetIListClaims(string claimType) @@ -684,7 +676,7 @@ internal IList GetIListClaims(string claimType) } else { - claimValues.Add(JsonExtensions.SerializeToJson(value)); + // TODO - do we need to do anything else } return claimValues; @@ -747,31 +739,49 @@ private DateTime GetDateTime(string key) /// Serializes this instance to JSON. /// /// This instance as JSON. - /// Use to customize JSON serialization. public virtual string SerializeToJson() { - return JsonExtensions.SerializeToJson(this as IDictionary); + using (MemoryStream memoryStream = new MemoryStream()) + { + Utf8JsonWriter writer = null; + + try + { + writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartObject(); + + JsonPrimitives.WriteObjects(ref writer, this); + + writer.WriteEndObject(); + writer.Flush(); + return Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); + } + finally + { + writer?.Dispose(); + } + } } /// - /// Encodes this instance as Base64UrlEncoded JSON. + /// Deserializes Base64UrlEncoded JSON into a instance. /// - /// Base64UrlEncoded JSON. - /// Use to customize JSON serialization. - public virtual string Base64UrlEncode() + /// Base64url encoded JSON to deserialize. + /// An instance of . + public static JwtPayload Base64UrlDeserialize(string base64UrlEncodedJsonString) { - return Base64UrlEncoder.Encode(SerializeToJson()); + _ = base64UrlEncodedJsonString ?? throw LogHelper.LogArgumentNullException(nameof(base64UrlEncodedJsonString)); + return new JwtPayload(Base64UrlEncoder.Decode(base64UrlEncodedJsonString)); } + /// - /// Deserializes Base64UrlEncoded JSON into a instance. + /// Encodes this instance as Base64UrlEncoded JSON. /// - /// base64url encoded JSON to deserialize. - /// An instance of . - /// Use to customize JSON serialization. - public static JwtPayload Base64UrlDeserialize(string base64UrlEncodedJsonString) + /// Base64UrlEncoded JSON. + public virtual string Base64UrlEncode() { - return JsonExtensions.DeserializeJwtPayload(Base64UrlEncoder.Decode(base64UrlEncodedJsonString)); + return Base64UrlEncoder.Encode(SerializeToJson()); } /// @@ -779,10 +789,11 @@ public static JwtPayload Base64UrlDeserialize(string base64UrlEncodedJsonString) /// /// The JSON to deserialize. /// An instance of . - /// Use to customize JSON serialization. public static JwtPayload Deserialize(string jsonString) { - return JsonExtensions.DeserializeJwtPayload(jsonString); + return new JwtPayload(jsonString); } + + internal JsonClaimSet ClaimSet { get; set; } } } diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityToken.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityToken.cs index 861b76094f..a405256b33 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityToken.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityToken.cs @@ -197,7 +197,7 @@ public string Actor { if (Payload != null) return Payload.Actort; - return String.Empty; + return string.Empty; } } @@ -207,7 +207,8 @@ public string Actor /// If the 'audience' claim is not found, enumeration will be empty. public IEnumerable Audiences { - get { + get + { if (Payload != null) return Payload.Aud; return new List(); @@ -222,7 +223,8 @@ public IEnumerable Audiences /// (s) returned will NOT have the translated according to public IEnumerable Claims { - get { + get + { if (Payload != null) return Payload.Claims; return new List(); @@ -246,7 +248,7 @@ public virtual string EncodedPayload { if (Payload != null) return Payload.Base64UrlEncode(); - return String.Empty; + return string.Empty; } } @@ -265,7 +267,7 @@ public override string Id { if (Payload != null) return Payload.Jti; - return String.Empty; + return string.Empty; } } @@ -280,7 +282,7 @@ public override string Issuer { if (Payload != null) return Payload.Iss; - return String.Empty; + return string.Empty; } } @@ -413,7 +415,7 @@ public string Subject { if (Payload != null) return Payload.Sub; - return String.Empty; + return string.Empty; } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonClaimSetTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonClaimSetTests.cs index e801cd28cc..3acf98a262 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonClaimSetTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonClaimSetTests.cs @@ -6,8 +6,8 @@ using System.Globalization; using System.Reflection; using System.Security.Claims; +using System.Text; using Microsoft.IdentityModel.TestUtils; -using Microsoft.IdentityModel.Tokens; using Xunit; #pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant @@ -34,6 +34,7 @@ public class JsonClaimSetTests }; [Theory, MemberData(nameof(ClaimSetTestCases))] + public void ClaimSetGetValueTests(JsonClaimSetTheoryData theoryData) { CompareContext context = TestUtilities.WriteHeader($"{this}.ClaimSetTests", theoryData); @@ -41,7 +42,7 @@ public void ClaimSetGetValueTests(JsonClaimSetTheoryData theoryData) try { - JsonClaimSet jsonClaimSet = new JsonClaimSet(theoryData.Json); + JsonClaimSet jsonClaimSet = new JsonClaimSet(Encoding.UTF8.GetBytes(theoryData.Json)); var methods = typeof(JsonClaimSet).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); var method = typeof(JsonClaimSet).GetMethod("GetValue", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Standard, new Type[] { typeof(string) }, null); var retval = method.MakeGenericMethod(theoryData.PropertyType).Invoke(jsonClaimSet, new object[] { theoryData.PropertyName }); @@ -82,15 +83,6 @@ public static TheoryData ClaimSetTestCases() #region datetime DateTime dateTime = new DateTime(2000, 01, 01, 0, 0, 0); - theoryData.Add(new JsonClaimSetTheoryData("datetime") - { - Json = $@"{{""datetime"":""{dateTime.ToString()}""}}", - PropertyName = "datetime", - PropertyType = typeof(DateTime), - PropertyValue = dateTime, - ShouldFind = true - }); - theoryData.Add(new JsonClaimSetTheoryData("datetimeAsString") { Json = $@"{{""datetime"":""{dateTime.ToString()}""}}", @@ -122,11 +114,10 @@ public static TheoryData ClaimSetTestCases() theoryData.Add(new JsonClaimSetTheoryData("IntegerAsIntArray") { - ExpectedException = new ExpectedException(typeof(TargetInvocationException), null, typeof(ArgumentException)) { InnerSubstringExpected = "IDX14305:" }, Json = $@"{{""IntegerAsIntArray"":1}}", PropertyName = "IntegerAsIntArray", PropertyType = typeof(int[]), - PropertyValue = (int) 1, + PropertyValue = new int[] { 1 }, ShouldFind = true }); @@ -135,7 +126,7 @@ public static TheoryData ClaimSetTestCases() Json = $@"{{""IntegersAsIntArray"":[1,2,3]}}", PropertyName = "IntegersAsIntArray", PropertyType = typeof(int[]), - PropertyValue = new int[] {1,2,3}, + PropertyValue = new int[] { 1, 2, 3 }, ShouldFind = true }); @@ -144,7 +135,7 @@ public static TheoryData ClaimSetTestCases() Json = $@"{{""IntegersAsObjArray"":[1,2,3]}}", PropertyName = "IntegersAsObjArray", PropertyType = typeof(object[]), - PropertyValue = new object[] { (long)1, (long)2, (long)3 }, + PropertyValue = new object[] { (int)1, (int)2, (int)3 }, ShouldFind = true }); @@ -153,7 +144,7 @@ public static TheoryData ClaimSetTestCases() Json = $@"{{""IntegersAsObj"":[1,2,3]}}", PropertyName = "IntegersAsObj", PropertyType = typeof(object[]), - PropertyValue = new object[] { (long)1, (long)2, (long)3 }, + PropertyValue = new object[] { (int)1, (int)2, (int)3 }, ShouldFind = true }); @@ -165,10 +156,10 @@ public static TheoryData ClaimSetTestCases() Json = $@"{{""MixedArrayAsObjArray"":[1,""2"",3]}}", PropertyName = "MixedArrayAsObjArray", PropertyType = typeof(object[]), - PropertyValue = new object[] { (long)1, "2", (long)3}, + PropertyValue = new object[] { (int)1, "2", (int)3}, ShouldFind = true }); -#endregion + #endregion #region strings theoryData.Add(new JsonClaimSetTheoryData("string") @@ -219,7 +210,6 @@ public static TheoryData ClaimSetTestCases() PropertyValue = (double)42.0, ShouldFind = true }); - #endregion return theoryData; diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index da86e007d0..6f65c78403 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -16,12 +16,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json; using Microsoft.IdentityModel.Validators; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant +using JsonWebTokenHandler6x = Microsoft.IdentityModel.JsonWebTokens.Tests.JsonWebTokenHandler6x; namespace Microsoft.IdentityModel.JsonWebTokens.Tests { @@ -798,9 +799,24 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa context.PropertiesToIgnoreWhenComparing = new Dictionary> { - { typeof(JsonWebToken), new List { "EncodedToken", "AuthenticationTag", "Ciphertext", "InitializationVector" } }, + { typeof(JsonWebToken), new List { "EncodedHeader", "EncodedToken", "AuthenticationTag", "Ciphertext", "InitializationVector" } }, }; + if (theoryData.PropertiesToIgnoreWhenComparing.Count > 0) + { + foreach (var ignore in theoryData.PropertiesToIgnoreWhenComparing) + { + if (context.PropertiesToIgnoreWhenComparing.TryGetValue(ignore.Key, out List list)) + { + list.AddRange(ignore.Value); + } + else + { + context.PropertiesToIgnoreWhenComparing[ignore.Key] = ignore.Value; + } + } + } + IdentityComparer.AreEqual(jweTokenFromSecurityTokenDescriptor, jweTokenFromString, context); theoryData.ExpectedException.ProcessNoException(context); } @@ -1002,6 +1018,10 @@ public static TheoryData CreateJWEUsingSecurityTokenDescr TokenDecryptionKey = KeyingMaterial.DefaultSymmetricSecurityKey_512, ValidAudience = "Audience", ValidIssuer = "Issuer" + }, + PropertiesToIgnoreWhenComparing = new Dictionary> + { + { typeof(JsonWebToken), new List { "InnerToken", "EncodedPayload", "EncodedSignature" } }, } }, new CreateTokenTheoryData @@ -1227,8 +1247,20 @@ public static TheoryData CreateJWSTheoryData public void CreateJWSWithAdditionalHeaderClaims(CreateTokenTheoryData theoryData) { var context = TestUtilities.WriteHeader($"{this}.CreateJWSWithAdditionalHeaderClaims", theoryData); + var jwtToken = new JsonWebTokenHandler().CreateToken(theoryData.TokenDescriptor); - IdentityComparer.AreEqual(jwtToken, theoryData.JwtToken, context); + var jwtToken6x = new JsonWebTokenHandler6x().CreateToken(theoryData.TokenDescriptor); + + JsonWebToken jsonWebToken = new JsonWebToken(jwtToken); + JsonWebToken jsonWebToken6x = new JsonWebToken(jwtToken6x); + + if (!IdentityComparer.AreEqual(jsonWebToken.Header, jsonWebToken6x.Header, context)) + { + context.AddDiff("jsonWebToken.Header != jsonWebToken6x.Header"); + context.AddDiff("********************************************"); + } + + IdentityComparer.AreEqual(jwtToken6x, theoryData.JwtToken, context); TestUtilities.AssertFailIfErrors(context); } @@ -1238,6 +1270,17 @@ public static TheoryData CreateJWSWithAdditionalHeaderCla { return new TheoryData { + new CreateTokenTheoryData + { + TestId = "DifferentTypHeaderValue", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + Claims = Default.PayloadDictionary, + AdditionalHeaderClaims = new Dictionary () { { JwtHeaderParameterNames.Typ, "TEST" } } + }, + JwtToken = ReferenceTokens.JWSWithDifferentTyp + }, new CreateTokenTheoryData { First = true, @@ -1262,17 +1305,6 @@ public static TheoryData CreateJWSWithAdditionalHeaderCla JwtToken = ReferenceTokens.JWSWithSingleAdditionalHeaderClaim }, new CreateTokenTheoryData - { - TestId = "DifferentTypHeaderValue", - TokenDescriptor = new SecurityTokenDescriptor - { - SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, - Claims = Default.PayloadDictionary, - AdditionalHeaderClaims = new Dictionary () { { JwtHeaderParameterNames.Typ, "TEST" } } - }, - JwtToken = ReferenceTokens.JWSWithDifferentTyp - }, - new CreateTokenTheoryData { TestId = "EmptyAdditionalHeaderClaims", TokenDescriptor = new SecurityTokenDescriptor @@ -1493,8 +1525,9 @@ public void CreateJWEWithAdditionalHeaderClaims(CreateTokenTheoryData theoryData context.PropertiesToIgnoreWhenComparing = new Dictionary> { - { typeof(JsonWebToken), new List { "EncodedToken", "AuthenticationTag", "Ciphertext", "InitializationVector", "EncryptedKey" } }, + { typeof(JsonWebToken), new List { "EncodedHeader", "EncodedToken", "AuthenticationTag", "Ciphertext", "InitializationVector", "EncryptedKey" } }, }; + IdentityComparer.AreEqual(validatedJwtTokenFromDescriptor, jwtTokenToCompare, context); foreach (var key in theoryData.TokenDescriptor.AdditionalHeaderClaims.Keys) @@ -1669,24 +1702,29 @@ public void CreateJWSUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa theoryData.ValidationParameters.ValidateLifetime = false; try { + JsonWebTokenHandler6x jsonWebTokenHandler6x = new JsonWebTokenHandler6x(); + + string jwtFromSecurityTokenDescriptor6x = jwtFromSecurityTokenDescriptor6x = jsonWebTokenHandler6x.CreateToken(theoryData.TokenDescriptor6x ?? theoryData.TokenDescriptor); string jwtFromSecurityTokenDescriptor = theoryData.JsonWebTokenHandler.CreateToken(theoryData.TokenDescriptor); - string jwtFromString; + string jwtPayloadAsString; + if (theoryData.TokenDescriptor.SigningCredentials == null) - jwtFromString = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload); + jwtPayloadAsString = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload); else if (theoryData.TokenDescriptor.AdditionalHeaderClaims != null) - jwtFromString = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload, theoryData.TokenDescriptor.SigningCredentials, theoryData.TokenDescriptor.AdditionalHeaderClaims); + jwtPayloadAsString = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload, theoryData.TokenDescriptor.SigningCredentials, theoryData.TokenDescriptor.AdditionalHeaderClaims); else - jwtFromString = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload, theoryData.TokenDescriptor.SigningCredentials); + jwtPayloadAsString = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload, theoryData.TokenDescriptor.SigningCredentials); + + var jwsTokenFromSecurityTokenDescriptor = new JsonWebToken(jwtFromSecurityTokenDescriptor); + var jwsTokenFromSecurityTokenDescriptor6x = new JsonWebToken(jwtFromSecurityTokenDescriptor6x); + var jwsTokenFromString = new JsonWebToken(jwtPayloadAsString); var tokenValidationResultFromSecurityTokenDescriptor = theoryData.JsonWebTokenHandler.ValidateToken(jwtFromSecurityTokenDescriptor, theoryData.ValidationParameters); - var tokenValidationResultFromString = theoryData.JsonWebTokenHandler.ValidateToken(jwtFromString, theoryData.ValidationParameters); + var tokenValidationResultFromString = theoryData.JsonWebTokenHandler.ValidateToken(jwtPayloadAsString, theoryData.ValidationParameters); IdentityComparer.AreEqual(tokenValidationResultFromSecurityTokenDescriptor.IsValid, theoryData.IsValid, context); IdentityComparer.AreEqual(tokenValidationResultFromString.IsValid, theoryData.IsValid, context); - var jwsTokenFromSecurityTokenDescriptor = new JsonWebToken(jwtFromSecurityTokenDescriptor); - var jwsTokenFromString = new JsonWebToken(jwtFromString); - // If the signing key used was an x509SecurityKey, make sure that the 'X5t' property was set properly and // that the values of 'X5t' and 'Kid' on the JsonWebToken are equal to each other. if (theoryData.TokenDescriptor.SigningCredentials?.Key is X509SecurityKey x509SecurityKey) @@ -1698,7 +1736,72 @@ public void CreateJWSUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa } context.PropertiesToIgnoreWhenComparing = theoryData.PropertiesToIgnoreWhenComparing; - IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor, jwsTokenFromString, context); + + if (!IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor.Header, jwsTokenFromSecurityTokenDescriptor6x.Header, context)) + { + context.AddDiff("jwsTokenFromSecurityTokenDescriptor.Header != jwsTokenFromSecurityTokenDescriptor6x.Header"); + context.AddDiff("******************************************************************************************"); + context.AddDiff(" "); + } + + bool claimsEqual; + if (!IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor.Claims, jwsTokenFromSecurityTokenDescriptor6x.Claims, context)) + { + context.AddDiff("jwsTokenFromSecurityTokenDescriptor.Claims != jwsTokenFromSecurityTokenDescriptor6x.Claims"); + context.AddDiff("****************************************************************************"); + context.AddDiff(" "); + claimsEqual = false; + } + else + { + claimsEqual = true; + } + + if (!IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor.Header, jwsTokenFromSecurityTokenDescriptor6x.Header)) + { + context.AddDiff("jwsTokenFromSecurityTokenDescriptor.Header != jwsTokenFromSecurityTokenDescriptor6x.Header"); + context.AddDiff("****************************************************************************"); + context.AddDiff(" "); + } + + // if the claims are the same some properties could be different because of ordering + CompareContext localContext = new CompareContext(context); + if (claimsEqual) + localContext.PropertiesToIgnoreWhenComparing = new Dictionary> + { + {typeof(JsonWebToken), new List {"EncodedHeader", "EncodedToken", "EncodedPayload", "EncodedSignature"}}, + }; + + if (!IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor, jwsTokenFromSecurityTokenDescriptor6x, localContext)) + { + context.AddDiff("jwsTokenFromSecurityTokenDescriptor != jwsTokenFromSecurityTokenDescriptor6x"); + context.AddDiff("****************************************************************************"); + context.AddDiff(" "); + } + + context.Merge(localContext); + + if (!IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor.Claims, jwsTokenFromString.Claims, context)) + { + context.AddDiff("jwsTokenFromSecurityTokenDescriptor.Claims != jwsTokenFromString.Claims"); + context.AddDiff("****************************************************************************"); + context.AddDiff(" "); + claimsEqual = false; + } + else + { + claimsEqual = true; + } + + localContext.Diffs.Clear(); + // if the claims are the same some properties could be different because of ordering + if (!IdentityComparer.AreEqual(jwsTokenFromSecurityTokenDescriptor, jwsTokenFromString, localContext)) + { + context.AddDiff("jwsTokenFromSecurityTokenDescriptor != jwsTokenFromString"); + context.AddDiff("****************************************************************************"); + context.AddDiff(" "); + } + theoryData.ExpectedException.ProcessNoException(context); } catch (Exception ex) @@ -1715,10 +1818,85 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr { return new TheoryData { - new CreateTokenTheoryData + // Test checks that the values in SecurityTokenDescriptor.Subject.Claims + // are properly combined with those specified in SecurityTokenDescriptor.Claims. + // Duplicate values (if present with different case) should not be overridden. + // For example, the 'aud' claim on TokenDescriptor.Claims will not be overridden + // by the 'AUD' claim on TokenDescriptor.Subject.Claims, but the 'exp' claim will. + new CreateTokenTheoryData("TokenDescriptorWithBothSubjectAndClaims") + { + Payload = new JObject() + { + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Default.Issuer }, + { JwtRegisteredClaimNames.Aud.ToUpper(), JArray.FromObject(new List() {"Audience1", "Audience2"}) }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString() }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString()}, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString() }, + { JwtRegisteredClaimNames.Aud, JArray.FromObject(Default.Audiences) }, + }.ToString(Formatting.None), + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + Claims = new Dictionary() + { + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Default.Issuer }, + { JwtRegisteredClaimNames.Aud, JsonSerializerPrimitives.CreateJsonElement(Default.Audiences) }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString() }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString()}, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString() }, + }, + Subject = new ClaimsIdentity(new List() + { + new Claim(JwtRegisteredClaimNames.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.GivenName, "Bob", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Iss, "Issuer", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Aud.ToUpper(), "Audience1", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Aud.ToUpper(), "Audience2", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), + }, "AuthenticationTypes.Federation") + }, + TokenDescriptor6x = new SecurityTokenDescriptor + { + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + Claims = new Dictionary() + { + { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, + { JwtRegisteredClaimNames.GivenName, "Bob" }, + { JwtRegisteredClaimNames.Iss, Default.Issuer }, + { JwtRegisteredClaimNames.Aud, JArray.FromObject(Default.Audiences) }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString() }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString()}, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString() }, + }, + Subject = new ClaimsIdentity(new List() + { + new Claim(JwtRegisteredClaimNames.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.GivenName, "Bob", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Iss, "Issuer", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Aud.ToUpper(), "Audience1", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Aud.ToUpper(), "Audience2", ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), + new Claim(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), + }, "AuthenticationTypes.Federation") + }, + + JsonWebTokenHandler = new JsonWebTokenHandler(), + ValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, + ValidAudience = Default.Audiences.First(), + ValidIssuer = Default.Issuer, + } + }, + new CreateTokenTheoryData("ValidUsingClaims") { - First = true, - TestId = "ValidUsingClaims", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1733,9 +1911,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidIssuer = Default.Issuer } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("ValidUsingSubject") { - TestId = "ValidUsingSubject", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1750,9 +1927,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidIssuer = Default.Issuer } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("ValidUsingClaimsAndX509SecurityKey") { - TestId = "ValidUsingClaimsAndX509SecurityKey", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1767,9 +1943,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidIssuer = Default.Issuer } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("TokenDescriptorNull") { - TestId = "TokenDescriptorNull", Payload = Default.PayloadString, TokenDescriptor = null, JsonWebTokenHandler = new JsonWebTokenHandler(), @@ -1781,9 +1956,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr }, ExpectedException = ExpectedException.ArgumentNullException("IDX10000:") }, - new CreateTokenTheoryData + new CreateTokenTheoryData("TokenDescriptorClaimsNull") { - TestId = "TokenDescriptorClaimsNull", Payload = new JObject() { { JwtRegisteredClaimNames.Aud, Default.Audience }, @@ -1808,9 +1982,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidateIssuer = false } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("TokenDescriptorClaimsEmpty") { - TestId = "TokenDescriptorClaimsEmpty", Payload = new JObject() { { JwtRegisteredClaimNames.Aud, Default.Audience }, @@ -1835,9 +2008,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidateIssuer = false } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("TokenDescriptorSigningCredentialsNullRequireSignedTokensFalse") { - TestId = "TokenDescriptorSigningCredentialsNullRequireSignedTokensFalse", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1853,9 +2025,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr RequireSignedTokens = false }, }, - new CreateTokenTheoryData + new CreateTokenTheoryData("TokenDescriptorSigningCredentialsNullRequireSignedTokensTrue") { - TestId = "TokenDescriptorSigningCredentialsNullRequireSignedTokensTrue", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1871,10 +2042,10 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr }, IsValid = false }, - new CreateTokenTheoryData // Test checks that values in SecurityTokenDescriptor.Payload + new CreateTokenTheoryData("UseSecurityTokenDescriptorProperties") + // Test checks that values in SecurityTokenDescriptor.Payload // are properly replaced with the properties that are explicitly specified on the SecurityTokenDescriptor. { - TestId = "UseSecurityTokenDescriptorProperties", Payload = new JObject() { { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, @@ -1901,63 +2072,10 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, ValidAudience = "Audience", ValidIssuer = "Issuer" - } - }, - // Test checks that the values in SecurityTokenDescriptor.Subject.Claims - // are properly combined with those specified in SecurityTokenDescriptor.Claims. - // Duplicate values (if present with different case) should not be overridden. - // For example, the 'aud' claim on TokenDescriptor.Claims will not be overridden - // by the 'AUD' claim on TokenDescriptor.Subject.Claims, but the 'exp' claim will. - new CreateTokenTheoryData - { - TestId = "TokenDescriptorWithBothSubjectAndClaims", - Payload = new JObject() - { - { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, - { JwtRegisteredClaimNames.GivenName, "Bob" }, - { JwtRegisteredClaimNames.Iss, Default.Issuer }, - { JwtRegisteredClaimNames.Aud.ToUpper(), JArray.FromObject(new List() {"Audience1", "Audience2"}) }, - { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString() }, - { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString()}, - { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString() }, - { JwtRegisteredClaimNames.Aud, JArray.FromObject(Default.Audiences) }, - }.ToString(Formatting.None), - TokenDescriptor = new SecurityTokenDescriptor - { - SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, - Claims = new Dictionary() - { - { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, - { JwtRegisteredClaimNames.GivenName, "Bob" }, - { JwtRegisteredClaimNames.Iss, Default.Issuer }, - { JwtRegisteredClaimNames.Aud, JArray.FromObject(Default.Audiences) }, - { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString() }, - { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString()}, - { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString() }, - }, - Subject = new ClaimsIdentity(new List() - { - new Claim(JwtRegisteredClaimNames.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.GivenName, "Bob", ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.Iss, "Issuer", ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.Aud.ToUpper(), "Audience1", ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.Aud.ToUpper(), "Audience2", ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), - new Claim(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString(), ClaimValueTypes.String, Default.Issuer, Default.Issuer), - }, "AuthenticationTypes.Federation") }, - JsonWebTokenHandler = new JsonWebTokenHandler(), - ValidationParameters = new TokenValidationParameters - { - IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, - ValidAudience = Default.Audiences.First(), - ValidIssuer = Default.Issuer, - } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("SingleAdditionalHeaderClaim") { - TestId = "SingleAdditionalHeaderClaim", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1973,9 +2091,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidIssuer = Default.Issuer } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("MultipleAdditionalHeaderClaims") { - TestId = "MultipleAdditionalHeaderClaims", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -1991,9 +2108,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidIssuer = Default.Issuer } }, - new CreateTokenTheoryData + new CreateTokenTheoryData("DuplicateAdditionalHeaderClaim") { - TestId = "DuplicateAdditionalHeaderClaim", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { @@ -2010,14 +2126,12 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr }, ExpectedException = ExpectedException.SecurityTokenException("IDX14116:") }, - new CreateTokenTheoryData + new CreateTokenTheoryData("DuplicateAdditionalHeaderClaimDifferentCase") { - TestId = "DuplicateAdditionalHeaderClaimDifferentCase", Payload = Default.PayloadString, TokenDescriptor = new SecurityTokenDescriptor { SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, - Claims = Default.PayloadDictionary, AdditionalHeaderClaims = new Dictionary () { { JwtHeaderParameterNames.Alg.ToUpper(), "alg" } } }, @@ -2030,11 +2144,8 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr }, ExpectedException = ExpectedException.SecurityTokenException("IDX14116:") }, - #if NET461 || NET462 || NET472 || NET_CORE - // RsaPss is not supported on .NET < 4.6 - new CreateTokenTheoryData + new CreateTokenTheoryData("RsaPss") { - TestId = "RsaPss", Payload = Default.PayloadString, //RsaPss produces different signatures PropertiesToIgnoreWhenComparing = new Dictionary> @@ -2055,8 +2166,7 @@ public static TheoryData CreateJWSUsingSecurityTokenDescr ValidateIssuer = false, } } -#endif - }; + }; } } @@ -2114,9 +2224,30 @@ public void CreateJWSWithDuplicateClaimsRoundTrip() var jsonWebTokenFromDictionary = new JsonWebToken(jwtFromDictionary); var jsonWebTokenFromSubject = new JsonWebToken(jwtFromSubject); - IdentityComparer.AreEqual(payloadClaimsIdentity.Claims, jsonWebTokenFromPayload.Claims, context); - IdentityComparer.AreEqual(jsonWebTokenFromPayload, jsonWebTokenFromDictionary, context); - IdentityComparer.AreEqual(jsonWebTokenFromPayload, jsonWebTokenFromSubject, context); + if (!IdentityComparer.AreEqual(payloadClaimsIdentity.Claims, jsonWebTokenFromPayload.Claims, context)) + { + context.AddDiff("payloadClaimsIdentity.Claims != jsonWebTokenFromPayload.Claims"); + context.AddDiff("**************************************************************"); + } + + if (!IdentityComparer.AreEqual(payloadClaimsIdentity.Claims, jsonWebTokenFromDictionary.Claims, context)) + { + context.AddDiff("payloadClaimsIdentity.Claims != jsonWebTokenFromDictionary.Claims"); + context.AddDiff("**************************************************************"); + } + + context.PropertiesToIgnoreWhenComparing = new Dictionary> { { typeof(JsonWebToken), new List { "EncodedPayload", "EncodedToken" } } }; + if (!IdentityComparer.AreEqual(jsonWebTokenFromPayload, jsonWebTokenFromDictionary, context)) + { + context.AddDiff("jsonWebTokenFromPayload != jsonWebTokenFromDictionary"); + context.AddDiff("*****************************************************"); + } + + if (!IdentityComparer.AreEqual(jsonWebTokenFromPayload, jsonWebTokenFromSubject, context)) + { + context.AddDiff("jsonWebTokenFromPayload != jsonWebTokenFromSubject"); + context.AddDiff("**************************************************"); + } TestUtilities.AssertFailIfErrors(context); } @@ -2469,13 +2600,16 @@ public static TheoryData RoundTripJWEKeyWrapTestCases // Test checks to make sure that default times are correctly added to the token // upon token creation. - [Fact] + [Fact (Skip = "Rewrite test to use claims, string will not succeed")] public void SetDefaultTimesOnTokenCreation() { + // when the payload is passed as a string to JsonWebTokenHandler.CreateToken, we no longer + // crack the string and add times {exp, iat, nbf} TestUtilities.WriteHeader($"{this}.SetDefaultTimesOnTokenCreation"); var context = new CompareContext(); - var tokenHandler = new JsonWebTokenHandler(); + var tokenHandler7 = new JsonWebTokenHandler(); + var tokenHandler6 = new JsonWebTokenHandler6x(); var payloadWithoutTimeValues = new JObject() { { JwtRegisteredClaimNames.Email, "Bob@contoso.com" }, @@ -2484,15 +2618,24 @@ public void SetDefaultTimesOnTokenCreation() { JwtRegisteredClaimNames.Aud, Default.Audience }, }.ToString(Formatting.None); - var jwtString = tokenHandler.CreateToken(payloadWithoutTimeValues, KeyingMaterial.JsonWebKeyRsa256SigningCredentials); - var jwt = new JsonWebToken(jwtString); + var jwtString7 = tokenHandler7.CreateToken(payloadWithoutTimeValues, KeyingMaterial.JsonWebKeyRsa256SigningCredentials); + var jwt7 = new JsonWebToken(jwtString7); + + var jwtString6 = tokenHandler6.CreateToken(payloadWithoutTimeValues, KeyingMaterial.JsonWebKeyRsa256SigningCredentials); + var jwt6 = new JsonWebToken(jwtString6); + + if (!IdentityComparer.AreEqual(jwt7, jwt6, context)) + { + context.AddDiff("jwt7 != jwt6"); + context.AddDiff("********************************************"); + } // DateTime.MinValue is returned if the value of a DateTime claim is not found in the payload - if (DateTime.MinValue.Equals(jwt.IssuedAt)) + if (DateTime.MinValue.Equals(jwt7.IssuedAt)) context.AddDiff("DateTime.MinValue.Equals(jwt.IssuedAt). Value for the 'iat' claim not found in the payload."); - if (DateTime.MinValue.Equals(jwt.ValidFrom)) + if (DateTime.MinValue.Equals(jwt7.ValidFrom)) context.AddDiff("DateTime.MinValue.Equals(jwt.ValidFrom). Value for the 'nbf' claim not found in the payload."); - if (DateTime.MinValue.Equals(jwt.ValidTo)) + if (DateTime.MinValue.Equals(jwt7.ValidTo)) context.AddDiff("DateTime.MinValue.Equals(jwt.ValidTo). Value for the 'exp' claim not found in the payload."); TestUtilities.AssertFailIfErrors(context); @@ -3717,6 +3860,8 @@ public CreateTokenTheoryData(string testId) public SecurityTokenDescriptor TokenDescriptor { get; set; } + public SecurityTokenDescriptor TokenDescriptor6x { get; set; } + public JsonWebTokenHandler JsonWebTokenHandler { get; set; } public JwtSecurityTokenHandler JwtSecurityTokenHandler { get; set; } @@ -3788,5 +3933,3 @@ protected override ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, To } } } - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 7035ade29a..d0a3d1c0e5 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -13,26 +13,26 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; -using JsonReaderException = System.Text.Json.JsonException; -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant +using JsonReaderException = System.Text.Json.JsonException; namespace Microsoft.IdentityModel.JsonWebTokens.Tests { public class JsonWebTokenTests { private static DateTime dateTime = new DateTime(2000, 01, 01, 0, 0, 0); - private string jsonString = $@"{{""intarray"":[1,2,3], ""array"":[1,""2"",3], ""jobject"": {{""string1"":""string1value"",""string2"":""string2value""}},""string"":""bob"", ""float"":42.0, ""integer"":42, ""nill"": null, ""bool"" : true, ""dateTime"": ""{dateTime}"", ""dateTimeIso8061"": ""{dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)}"" }}"; + private static DateTime dateTimeUtc = new DateTime(2000, 01, 01, 0, 0, 0).ToUniversalTime(); + private string jsonString = $@"{{""intarray"":[1,2,3], ""array"":[1,""2"",3], ""jobject"": {{""string1"":""string1value"",""string2"":""string2value""}},""string"":""bob"", ""float"":42, ""integer"":42, ""nill"": null, ""bool"" : true, ""dateTime"": ""{dateTime}"", ""dateTimeIso8061"": ""{dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)}"" }}"; private List payloadClaims = new List() { - new Claim("intarray", @"[1,2,3]", JsonClaimValueTypes.JsonArray, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), - new Claim("array", @"[1,""2"",3]", JsonClaimValueTypes.JsonArray, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), + new Claim("intarray", @"1", "http://www.w3.org/2001/XMLSchema#integer32", "LOCAL AUTHORITY", "LOCAL AUTHORITY"), + new Claim("array", @"1", "http://www.w3.org/2001/XMLSchema#integer32", "LOCAL AUTHORITY", "LOCAL AUTHORITY"), new Claim("jobject", @"{""string1"":""string1value"",""string2"":""string2value""}", JsonClaimValueTypes.Json, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), new Claim("string", "bob", ClaimValueTypes.String, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), - new Claim("float", "42.0", ClaimValueTypes.Double, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), - new Claim("integer", "42", ClaimValueTypes.Integer, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), + new Claim("float", "42", ClaimValueTypes.Double, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), + new Claim("integer", "42", ClaimValueTypes.Integer32, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), new Claim("nill", "", JsonClaimValueTypes.JsonNull, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), - new Claim("bool", "true", ClaimValueTypes.Boolean, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), + new Claim("bool", "True", ClaimValueTypes.Boolean, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), new Claim("dateTime", dateTime.ToString(), ClaimValueTypes.String, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), new Claim("dateTimeIso8061", dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), }; @@ -262,9 +262,6 @@ public void GetPayloadValues() var token = new JsonWebToken("{}", jsonString); -// private string jsonString = $@"{""array"":[1,""2"",3], ""jobject"": {{""string1"":""string1value"",""string2"":""string2value""}},""string"":""bob"", ""float"":42.0, ""integer"":42, ""nill"": null, ""bool"" : true, ""dateTime"": ""{dateTime}"", ""dateTimeIso8061"": ""{dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)}"" }}"; - - try // Try to retrieve a value that doesn't exist in the header. { token.GetPayloadValue("doesnotexist"); @@ -280,7 +277,7 @@ public void GetPayloadValues() } catch (Exception ex) { - ExpectedException.ArgumentException("IDX14305:", typeof(System.Text.Json.JsonException)).ProcessException(ex, context); + ExpectedException.ArgumentException("IDX14305:").ProcessException(ex, context); } TestUtilities.AssertFailIfErrors(context); @@ -328,7 +325,7 @@ public void TryGetPayloadValues() IdentityComparer.AreEqual(true, success, context); var dateTimeIso8061Value = token.GetPayloadValue("dateTimeIso8061"); - IdentityComparer.AreEqual(dateTimeIso8061Value, dateTime.ToUniversalTime(), context); + IdentityComparer.AreEqual(dateTimeIso8061Value, dateTimeUtc, context); IdentityComparer.AreEqual(true, success, context); success = token.TryGetPayloadValue("doesnotexist", out int doesNotExist); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs new file mode 100644 index 0000000000..4694abddc7 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/json/JsonWebTokenHandler.cs @@ -0,0 +1,1928 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + /// + /// A designed for creating and validating Json Web Tokens. + /// See: https://datatracker.ietf.org/doc/html/rfc7519 and http://www.rfc-editor.org/info/rfc7515. + /// + public class JsonWebTokenHandler6x : 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: + /// { , }. + /// + /// The Base64Url encoded string representation of the unsigned JWT header. + public const string Base64UrlEncodedUnsignedJWSHeader = "eyJhbGciOiJub25lIn0"; + + /// + /// Initializes a new instance of the class. + /// + public JsonWebTokenHandler6x() + { + if (_mapInboundClaims) + _inboundClaimTypeMap = new Dictionary(DefaultInboundClaimTypeMap); + else + _inboundClaimTypeMap = new Dictionary(); + } + + /// + /// Gets the type of the . + /// + /// The type of + 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) + return additionalClaims; + + if (additionalClaims == null) + additionalClaims = new Dictionary { { JwtHeaderParameterNames.Cty, JwtConstants.HeaderType } }; + else if (!additionalClaims.TryGetValue(JwtHeaderParameterNames.Cty, out _)) + additionalClaims.Add(JwtHeaderParameterNames.Cty, JwtConstants.HeaderType); + + return additionalClaims; + } + + /// + /// Determines if the string is a well formed Json Web Token (JWT). + /// See: https://datatracker.ietf.org/doc/html/rfc7519 + /// + /// String that should represent a valid JWT. + /// Uses matching: + /// JWS: @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$" + /// JWE: (dir): @"^[A-Za-z0-9-_]+\.\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$" + /// JWE: (wrappedkey): @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]$" + /// + /// + /// 'false' if the token is null or whitespace. + /// 'false' if token.Length is greater than . + /// 'true' if the token is in JSON compact serialization format. + /// + public virtual bool CanReadToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + return false; + + if (token.Length > MaximumTokenSizeInBytes) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)); + + return false; + } + + // Count the number of segments, which is the number of periods + 1. We can stop when we've encountered + // more segments than the maximum we know how to handle. + int pos = 0; + int segmentCount = 1; + while (segmentCount <= JwtConstants.MaxJwtSegmentCount && ((pos = token.IndexOf('.', pos)) >= 0)) + { + pos++; + segmentCount++; + } + + switch (segmentCount) + { + case JwtConstants.JwsSegmentCount: + return JwtTokenUtilities.RegexJws.IsMatch(token); + + case JwtConstants.JweSegmentCount: + return JwtTokenUtilities.RegexJwe.IsMatch(token); + + default: + LogHelper.LogInformation(LogMessages.IDX14107); + return false; + } + } + + /// + /// Returns a value that indicates if this handler can validate a . + /// + /// 'true', indicating this instance can validate a . + public virtual bool CanValidateToken + { + get { return true; } + } + + private static JObject CreateDefaultJWEHeader(EncryptingCredentials encryptingCredentials, string compressionAlgorithm, string tokenType) + { + var header = new JObject(); + header.Add(JwtHeaderParameterNames.Alg, encryptingCredentials.Alg); + header.Add(JwtHeaderParameterNames.Enc, encryptingCredentials.Enc); + + if (!string.IsNullOrEmpty(encryptingCredentials.Key.KeyId)) + header.Add(JwtHeaderParameterNames.Kid, encryptingCredentials.Key.KeyId); + + if (!string.IsNullOrEmpty(compressionAlgorithm)) + header.Add(JwtHeaderParameterNames.Zip, compressionAlgorithm); + + if (string.IsNullOrEmpty(tokenType)) + header.Add(JwtHeaderParameterNames.Typ, JwtConstants.HeaderType); + else + header.Add(JwtHeaderParameterNames.Typ, tokenType); + + return header; + } + + private static JObject CreateDefaultJWSHeader(SigningCredentials signingCredentials, string tokenType) + { + JObject header = null; + + if (signingCredentials == null) + { + header = new JObject() + { + {JwtHeaderParameterNames.Alg, SecurityAlgorithms.None } + }; + } + else + { + header = new JObject() + { + { JwtHeaderParameterNames.Alg, signingCredentials.Algorithm } + }; + + if (signingCredentials.Key.KeyId != null) + header.Add(JwtHeaderParameterNames.Kid, signingCredentials.Key.KeyId); + + if (signingCredentials.Key is X509SecurityKey x509SecurityKey) + header[JwtHeaderParameterNames.X5t] = x509SecurityKey.X5t; + } + + if (string.IsNullOrEmpty(tokenType)) + header.Add(JwtHeaderParameterNames.Typ, JwtConstants.HeaderType); + else + header.Add(JwtHeaderParameterNames.Typ, tokenType); + + return header; + } + + /// + /// Creates an unsigned JWS (Json Web Signature). + /// + /// A string containing JSON which represents the JWT token payload. + /// if is null. + /// A JWS in Compact Serialization Format. + public virtual string CreateToken(string payload) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + return CreateTokenPrivate(payload, null, null, null, null, null, null); + } + + /// + /// Creates an unsigned JWS (Json Web Signature). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the dictionary containing any custom header claims that need to be added to the JWT token header. + /// if is null. + /// if is null. + /// A JWS in Compact Serialization Format. + public virtual string CreateToken(string payload, IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return CreateTokenPrivate(payload, null, null, null, additionalHeaderClaims, null, null); + } + + /// + /// Creates a JWS (Json Web Signature). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWS. + /// if is null. + /// if is null. + /// A JWS in Compact Serialization Format. + public virtual string CreateToken(string payload, SigningCredentials signingCredentials) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + return CreateTokenPrivate(payload, signingCredentials, null, null, null, null, null); + } + + /// + /// Creates a JWS (Json Web Signature). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWS. + /// Defines the dictionary containing any custom header claims that need to be added to the JWT token header. + /// if is null. + /// if is null. + /// if is null. + /// if , + /// , , and/or + /// are present inside of . + /// A JWS in Compact Serialization Format. + public virtual string CreateToken(string payload, SigningCredentials signingCredentials, IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return CreateTokenPrivate(payload, signingCredentials, null, null, additionalHeaderClaims, null, null); + } + + /// + /// Creates a JWS(Json Web Signature). + /// + /// A that contains details of contents of the token. + /// A JWS in Compact Serialization Format. + public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) + { + if (tokenDescriptor == null) + throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor)); + + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + { + if ((tokenDescriptor.Subject == null || !tokenDescriptor.Subject.Claims.Any()) + && (tokenDescriptor.Claims == null || !tokenDescriptor.Claims.Any())) + LogHelper.LogWarning(LogMessages.IDX14114, LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor)), LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor.Subject)), LogHelper.MarkAsNonPII(nameof(SecurityTokenDescriptor.Claims))); + } + + JObject payload; + if (tokenDescriptor.Subject != null) + payload = JObject.FromObject(TokenUtilities.CreateDictionaryFromClaims(tokenDescriptor.Subject.Claims)); + else + payload = new JObject(); + + // If a key is present in both tokenDescriptor.Subject.Claims and tokenDescriptor.Claims, the value present in tokenDescriptor.Claims is the + // one that takes precedence and will remain after the merge. Key comparison is case sensitive. + if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) + payload.Merge(JObject.FromObject(tokenDescriptor.Claims), new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); + + if (tokenDescriptor.Audience != null) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Aud)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Audience)))); + + payload[JwtRegisteredClaimNames.Aud] = tokenDescriptor.Audience; + } + + if (tokenDescriptor.Expires.HasValue) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Exp)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); + + payload[JwtRegisteredClaimNames.Exp] = EpochTime.GetIntDate(tokenDescriptor.Expires.Value); + } + + if (tokenDescriptor.Issuer != null) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Iss)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Issuer)))); + + payload[JwtRegisteredClaimNames.Iss] = tokenDescriptor.Issuer; + } + + if (tokenDescriptor.IssuedAt.HasValue) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Iat)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.IssuedAt)))); + + payload[JwtRegisteredClaimNames.Iat] = EpochTime.GetIntDate(tokenDescriptor.IssuedAt.Value); + } + + if (tokenDescriptor.NotBefore.HasValue) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational) && payload.ContainsKey(JwtRegisteredClaimNames.Nbf)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.NotBefore)))); + + payload[JwtRegisteredClaimNames.Nbf] = EpochTime.GetIntDate(tokenDescriptor.NotBefore.Value); + } + + return CreateTokenPrivate( + payload.ToString(Formatting.None), + tokenDescriptor.SigningCredentials, + tokenDescriptor.EncryptingCredentials, + tokenDescriptor.CompressionAlgorithm, + tokenDescriptor.AdditionalHeaderClaims, + tokenDescriptor.AdditionalInnerHeaderClaims, + tokenDescriptor.TokenType); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// A JWE in compact serialization format. + public virtual string CreateToken(string payload, EncryptingCredentials encryptingCredentials) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + return CreateTokenPrivate(payload, null, encryptingCredentials, null, null, null, null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// Defines the dictionary containing any custom header claims that need to be added to the outer JWT token header. + /// if is null. + /// if is null. + /// if is null. + /// if , + /// , , and/or + /// are present inside of . + /// A JWS in Compact Serialization Format. + public virtual string CreateToken(string payload, EncryptingCredentials encryptingCredentials, IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return CreateTokenPrivate(payload, null, encryptingCredentials, null, additionalHeaderClaims, null, null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWT. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// if is null. + /// if is null. + /// if is null. + /// A JWE in compact serialization format. + public virtual string CreateToken(string payload, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, null, null, null, null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWT. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// Defines the dictionary containing any custom header claims that need to be added to the outer JWT token header. + /// if is null. + /// if is null. + /// if is null. + /// if is null. + /// if , + /// , , and/or + /// are present inside of . + /// A JWE in compact serialization format. + public virtual string CreateToken( + string payload, + SigningCredentials signingCredentials, + EncryptingCredentials encryptingCredentials, + IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, null, additionalHeaderClaims, null, null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// Defines the compression algorithm that will be used to compress the JWT token payload. + /// A JWE in compact serialization format. + public virtual string CreateToken(string payload, EncryptingCredentials encryptingCredentials, string compressionAlgorithm) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (string.IsNullOrEmpty(compressionAlgorithm)) + throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); + + return CreateTokenPrivate(payload, null, encryptingCredentials, compressionAlgorithm, null, null, null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWT. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// Defines the compression algorithm that will be used to compress the JWT token payload. + /// if is null. + /// if is null. + /// if is null. + /// if is null. + /// A JWE in compact serialization format. + public virtual string CreateToken(string payload, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, string compressionAlgorithm) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (string.IsNullOrEmpty(compressionAlgorithm)) + throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); + + return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, compressionAlgorithm, null, null, null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWT. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// Defines the compression algorithm that will be used to compress the JWT token payload. + /// Defines the dictionary containing any custom header claims that need to be added to the outer JWT token header. + /// Defines the dictionary containing any custom header claims that need to be added to the inner JWT token header. + /// if is null. + /// if is null. + /// if is null. + /// if is null. + /// if is null. + /// if , + /// , , and/or + /// are present inside of . + /// A JWE in compact serialization format. + public virtual string CreateToken( + string payload, + SigningCredentials signingCredentials, + EncryptingCredentials encryptingCredentials, + string compressionAlgorithm, + IDictionary additionalHeaderClaims, + IDictionary additionalInnerHeaderClaims) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (string.IsNullOrEmpty(compressionAlgorithm)) + throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + if (additionalInnerHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalInnerHeaderClaims)); + + return CreateTokenPrivate( + payload, + signingCredentials, + encryptingCredentials, + compressionAlgorithm, + additionalHeaderClaims, + additionalInnerHeaderClaims, + null); + } + + /// + /// Creates a JWE (Json Web Encryption). + /// + /// A string containing JSON which represents the JWT token payload. + /// Defines the security key and algorithm that will be used to sign the JWT. + /// Defines the security key and algorithm that will be used to encrypt the JWT. + /// Defines the compression algorithm that will be used to compress the JWT token payload. + /// Defines the dictionary containing any custom header claims that need to be added to the outer JWT token header. + /// if is null. + /// if is null. + /// if is null. + /// if is null. + /// if is null. + /// if , + /// , , and/or + /// are present inside of . + /// A JWE in compact serialization format. + public virtual string CreateToken( + string payload, + SigningCredentials signingCredentials, + EncryptingCredentials encryptingCredentials, + string compressionAlgorithm, + IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(payload)) + throw LogHelper.LogArgumentNullException(nameof(payload)); + + if (signingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(signingCredentials)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (string.IsNullOrEmpty(compressionAlgorithm)) + throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return CreateTokenPrivate(payload, signingCredentials, encryptingCredentials, compressionAlgorithm, additionalHeaderClaims, null, null); + } + + private string CreateTokenPrivate( + string payload, + SigningCredentials signingCredentials, + EncryptingCredentials encryptingCredentials, + string compressionAlgorithm, + IDictionary additionalHeaderClaims, + IDictionary additionalInnerHeaderClaims, + string tokenType) + { + if (additionalHeaderClaims?.Count > 0 && additionalHeaderClaims.Keys.Intersect(JwtTokenUtilities.DefaultHeaderParameters, StringComparer.OrdinalIgnoreCase).Any()) + throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(LogMessages.IDX14116, LogHelper.MarkAsNonPII(nameof(additionalHeaderClaims)), LogHelper.MarkAsNonPII(string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters))))); + + if (additionalInnerHeaderClaims?.Count > 0 && additionalInnerHeaderClaims.Keys.Intersect(JwtTokenUtilities.DefaultHeaderParameters, StringComparer.OrdinalIgnoreCase).Any()) + throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(LogMessages.IDX14116, nameof(additionalInnerHeaderClaims), string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters)))); + + var header = CreateDefaultJWSHeader(signingCredentials, tokenType); + + if (encryptingCredentials == null && additionalHeaderClaims != null && additionalHeaderClaims.Count > 0) + header.Merge(JObject.FromObject(additionalHeaderClaims)); + + if (additionalInnerHeaderClaims != null && additionalInnerHeaderClaims.Count > 0) + header.Merge(JObject.FromObject(additionalInnerHeaderClaims)); + + var rawHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(header.ToString(Formatting.None))); + JObject jsonPayload = null; + try + { + if (SetDefaultTimesOnTokenCreation) + { + jsonPayload = JObject.Parse(payload); + if (jsonPayload != null) + { + var now = EpochTime.GetIntDate(DateTime.UtcNow); + if (!jsonPayload.TryGetValue(JwtRegisteredClaimNames.Exp, out _)) + jsonPayload.Add(JwtRegisteredClaimNames.Exp, now + TokenLifetimeInMinutes * 60); + + if (!jsonPayload.TryGetValue(JwtRegisteredClaimNames.Iat, out _)) + jsonPayload.Add(JwtRegisteredClaimNames.Iat, now); + + if (!jsonPayload.TryGetValue(JwtRegisteredClaimNames.Nbf, out _)) + jsonPayload.Add(JwtRegisteredClaimNames.Nbf, now); + } + } + } + catch(Exception ex) + { + if (LogHelper.IsEnabled(EventLogLevel.Error)) + LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(LogMessages.IDX14307, ex, payload))); + } + + payload = jsonPayload != null ? jsonPayload.ToString(Formatting.None) : payload; + var rawPayload = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(payload)); + var message = rawHeader + "." + rawPayload; + var rawSignature = signingCredentials == null ? string.Empty : JwtTokenUtilities.CreateEncodedSignature(message, signingCredentials); + + if (encryptingCredentials != null) + { + additionalHeaderClaims = AddCtyClaimDefaultValue(additionalHeaderClaims, encryptingCredentials.SetDefaultCtyClaim); + + return EncryptTokenPrivate(message + "." + rawSignature, encryptingCredentials, compressionAlgorithm, additionalHeaderClaims, tokenType); + } + + return message + "." + rawSignature; + } + + /// + /// Compress a JWT token string. + /// + /// + /// + /// if is null. + /// if is null. + /// if the compression algorithm is not supported. + /// Compressed JWT token bytes. + private static byte[] CompressToken(string token, string compressionAlgorithm) + { + if (token == null) + throw LogHelper.LogArgumentNullException(nameof(token)); + + if (string.IsNullOrEmpty(compressionAlgorithm)) + throw LogHelper.LogArgumentNullException(nameof(compressionAlgorithm)); + + if (!CompressionProviderFactory.Default.IsSupportedAlgorithm(compressionAlgorithm)) + throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10682, LogHelper.MarkAsNonPII(compressionAlgorithm)))); + + var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(compressionAlgorithm); + + return compressionProvider.Compress(Encoding.UTF8.GetBytes(token)) ?? throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10680, LogHelper.MarkAsNonPII(compressionAlgorithm)))); + } + + private static StringComparison GetStringComparisonRuleIf509(SecurityKey securityKey) => (securityKey is X509SecurityKey) + ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + private static StringComparison GetStringComparisonRuleIf509OrECDsa(SecurityKey securityKey) => (securityKey is X509SecurityKey + || securityKey is ECDsaSecurityKey) + ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + /// + /// Creates a from a . + /// + /// The to use as a source. + /// Contains parameters for validating the token. + /// A containing the . + protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, TokenValidationParameters validationParameters) + { + _ = jwtToken ?? throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + return CreateClaimsIdentityPrivate(jwtToken, validationParameters, GetActualIssuer(jwtToken)); + } + + /// + /// Creates a from a with the specified issuer. + /// + /// The to use as a source. + /// Contains parameters for validating the token. + /// Specifies the issuer for the . + /// A containing the . + protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) + { + _ = jwtToken ?? throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + 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); + } + + private static string GetActualIssuer(JsonWebToken jwtToken) + { + string actualIssuer = jwtToken.Issuer; + if (string.IsNullOrWhiteSpace(actualIssuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Verbose)) + LogHelper.LogVerbose(TokenLogMessages.IDX10244, ClaimsIdentity.DefaultIssuer); + + actualIssuer = ClaimsIdentity.DefaultIssuer; + } + + return actualIssuer; + } + + private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) + { + _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); + foreach (Claim jwtClaim in jwtToken.Claims) + { + string 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, issuer); + } + } + + if (jwtClaim.Properties.Count == 0) + { + identity.AddClaim(new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, issuer, issuer, identity)); + } + else + { + Claim claim = new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, issuer, issuer, identity); + + foreach (var kv in jwtClaim.Properties) + claim.Properties[kv.Key] = kv.Value; + + identity.AddClaim(claim); + } + } + + return identity; + } + + /// + /// Decrypts a JWE and returns the clear text + /// + /// the JWE that contains the cypher text. + /// contains crypto material. + /// the decoded / cleartext contents of the JWE. + /// if is null. + /// if is null. + /// if ' .Enc' is null or empty. + /// if decompression failed. + /// if ' .Kid' is not null AND decryption fails. + /// if the JWE was not able to be decrypted. + public string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters validationParameters) + { + return DecryptToken(jwtToken, validationParameters, null); + } + + private string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + if (jwtToken == null) + throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (string.IsNullOrEmpty(jwtToken.Enc)) + throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(TokenLogMessages.IDX10612))); + + var keys = GetContentEncryptionKeys(jwtToken, validationParameters, configuration); + return JwtTokenUtilities.DecryptJwtToken( + jwtToken, + validationParameters, + new JwtTokenDecryptionParameters + { + DecompressionFunction = JwtTokenUtilities.DecompressToken, + Keys = keys + }); + } + + /// + /// Encrypts a JWS. + /// + /// A 'JSON Web Token' (JWT) in JWS Compact Serialization Format. + /// Defines the security key and algorithm that will be used to encrypt the . + /// if is null or empty. + /// if is null. + /// if both and . are null. + /// if the CryptoProviderFactory being used does not support the (algorithm), pair. + /// if unable to create a token encryption provider for the (algorithm), pair. + /// if encryption fails using the (algorithm), pair. + /// if not using one of the supported content encryption key (CEK) algorithms: 128, 384 or 512 AesCbcHmac (this applies in the case of key wrap only, not direct encryption). + public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCredentials) + { + if (string.IsNullOrEmpty(innerJwt)) + throw LogHelper.LogArgumentNullException(nameof(innerJwt)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + return EncryptTokenPrivate(innerJwt, encryptingCredentials, null, null, null); + } + + /// + /// Encrypts a JWS. + /// + /// A 'JSON Web Token' (JWT) in JWS Compact Serialization Format. + /// Defines the security key and algorithm that will be used to encrypt the . + /// Defines the dictionary containing any custom header claims that need to be added to the outer JWT token header. + /// if is null or empty. + /// if is null. + /// if is null. + /// if both and . are null. + /// if the CryptoProviderFactory being used does not support the (algorithm), pair. + /// if unable to create a token encryption provider for the (algorithm), pair. + /// if encryption fails using the (algorithm), pair. + /// if not using one of the supported content encryption key (CEK) algorithms: 128, 384 or 512 AesCbcHmac (this applies in the case of key wrap only, not direct encryption). + public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCredentials, IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(innerJwt)) + throw LogHelper.LogArgumentNullException(nameof(innerJwt)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return EncryptTokenPrivate(innerJwt, encryptingCredentials, null, additionalHeaderClaims, null); + } + + /// + /// Encrypts a JWS. + /// + /// A 'JSON Web Token' (JWT) in JWS Compact Serialization Format. + /// Defines the security key and algorithm that will be used to encrypt the . + /// Defines the compression algorithm that will be used to compress the 'innerJwt'. + /// if is null or empty. + /// if is null. + /// if is null or empty. + /// if both and . are null. + /// if the CryptoProviderFactory being used does not support the (algorithm), pair. + /// if unable to create a token encryption provider for the (algorithm), pair. + /// if compression using fails. + /// if encryption fails using the (algorithm), pair. + /// if not using one of the supported content encryption key (CEK) algorithms: 128, 384 or 512 AesCbcHmac (this applies in the case of key wrap only, not direct encryption). + public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCredentials, string algorithm) + { + if (string.IsNullOrEmpty(innerJwt)) + throw LogHelper.LogArgumentNullException(nameof(innerJwt)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (string.IsNullOrEmpty(algorithm)) + throw LogHelper.LogArgumentNullException(nameof(algorithm)); + + return EncryptTokenPrivate(innerJwt, encryptingCredentials, algorithm, null, null); + } + + /// + /// Encrypts a JWS. + /// + /// A 'JSON Web Token' (JWT) in JWS Compact Serialization Format. + /// Defines the security key and algorithm that will be used to encrypt the . + /// Defines the compression algorithm that will be used to compress the + /// Defines the dictionary containing any custom header claims that need to be added to the outer JWT token header. + /// if is null or empty. + /// if is null. + /// if is null or empty. + /// if is null or empty. + /// if both and . are null. + /// if the CryptoProviderFactory being used does not support the (algorithm), pair. + /// if unable to create a token encryption provider for the (algorithm), pair. + /// if compression using 'algorithm' fails. + /// if encryption fails using the (algorithm), pair. + /// if not using one of the supported content encryption key (CEK) algorithms: 128, 384 or 512 AesCbcHmac (this applies in the case of key wrap only, not direct encryption). + public string EncryptToken(string innerJwt, EncryptingCredentials encryptingCredentials, string algorithm, IDictionary additionalHeaderClaims) + { + if (string.IsNullOrEmpty(innerJwt)) + throw LogHelper.LogArgumentNullException(nameof(innerJwt)); + + if (encryptingCredentials == null) + throw LogHelper.LogArgumentNullException(nameof(encryptingCredentials)); + + if (string.IsNullOrEmpty(algorithm)) + throw LogHelper.LogArgumentNullException(nameof(algorithm)); + + if (additionalHeaderClaims == null) + throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims)); + + return EncryptTokenPrivate(innerJwt, encryptingCredentials, algorithm, additionalHeaderClaims, null); + } + + private static string EncryptTokenPrivate(string innerJwt, EncryptingCredentials encryptingCredentials, string compressionAlgorithm, IDictionary additionalHeaderClaims, string tokenType) + { + var cryptoProviderFactory = encryptingCredentials.CryptoProviderFactory ?? encryptingCredentials.Key.CryptoProviderFactory; + + if (cryptoProviderFactory == null) + throw LogHelper.LogExceptionMessage(new ArgumentException(TokenLogMessages.IDX10620)); + + byte[] wrappedKey = null; + SecurityKey securityKey = JwtTokenUtilities.GetSecurityKey(encryptingCredentials, cryptoProviderFactory, additionalHeaderClaims, out wrappedKey); + + using (var encryptionProvider = cryptoProviderFactory.CreateAuthenticatedEncryptionProvider(securityKey, encryptingCredentials.Enc)) + { + if (encryptionProvider == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogMessages.IDX14103)); + + var header = CreateDefaultJWEHeader(encryptingCredentials, compressionAlgorithm, tokenType); + if (additionalHeaderClaims != null) + header.Merge(JObject.FromObject(additionalHeaderClaims)); + + byte[] plainText; + if (!string.IsNullOrEmpty(compressionAlgorithm)) + { + try + { + plainText = CompressToken(innerJwt, compressionAlgorithm); + } + catch (Exception ex) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenCompressionFailedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10680, LogHelper.MarkAsNonPII(compressionAlgorithm)), ex)); + } + } + else + { + plainText = Encoding.UTF8.GetBytes(innerJwt); + } + + try + { + var rawHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(header.ToString(Formatting.None))); + var encryptionResult = encryptionProvider.Encrypt(plainText, Encoding.ASCII.GetBytes(rawHeader)); + return JwtConstants.DirectKeyUseAlg.Equals(encryptingCredentials.Alg) ? + string.Join(".", rawHeader, string.Empty, Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)): + string.Join(".", rawHeader, Base64UrlEncoder.Encode(wrappedKey), Base64UrlEncoder.Encode(encryptionResult.IV), Base64UrlEncoder.Encode(encryptionResult.Ciphertext), Base64UrlEncoder.Encode(encryptionResult.AuthenticationTag)); + } + catch (Exception ex) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10616, LogHelper.MarkAsNonPII(encryptingCredentials.Enc), encryptingCredentials.Key), ex)); + } + } + } + + private static SecurityKey ResolveTokenDecryptionKeyFromConfig(JsonWebToken jwtToken, BaseConfiguration configuration) + { + if (jwtToken == null) + throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + if (!string.IsNullOrEmpty(jwtToken.Kid) && configuration.TokenDecryptionKeys != null) + { + foreach (var key in configuration.TokenDecryptionKeys) + { + if (key != null && string.Equals(key.KeyId, jwtToken.Kid, GetStringComparisonRuleIf509OrECDsa(key))) + return key; + } + } + + if (!string.IsNullOrEmpty(jwtToken.X5t) && configuration.TokenDecryptionKeys != null) + { + foreach (var key in configuration.TokenDecryptionKeys) + { + if (key != null && string.Equals(key.KeyId, jwtToken.X5t, GetStringComparisonRuleIf509(key))) + return key; + + var x509Key = key as X509SecurityKey; + if (x509Key != null && string.Equals(x509Key.X5t, jwtToken.X5t, StringComparison.OrdinalIgnoreCase)) + return key; + } + } + + return null; + } + + internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + IEnumerable keys = null; + + // First we check to see if the caller has set a custom decryption resolver on TVP for the call, if so any keys set on TVP and keys in Configuration are ignored. + // If no custom decryption resolver set, we'll check to see if they've set some static decryption keys on TVP. If a key found, we ignore configuration. + // If no key found in TVP, we'll check the configuration. + if (validationParameters.TokenDecryptionKeyResolver != null) + { + keys = validationParameters.TokenDecryptionKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters); + } + else + { + var key = ResolveTokenDecryptionKey(jwtToken.EncodedToken, jwtToken, validationParameters); + if (key != null) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10904, key); + } + else if (configuration != null) + { + key = ResolveTokenDecryptionKeyFromConfig(jwtToken, configuration); + if (key != null && LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10905, key); + } + + if (key != null) + keys = new List { key }; + } + + // on decryption for ECDH-ES, we get the public key from the EPK value see: https://datatracker.ietf.org/doc/html/rfc7518#appendix-C + // we need the ECDSASecurityKey for the receiver, use TokenValidationParameters.TokenDecryptionKey + + // control gets here if: + // 1. User specified delegate: TokenDecryptionKeyResolver returned null + // 2. ResolveTokenDecryptionKey returned null + // 3. ResolveTokenDecryptionKeyFromConfig returned null + // Try all the keys. This is the degenerate case, not concerned about perf. + if (keys == null) + { + keys = JwtTokenUtilities.GetAllDecryptionKeys(validationParameters); + if (configuration != null) + keys = keys == null ? configuration.TokenDecryptionKeys : keys.Concat(configuration.TokenDecryptionKeys); + } + + if (jwtToken.Alg.Equals(JwtConstants.DirectKeyUseAlg, StringComparison.Ordinal) + || jwtToken.Alg.Equals(SecurityAlgorithms.EcdhEs, StringComparison.Ordinal)) + return keys; + + var unwrappedKeys = new List(); + // keep track of exceptions thrown, keys that were tried + StringBuilder exceptionStrings = null; + StringBuilder keysAttempted = null; + foreach (var key in keys) + { + try + { +#if NET472 || NET6_0_OR_GREATER + if (SupportedAlgorithms.EcdsaWrapAlgorithms.Contains(jwtToken.Alg)) + { + // on decryption we get the public key from the EPK value see: https://datatracker.ietf.org/doc/html/rfc7518#appendix-C + var ecdhKeyExchangeProvider = new EcdhKeyExchangeProvider( + key as ECDsaSecurityKey, + validationParameters.TokenDecryptionKey as ECDsaSecurityKey, + jwtToken.Alg, + jwtToken.Enc); + jwtToken.TryGetHeaderValue(JwtHeaderParameterNames.Apu, out string apu); + jwtToken.TryGetHeaderValue(JwtHeaderParameterNames.Apv, out string apv); + SecurityKey kdf = ecdhKeyExchangeProvider.GenerateKdf(apu, apv); + var kwp = key.CryptoProviderFactory.CreateKeyWrapProviderForUnwrap(kdf, ecdhKeyExchangeProvider.GetEncryptionAlgorithm()); + var unwrappedKey = kwp.UnwrapKey(Base64UrlEncoder.DecodeBytes(jwtToken.EncryptedKey)); + unwrappedKeys.Add(new SymmetricSecurityKey(unwrappedKey)); + } + else +#endif + if (key.CryptoProviderFactory.IsSupportedAlgorithm(jwtToken.Alg, key)) + { + var kwp = key.CryptoProviderFactory.CreateKeyWrapProviderForUnwrap(key, jwtToken.Alg); + var unwrappedKey = kwp.UnwrapKey(jwtToken.EncryptedKeyBytes); + unwrappedKeys.Add(new SymmetricSecurityKey(unwrappedKey)); + } + } + catch (Exception ex) + { + (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); + } + + (keysAttempted ??= new StringBuilder()).AppendLine(key.ToString()); + } + + if (unwrappedKeys.Count > 0 && exceptionStrings is null) + return unwrappedKeys; + else + throw LogHelper.LogExceptionMessage(new SecurityTokenKeyWrapException(LogHelper.FormatInvariant(TokenLogMessages.IDX10618, (object)keysAttempted ?? "", (object)exceptionStrings ?? "", jwtToken))); + } + + /// + /// Returns a to use when decrypting a JWE. + /// + /// The the token that is being decrypted. + /// The that is being decrypted. + /// A required for validation. + /// Returns a to use for signature validation. + /// If key fails to resolve, then null is returned + protected virtual SecurityKey ResolveTokenDecryptionKey(string token, JsonWebToken jwtToken, TokenValidationParameters validationParameters) + { + if (jwtToken == null) + throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + StringComparison stringComparison = GetStringComparisonRuleIf509OrECDsa(validationParameters.TokenDecryptionKey); + if (!string.IsNullOrEmpty(jwtToken.Kid)) + { + if (validationParameters.TokenDecryptionKey != null + && string.Equals(validationParameters.TokenDecryptionKey.KeyId, jwtToken.Kid, stringComparison)) + return validationParameters.TokenDecryptionKey; + + if (validationParameters.TokenDecryptionKeys != null) + { + foreach (var key in validationParameters.TokenDecryptionKeys) + { + if (key != null && string.Equals(key.KeyId, jwtToken.Kid, GetStringComparisonRuleIf509OrECDsa(key))) + return key; + } + } + } + + if (!string.IsNullOrEmpty(jwtToken.X5t)) + { + if (validationParameters.TokenDecryptionKey != null) + { + if (string.Equals(validationParameters.TokenDecryptionKey.KeyId, jwtToken.X5t, stringComparison)) + return validationParameters.TokenDecryptionKey; + + var x509Key = validationParameters.TokenDecryptionKey as X509SecurityKey; + if (x509Key != null && string.Equals(x509Key.X5t, jwtToken.X5t, StringComparison.OrdinalIgnoreCase)) + return validationParameters.TokenDecryptionKey; + } + + if (validationParameters.TokenDecryptionKeys != null) + { + foreach (var key in validationParameters.TokenDecryptionKeys) + { + if (key != null && string.Equals(key.KeyId, jwtToken.X5t, GetStringComparisonRuleIf509(key))) + return key; + + var x509Key = key as X509SecurityKey; + if (x509Key != null && string.Equals(x509Key.X5t, jwtToken.X5t, StringComparison.OrdinalIgnoreCase)) + return key; + } + } + } + + return null; + } + + /// + /// Converts a string into an instance of . + /// + /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// A + /// is null or empty. + /// 'token.Length' is greater than . + /// If the is in JWE Compact Serialization format, only the protected header will be deserialized. + /// This method is unable to decrypt the payload. Use to obtain the payload. + /// The token is NOT validated and no security decisions should be made about the contents. + /// Use or to ensure the token is acceptable. + public virtual JsonWebToken ReadJsonWebToken(string token) + { + if (string.IsNullOrEmpty(token)) + throw LogHelper.LogArgumentNullException(nameof(token)); + + if (token.Length > MaximumTokenSizeInBytes) + throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))); + + return new JsonWebToken(token); + } + + /// + /// Converts a string into an instance of . + /// + /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// A + /// is null or empty. + /// 'token.Length' is greater than . + /// The token is NOT validated and no security decisions should be made about the contents. + /// Use or to ensure the token is acceptable. + public override SecurityToken ReadToken(string token) + { + return ReadJsonWebToken(token); + } + + /// + /// Validates a JWS or a JWE. + /// + /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// A required for validation. + /// A + public virtual TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) + { + return ValidateTokenAsync(token, validationParameters).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Validates a token. + /// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property. + /// Callers should always check the TokenValidationResult.IsValid property to verify the validity of the result. + /// + /// The token to be validated. + /// A required for validation. + /// A + /// + /// TokenValidationResult.Exception will be set to one of the following exceptions if the is invalid. + /// if is null or empty. + /// if is null. + /// 'token.Length' is greater than . + /// if is not a valid , + /// if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid , + /// + public override async Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + if (string.IsNullOrEmpty(token)) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(token)), IsValid = false }; + + if (validationParameters == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + if (token.Length > MaximumTokenSizeInBytes) + return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))), IsValid = false }; + + try + { + TokenValidationResult result = ReadToken(token, validationParameters); + if (result.IsValid) + return await ValidateTokenAsync(result.SecurityToken, validationParameters).ConfigureAwait(false); + + return result; + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false + }; + } + } + + /// + public override async Task ValidateTokenAsync(SecurityToken token, TokenValidationParameters validationParameters) + { + if (token == null) + throw LogHelper.LogArgumentNullException(nameof(token)); + + if (validationParameters == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + var jwt = token as JsonWebToken; + if (jwt == null) + return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogHelper.FormatInvariant(LogMessages.IDX14100, token))), IsValid = false }; + + try + { + return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false + }; + } + } + + /// + /// Converts a string into an instance of . + /// + /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// A whose TokenReader, if set, will be used to read a JWT. + /// A + /// if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid . + /// if is not a valid JWT, . + private static TokenValidationResult ReadToken(string token, TokenValidationParameters validationParameters) + { + JsonWebToken jsonWebToken = null; + if (validationParameters.TokenReader != null) + { + var securityToken = validationParameters.TokenReader(token, validationParameters); + if (securityToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10510, LogHelper.MarkAsSecurityArtifact(token, JwtTokenUtilities.SafeLogJwtToken)))); + + jsonWebToken = securityToken as JsonWebToken; + if (jsonWebToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10509, typeof(JsonWebToken), securityToken.GetType(), LogHelper.MarkAsSecurityArtifact(token, JwtTokenUtilities.SafeLogJwtToken)))); + } + else + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + jsonWebToken = new JsonWebToken(token); + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogHelper.FormatInvariant(LogMessages.IDX14100, LogHelper.MarkAsSecurityArtifact(token, JwtTokenUtilities.SafeLogJwtToken), ex))), + IsValid = false + }; + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + return new TokenValidationResult + { + SecurityToken = jsonWebToken, + IsValid = true + }; + } + + /// + /// Private method for token validation, responsible for: + /// (1) Obtaining a configuration from the . + /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. + /// + /// The JWT token + /// The to be used for validation. + /// + private async ValueTask ValidateTokenAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) + { + BaseConfiguration currentConfiguration = null; + if (validationParameters.ConfigurationManager != null) + { + try + { + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set + // directly on them, allowing the library to continue with token validation. + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); + } + } + + TokenValidationResult tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); + if (validationParameters.ConfigurationManager != null) + { + if (tokenValidationResult.IsValid) + { + // Set current configuration as LKG if it exists. + if (currentConfiguration != null) + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + + return tokenValidationResult; + } + else if (TokenUtilities.IsRecoverableException(tokenValidationResult.Exception)) + { + // If we were still unable to validate, attempt to refresh the configuration and validate using it + // but ONLY if the currentConfiguration is not null. We want to avoid refreshing the configuration on + // retrieval error as this case should have already been hit before. This refresh handles the case + // where a new valid configuration was somehow published during validation time. + if (currentConfiguration != null) + { + validationParameters.ConfigurationManager.RequestRefresh(); + validationParameters.RefreshBeforeValidation = true; + var lastConfig = currentConfiguration; + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + + // Only try to re-validate using the newly obtained config if it doesn't reference equal the previously used configuration. + if (lastConfig != currentConfiguration) + { + tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + { + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + return tokenValidationResult; + } + } + } + + if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration) + { + validationParameters.RefreshBeforeValidation = false; + validationParameters.ValidateWithLKG = true; + var recoverableException = tokenValidationResult.Exception; + + foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations()) + { + if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException)) + { + tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + return tokenValidationResult; + } + } + } + } + } + + return tokenValidationResult; + } + + private ValueTask ValidateTokenAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + return jsonWebToken.IsEncrypted ? + ValidateJWEAsync(jsonWebToken, validationParameters, configuration) : + ValidateJWSAsync(jsonWebToken, validationParameters, configuration); + } + + private async ValueTask ValidateJWSAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + try + { + TokenValidationResult tokenValidationResult; + if (validationParameters.TransformBeforeSignatureValidation != null) + jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; + + if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) + { + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters, configuration); + tokenValidationResult = await ValidateTokenPayloadAsync(validatedToken, validationParameters, configuration).ConfigureAwait(false); + Tokens.Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters, configuration); + } + else + { + if (validationParameters.ValidateSignatureLast) + { + tokenValidationResult = await ValidateTokenPayloadAsync(jsonWebToken, validationParameters, configuration).ConfigureAwait(false); + if (tokenValidationResult.IsValid) + tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); + } + else + { + var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); + tokenValidationResult = await ValidateTokenPayloadAsync(validatedToken, validationParameters, configuration).ConfigureAwait(false); + } + } + + return tokenValidationResult; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null + }; + } + } + + private async ValueTask ValidateJWEAsync(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + try + { + TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters, configuration), validationParameters); + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + tokenValidationResult = await ValidateJWSAsync(tokenValidationResult.SecurityToken as JsonWebToken, validationParameters, configuration).ConfigureAwait(false); + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; + jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; + return new TokenValidationResult + { + SecurityToken = jwtToken, + ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, + IsValid = true, + TokenType = tokenValidationResult.TokenType + }; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null + }; + } + } + + private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + if (validationParameters.SignatureValidatorUsingConfiguration != null) + { + var validatedToken = validationParameters.SignatureValidatorUsingConfiguration(jsonWebToken.EncodedToken, validationParameters, configuration); + if (validatedToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + else if (validationParameters.SignatureValidator != null) + { + var validatedToken = validationParameters.SignatureValidator(jsonWebToken.EncodedToken, validationParameters); + if (validatedToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + } + + private static JsonWebToken ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + JsonWebToken validatedToken = ValidateSignature(jsonWebToken, validationParameters, configuration); + Microsoft.IdentityModel.Tokens.Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, jsonWebToken, validationParameters, configuration); + + return validatedToken; + } + + private async ValueTask ValidateTokenPayloadAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; + var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; + + Tokens.Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); + Tokens.Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); + string issuer = await Tokens.Validators.ValidateIssuerAsync(jsonWebToken.Issuer, jsonWebToken, validationParameters, configuration).ConfigureAwait(false); + + Tokens.Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); + if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) + { + // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string + // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, + // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature + // and (since issuer validation occurs first) came from a trusted authority. + // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, + // this code will still work properly. + TokenValidationResult tokenValidationResult = + await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + } + + string tokenType = Tokens.Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); + return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer) + { + IsValid = true, + TokenType = tokenType + }; + } + + /// + /// Validates the JWT signature. + /// + private static JsonWebToken ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + bool kidMatched = false; + IEnumerable keys = null; + + if (!jwtToken.IsSigned) + { + if (validationParameters.RequireSignedTokens) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10504, jwtToken))); + else + return jwtToken; + } + + if (validationParameters.IssuerSigningKeyResolverUsingConfiguration != null) + { + keys = validationParameters.IssuerSigningKeyResolverUsingConfiguration(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters, configuration); + } + else if (validationParameters.IssuerSigningKeyResolver != null) + { + keys = validationParameters.IssuerSigningKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters); + } + else + { + var key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters, configuration); + if (key != null) + { + kidMatched = true; + keys = new List { key }; + } + } + + if (validationParameters.TryAllIssuerSigningKeys && keys.IsNullOrEmpty()) + { + // control gets here if: + // 1. User specified delegate: IssuerSigningKeyResolver returned null + // 2. ResolveIssuerSigningKey returned null + // Try all the keys. This is the degenerate case, not concerned about perf. + keys = TokenUtilities.GetAllSigningKeys(configuration, validationParameters); + } + + // keep track of exceptions thrown, keys that were tried + StringBuilder exceptionStrings = null; + StringBuilder keysAttempted = null; + var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); + + if (keys != null) + { + foreach (var key in keys) + { + try + { + if (ValidateSignature(jwtToken, key, validationParameters)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); + + jwtToken.SigningKey = key; + return jwtToken; + } + } + catch (Exception ex) + { + (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); + } + + if (key != null) + { + (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); + if (kidExists && !kidMatched && key.KeyId != null) + kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + } + } + + // Get information on where keys used during token validation came from for debugging purposes. + var keysInTokenValidationParameters = TokenUtilities.GetAllSigningKeys(validationParameters: validationParameters); + var keysInConfiguration = TokenUtilities.GetAllSigningKeys(configuration); + var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count(); + var numKeysInConfiguration = keysInConfiguration.Count(); + + if (kidExists) + { + if (kidMatched) + { + JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path + var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); + var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10511, + (object)keysAttempted ?? "", + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + LogHelper.MarkAsNonPII(keyLocation), + LogHelper.MarkAsNonPII(jwtToken.Kid), + (object)exceptionStrings ?? "", + jwtToken))); + } + + var expires = jwtToken.TryGetClaim(JwtRegisteredClaimNames.Exp, out var _) ? (DateTime?)jwtToken.ValidTo : null; + var notBefore = jwtToken.TryGetClaim(JwtRegisteredClaimNames.Nbf, out var _) ? (DateTime?)jwtToken.ValidFrom : null; + + if (!validationParameters.ValidateSignatureLast) + { + InternalValidators.ValidateAfterSignatureFailed( + jwtToken, + notBefore, + expires, + jwtToken.Audiences, + validationParameters, + configuration); + } + } + + if (keysAttempted is not null) + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10503, + keysAttempted, + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + (object)exceptionStrings ?? "", + jwtToken))); + + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(TokenLogMessages.IDX10500)); + } + + /// + /// Obtains a and validates the signature. + /// + /// Bytes to validate. + /// Signature to compare against. + /// to use. + /// Crypto algorithm to use. + /// The being validated. + /// Priority will be given to over . + /// 'true' if signature is valid. + internal static bool ValidateSignature(byte[] encodedBytes, byte[] signature, SecurityKey key, string algorithm, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + if (!cryptoProviderFactory.IsSupportedAlgorithm(algorithm, key)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX14000, LogHelper.MarkAsNonPII(algorithm), key); + + return false; + } + + Tokens.Validators.ValidateAlgorithm(algorithm, key, securityToken, validationParameters); + + var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, algorithm); + if (signatureProvider == null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), LogHelper.MarkAsNonPII(algorithm)))); + + try + { + return signatureProvider.Verify(encodedBytes, signature); + } + finally + { + cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); + } + } + + internal static bool IsSignatureValid(byte[] signatureBytes, int signatureBytesLength, SignatureProvider signatureProvider, byte[] dataToVerify, int dataToVerifyLength) + { + if (signatureProvider is SymmetricSignatureProvider) + { + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); + } + else + { + if (signatureBytes.Length == signatureBytesLength) + { + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); + } + else + { + byte[] sigBytes = new byte[signatureBytesLength]; + Array.Copy(signatureBytes, 0, sigBytes, 0, signatureBytesLength); + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, sigBytes, 0, signatureBytesLength); + } + } + } + + internal static bool ValidateSignature(byte[] bytes, int len, string stringWithSignature, int signatureStartIndex, SignatureProvider signatureProvider) + { + return Base64UrlEncoding.Decode( + stringWithSignature, + signatureStartIndex + 1, + stringWithSignature.Length - signatureStartIndex - 1, + signatureProvider, + bytes, + len, + IsSignatureValid); + } + + internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters) + { + var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX14000, LogHelper.MarkAsNonPII(jsonWebToken.Alg), key); + + return false; + } + + Tokens.Validators.ValidateAlgorithm(jsonWebToken.Alg, key, jsonWebToken, validationParameters); + var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); + try + { + if (signatureProvider == null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), LogHelper.MarkAsNonPII(jsonWebToken.Alg)))); + + return EncodingUtils.PerformEncodingDependentOperation( + jsonWebToken.EncodedToken, + 0, + jsonWebToken.Dot2, + Encoding.UTF8, + jsonWebToken.EncodedToken, + jsonWebToken.Dot2, + signatureProvider, + ValidateSignature); + } + finally + { + cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); + } + } + } +} diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectProtocolValidatorTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectProtocolValidatorTests.cs index 316bda6425..0aa3497301 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectProtocolValidatorTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectProtocolValidatorTests.cs @@ -423,7 +423,7 @@ public static TheoryData ValidateUserInfoRespon new OidcProtocolValidatorTheoryData { - ExpectedException = new ExpectedException(typeof(OpenIdConnectProtocolException), "IDX21343:", typeof(JsonReaderException)), + ExpectedException = new ExpectedException(typeof(OpenIdConnectProtocolException), "IDX21343:", typeof(System.Text.Json.JsonException)), ProtocolValidator = new PublicOpenIdConnectProtocolValidator(), TestId = "UserInfoEndpointResponse is not valid JSON", ValidationContext = new OpenIdConnectProtocolValidationContext diff --git a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/PopKeyResolvingTests.cs b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/PopKeyResolvingTests.cs index 4631d668c1..72cf29b517 100644 --- a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/PopKeyResolvingTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/PopKeyResolvingTests.cs @@ -26,7 +26,9 @@ public async Task ResolvePopKeyFromCnfClaimAsync(ResolvePopKeyTheoryData theoryD { var signedHttpRequestValidationContext = theoryData.BuildSignedHttpRequestValidationContext(); var handler = new SignedHttpRequestHandlerPublic(); - _ = await handler.ResolvePopKeyFromCnfClaimAsync(theoryData.ConfirmationClaim, theoryData.SignedHttpRequestToken, theoryData.ValidatedAccessToken, signedHttpRequestValidationContext, CancellationToken.None).ConfigureAwait(false); + + Cnf cnf = theoryData.ConfirmationClaim != null ? new Cnf(theoryData.ConfirmationClaim.ToString(Formatting.None)) : null; + _ = await handler.ResolvePopKeyFromCnfClaimAsync(cnf, theoryData.SignedHttpRequestToken, theoryData.ValidatedAccessToken, signedHttpRequestValidationContext, CancellationToken.None).ConfigureAwait(false); if ((bool)signedHttpRequestValidationContext.CallContext.PropertyBag[theoryData.MethodToCall] == false) context.AddDiff($"{theoryData.MethodToCall} was not called."); @@ -201,7 +203,7 @@ public void ResolvePopKeyFromJwk(ResolvePopKeyTheoryData theoryData) { var signedHttpRequestValidationContext = theoryData.BuildSignedHttpRequestValidationContext(); var handler = new SignedHttpRequestHandler(); - _ = handler.ResolvePopKeyFromJwk(theoryData.PopKeyString, signedHttpRequestValidationContext); + _ = handler.ResolvePopKeyFromJwk(new JsonWebKey(theoryData.PopKeyString), signedHttpRequestValidationContext); theoryData.ExpectedException.ProcessNoException(context); } @@ -528,7 +530,8 @@ public async Task ResolvePopKeyFromJkuKidAsync(ResolvePopKeyTheoryData theoryDat { var signedHttpRequestValidationContext = theoryData.BuildSignedHttpRequestValidationContext(); var handler = new SignedHttpRequestHandlerPublic(); - var popKey = await handler.ResolvePopKeyFromJkuAsync(string.Empty, JObject.Parse($@"{{""kid"": ""{theoryData.Kid}""}}"), signedHttpRequestValidationContext, CancellationToken.None).ConfigureAwait(false); + Cnf cnf = new Cnf { Kid = theoryData.Kid }; + var popKey = await handler.ResolvePopKeyFromJkuAsync(string.Empty, cnf, signedHttpRequestValidationContext, CancellationToken.None).ConfigureAwait(false); if (popKey == null) context.AddDiff("Resolved Pop key is null."); @@ -549,6 +552,19 @@ public static TheoryData ResolvePopKeyFromJkuKidTheoryD { return new TheoryData { + new ResolvePopKeyTheoryData + { + Kid ="bad_kid", + CallContext = new CallContext() + { + PropertyBag = new Dictionary() + { + {"mockGetPopKeysFromJkuAsync_return2Keys", null } + } + }, + ExpectedException = new ExpectedException(typeof(SignedHttpRequestInvalidPopKeyException), "IDX23021"), + TestId = "InvalidNoKidMatch", + }, new ResolvePopKeyTheoryData { First = true, diff --git a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestE2ETests.cs b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestE2ETests.cs index 4b68030750..67c388c080 100644 --- a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestE2ETests.cs +++ b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestE2ETests.cs @@ -6,6 +6,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Security.Cryptography; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Logging; @@ -304,10 +305,10 @@ public static TheoryData RoundtripTheoryDa TokenValidationParameters = SignedHttpRequestTestUtils.DefaultTokenValidationParameters, HttpRequestData = httpRequestData, AccessToken = SignedHttpRequestTestUtils.CreateAt(x509KeyCnfKeyId, false), - CnfClaimValue = new JObject - { - { ConfirmationClaimTypes.Jwk, $@"{{""{JsonWebKeyParameterNames.Kid}"":""{KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256.KeyId}"",""{JsonWebKeyParameterNames.Kty}"":""{JsonWebAlgorithmsKeyTypes.RSA}"",""{JsonWebKeyParameterNames.X5c}"":[""{Convert.ToBase64String(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256.Certificate.RawData)}""]}}" }, - }.ToString(Formatting.None), + CnfClaimValue = $@"{{""{ConfirmationClaimTypes.Jwk}"":" + + $@"{{""{JsonWebKeyParameterNames.Kid}"":""{KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256.KeyId}""," + + $@"""{JsonWebKeyParameterNames.Kty}"":""{JsonWebAlgorithmsKeyTypes.RSA}""," + + $@"""{JsonWebKeyParameterNames.X5c}"":[""{Convert.ToBase64String(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256.Certificate.RawData)}""]}}}}", SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256){CryptoProviderFactory = CreateCryptoProviderFactory() }, TestId = "ValidX5cThumbprint", }, diff --git a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestHandlerPublic.cs b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestHandlerPublic.cs index 161f038d32..9952634a61 100644 --- a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestHandlerPublic.cs +++ b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestHandlerPublic.cs @@ -66,17 +66,17 @@ public async Task ResolvePopKeyPublicAsync(JsonWebToken signedHttpR return await ResolvePopKeyAsync(signedHttpRequest, validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); } - internal JObject GetCnfClaimValuePublic(JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) + internal Cnf GetCnfClaimValuePublic(JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { return GetCnfClaimValue(signedHttpRequest, validatedAccessToken, signedHttpRequestValidationContext); } - internal async Task ResolvePopKeyFromCnfClaimPublicAsync(JObject confirmationClaim, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal async Task ResolvePopKeyFromCnfClaimPublicAsync(Cnf confirmationClaim, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { return await ResolvePopKeyFromCnfClaimAsync(confirmationClaim, signedHttpRequest, validatedAccessToken, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); } - public SecurityKey ResolvePopKeyFromJwkPublic(string jwk, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) + public SecurityKey ResolvePopKeyFromJwkPublic(JsonWebKey jwk, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { return ResolvePopKeyFromJwk(jwk, signedHttpRequestValidationContext); } @@ -86,7 +86,7 @@ public async Task ResolvePopKeyFromJwePublicAsync(string jwe, JsonW return await ResolvePopKeyFromJweAsync(jwe, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); } - internal async Task ResolvePopKeyFromJkuPublicAsync(string jkuSetUrl, JObject cnf, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal async Task ResolvePopKeyFromJkuPublicAsync(string jkuSetUrl, Cnf cnf, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { return await ResolvePopKeyFromJkuAsync(jkuSetUrl, cnf, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); } @@ -202,31 +202,31 @@ internal override async Task ValidateAccessTokenAsync(str } } - internal override JObject GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) + internal override Cnf GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockGetCnfClaimValue_returnJwk")) { - return SignedHttpRequestTestUtils.DefaultCnfJwk; + return new Cnf(SignedHttpRequestTestUtils.DefaultCnfJwk.ToString(Newtonsoft.Json.Formatting.None)); } else if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockGetCnfClaimValue_returnJwe")) { - return SignedHttpRequestTestUtils.DefaultCnfJwe; + return new Cnf(SignedHttpRequestTestUtils.DefaultCnfJwe.ToString(Newtonsoft.Json.Formatting.None)); } else if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockGetCnfClaimValue_returnJku")) { - return SignedHttpRequestTestUtils.DefaultJku; + return new Cnf(SignedHttpRequestTestUtils.DefaultJku.ToString(Newtonsoft.Json.Formatting.None)); } else if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockGetCnfClaimValue_returnJkuKid")) { - return SignedHttpRequestTestUtils.DefaultJkuKid; + return new Cnf(SignedHttpRequestTestUtils.DefaultJkuKid.ToString(Newtonsoft.Json.Formatting.None)); } else if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockGetCnfClaimValue_returnKid")) { - return SignedHttpRequestTestUtils.DefaultKid; + return new Cnf(SignedHttpRequestTestUtils.DefaultKid.ToString(Newtonsoft.Json.Formatting.None)); } else if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockGetCnfClaimValue_returnCustom")) { - return JObject.Parse("{\"custom\": 1}"); + return new Cnf("{\"custom\": 1}"); } else { @@ -234,7 +234,7 @@ internal override JObject GetCnfClaimValue(JsonWebToken signedHttpRequest, JsonW } } - internal override async Task ResolvePopKeyFromCnfClaimAsync(JObject confirmationClaim, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal override async Task ResolvePopKeyFromCnfClaimAsync(Cnf confirmationClaim, JsonWebToken signedHttpRequest, JsonWebToken validatedAccessToken, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { if (signedHttpRequestValidationContext?.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("mockResolvePopKeyFromCnfClaimAsync_returnRsa")) { @@ -246,7 +246,7 @@ internal override async Task ResolvePopKeyFromCnfClaimAsync(JObject } } - internal override SecurityKey ResolvePopKeyFromJwk(string jwk, SignedHttpRequestValidationContext signedHttpRequestValidationContext) + internal override SecurityKey ResolvePopKeyFromJwk(JsonWebKey jwk, SignedHttpRequestValidationContext signedHttpRequestValidationContext) { if (signedHttpRequestValidationContext.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("trackResolvePopKeyFromJwk")) { @@ -268,7 +268,7 @@ internal override async Task ResolvePopKeyFromJweAsync(string jwe, return await base.ResolvePopKeyFromJweAsync(jwe, signedHttpRequestValidationContext, cancellationToken).ConfigureAwait(false); } - internal override async Task ResolvePopKeyFromJkuAsync(string jkuSetUrl, JObject cnf, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) + internal override async Task ResolvePopKeyFromJkuAsync(string jkuSetUrl, Cnf cnf, SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) { if (signedHttpRequestValidationContext.CallContext.PropertyBag != null && signedHttpRequestValidationContext.CallContext.PropertyBag.ContainsKey("trackResolvePopKeyFromJku")) { diff --git a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs index 6512b0ba08..8f2a14b123 100644 --- a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs +++ b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestTestUtils.cs @@ -7,6 +7,7 @@ using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; @@ -17,7 +18,8 @@ namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests { public static class SignedHttpRequestTestUtils { - internal static string DefaultEncodedAccessToken => CreateAt(DefaultCnfJwk, false); + internal static string DefaultEncodedAccessToken = CreateAt(DefaultCnfJwk, false); + internal static string DefaultEncodedAccessTokenWithCnfThumprint = CreateAt(DefaultCnfJwkThumprint, false); internal static SigningCredentials DefaultSigningCredentials => new SigningCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Sha256){ CryptoProviderFactory = new CryptoProviderFactory()}; @@ -61,6 +63,11 @@ public static class SignedHttpRequestTestUtils { JwtHeaderParameterNames.Kid, KeyingMaterial.RsaSecurityKey_2048.InternalId } }; + internal static Cnf CnfJwk => new Cnf + { + JsonWebKey = new JsonWebKey(DefaultJwk.ToString(Formatting.None)) + }; + internal static JObject DefaultCnfJwk => new JObject { { JwtHeaderParameterNames.Jwk, DefaultJwk }, @@ -71,16 +78,31 @@ public static class SignedHttpRequestTestUtils { JwtHeaderParameterNames.Kid, Base64UrlEncoder.Encode(new JsonWebKey(DefaultJwk.ToString(Formatting.None)).ComputeJwkThumbprint()) }, }; + internal static Cnf CnfJwkThumprint => new Cnf + { + Kid = Base64UrlEncoder.Encode(new JsonWebKey(DefaultJwk.ToString(Formatting.None)).ComputeJwkThumbprint()) + }; + internal static JObject DefaultCnfJwkEcdsa => new JObject { { JwtHeaderParameterNames.Jwk, DefaultJwkEcdsa }, }; + internal static Cnf CnfJwkEcdsa => new Cnf + { + JsonWebKey = new JsonWebKey(DefaultJwkEcdsa.ToString(Formatting.None)) + }; + internal static JObject DefaultCnfJwkEcdsaThumbprint => new JObject { { JwtHeaderParameterNames.Kid, Base64UrlEncoder.Encode(new JsonWebKey(DefaultJwkEcdsa.ToString(Formatting.None)).ComputeJwkThumbprint()) }, }; + internal static Cnf CnfJwkEcdsaThumbprint => new Cnf + { + Kid = Base64UrlEncoder.Encode(new JsonWebKey(DefaultJwkEcdsa.ToString(Formatting.None)).ComputeJwkThumbprint()) + }; + #if NET461 || NET462 internal static JObject DefaultJwkEcdsa => new JObject { diff --git a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestValidationTests.cs b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestValidationTests.cs index 632d5922a1..91db0e6369 100644 --- a/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestValidationTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.SignedHttpRequest.Tests/SignedHttpRequestValidationTests.cs @@ -666,7 +666,7 @@ public static TheoryData ValidateHClaimTheo { HttpRequestHeaders = new Dictionary>(), SignedHttpRequestToken = SignedHttpRequestTestUtils.ReplaceOrAddPropertyAndCreateDefaultSignedHttpRequest(new JProperty(SignedHttpRequestClaimTypes.H, "notAnArray")), - ExpectedException = new ExpectedException(typeof(SignedHttpRequestInvalidHClaimException), "IDX23003"), + ExpectedException = new ExpectedException(typeof(SignedHttpRequestInvalidHClaimException), "IDX23024", innerTypeExpected: typeof(ArgumentOutOfRangeException)), TestId = "InvalidClaimType", }, new ValidateSignedHttpRequestTheoryData @@ -841,7 +841,7 @@ public static TheoryData ValidateQClaimTheo { HttpRequestUri = new Uri("https://www.contoso.com"), SignedHttpRequestToken = SignedHttpRequestTestUtils.ReplaceOrAddPropertyAndCreateDefaultSignedHttpRequest(new JProperty(SignedHttpRequestClaimTypes.Q, "notAnArray")), - ExpectedException = new ExpectedException(typeof(SignedHttpRequestInvalidQClaimException), "IDX23003"), + ExpectedException = new ExpectedException(typeof(SignedHttpRequestInvalidQClaimException), "IDX23024", innerTypeExpected: typeof(ArgumentOutOfRangeException)), TestId = "InvalidClaimType", }, new ValidateSignedHttpRequestTheoryData diff --git a/test/Microsoft.IdentityModel.TestUtils/ClaimSets.cs b/test/Microsoft.IdentityModel.TestUtils/ClaimSets.cs index d90c009c27..703e9eabd7 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ClaimSets.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ClaimSets.cs @@ -6,6 +6,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; +using System.Text.Json; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace Microsoft.IdentityModel.TestUtils @@ -404,7 +405,7 @@ public static List EntityAsJsonClaim( string issuer, string originalIssue return new List { new Claim( typeof(Entity).ToString(), - JsonExtensions.SerializeToJson(Entity.Default), + JsonSerializer.Serialize(Entity.Default), JsonClaimValueTypes.Json, issuer ?? Default.Issuer, originalIssuer) }; diff --git a/test/Microsoft.IdentityModel.TestUtils/Default.cs b/test/Microsoft.IdentityModel.TestUtils/Default.cs index 56eefc7091..3b7bab43e8 100644 --- a/test/Microsoft.IdentityModel.TestUtils/Default.cs +++ b/test/Microsoft.IdentityModel.TestUtils/Default.cs @@ -435,8 +435,8 @@ public static List PayloadJsonClaims new Claim(JwtRegisteredClaimNames.Aud, Audience, ClaimValueTypes.String), new Claim(JwtRegisteredClaimNames.Iss, Issuer, ClaimValueTypes.String), new Claim("ClaimValueTypes.String", "ClaimValueTypes.String.Value", ClaimValueTypes.String), - new Claim("ClaimValueTypes.Boolean.true", "true", ClaimValueTypes.Boolean), - new Claim("ClaimValueTypes.Boolean.false", "false", ClaimValueTypes.Boolean), + new Claim("ClaimValueTypes.Boolean.true", "True", ClaimValueTypes.Boolean), + new Claim("ClaimValueTypes.Boolean.false", "False", ClaimValueTypes.Boolean), new Claim("ClaimValueTypes.Double", "123.4", ClaimValueTypes.Double), new Claim("ClaimValueTypes.DateTime.IS8061", "2019-11-15T14:31:21.6101326Z", ClaimValueTypes.DateTime), new Claim("ClaimValueTypes.DateTime", "2019-11-15", ClaimValueTypes.DateTime), @@ -460,7 +460,7 @@ public static Dictionary PayloadJsonDictionary { "ClaimValueTypes.Boolean.true", true }, { "ClaimValueTypes.Boolean.false", false }, { "ClaimValueTypes.Double", 123.4 }, - { "ClaimValueTypes.DateTime.IS8061", DateTime.TryParse("2019-11-15T14:31:21.6101326Z", out DateTime dateTimeValue1) ? dateTimeValue1 : new DateTime()}, + { "ClaimValueTypes.DateTime.IS8061", DateTime.TryParse("2019-11-15T14:31:21.6101326Z", out DateTime dateTimeValue1) ? dateTimeValue1.ToUniversalTime() : new DateTime()}, { "ClaimValueTypes.DateTime", DateTime.TryParse("2019-11-15", out DateTime dateTimeValue2) ? dateTimeValue2 : new DateTime()}, { "ClaimValueTypes.JsonClaimValueTypes.Json1", JObject.Parse(@"{""jsonProperty1"":""jsonvalue1""}") }, { "ClaimValueTypes.JsonClaimValueTypes.Json2", JObject.Parse(@"{""jsonProperty2"":""jsonvalue2""}") }, diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerPrimitivesTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerPrimitivesTests.cs index 09eac0a0be..3be0c65d81 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerPrimitivesTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerPrimitivesTests.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.IdentityModel.TestUtils; @@ -14,6 +16,127 @@ namespace Microsoft.IdentityModel.Tokens.Json.Tests { public class JsonSerializerPrimitivesTests { + /// + /// This test is designed to ensure that JsonSerializationPrimitives maximize depth of arrays of arrays to two. + /// + /// + [Theory, MemberData(nameof(CheckMaximumDepthTheoryData))] + public void CheckMaximumDepth(JsonSerializerTheoryData theoryData) + { + CompareContext context = new CompareContext(theoryData); + using (MemoryStream memoryStream = new MemoryStream()) + { + Utf8JsonWriter writer = null; + try + { + writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + writer.WriteStartObject(); + + JsonSerializerPrimitives.WriteObject(ref writer, theoryData.PropertyName, theoryData.Object); + + writer.WriteEndObject(); + writer.Flush(); + + string json = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); + IdentityComparer.AreEqual(json, theoryData.Json, context); + } + finally + { + writer?.Dispose(); + } + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData CheckMaximumDepthTheoryData + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add( + new JsonSerializerTheoryData("ObjectWithDictionary") + { + Json = $@"{{""_claim_sources"":{{"+ + $@"""src1"":{{"+ + $@"""endpoint"":""https://graph.windows.net/5803816d-c4ab-4601-a128-e2576e5d6910/users/0c9545d0-a670-4628-8c1f-e90618a3b940/getMemberObjects"","+ + $@"""access_token"":""ksj3n283dke"""+ + $@"}},"+ + $@"""src2"":{{"+ + $@"""endpoint2"":""https://graph.windows.net/5803816d-c4ab-4601-a128-e2576e5d6910/users/0c9545d0-a670-4628-8c1f-e90618a3b940/getMemberObjects"","+ + $@"""access_token2"":""ksj3n283dke"""+ + $@"}}}}}}", + PropertyName = "_claim_sources", + Object = new Dictionary + { + { + "src1", + new Dictionary + { + { "endpoint", "https://graph.windows.net/5803816d-c4ab-4601-a128-e2576e5d6910/users/0c9545d0-a670-4628-8c1f-e90618a3b940/getMemberObjects"}, + { "access_token", "ksj3n283dke"} + } + }, + { + "src2", + new Dictionary + { + { "endpoint2", "https://graph.windows.net/5803816d-c4ab-4601-a128-e2576e5d6910/users/0c9545d0-a670-4628-8c1f-e90618a3b940/getMemberObjects"}, + { "access_token2", "ksj3n283dke"} + } + } + } + }); + + theoryData.Add( + new JsonSerializerTheoryData("DictionaryLevel3") + { + Json = $@"{{""key"":{{""l1_1"":1,""l1_2"":""level1"",""l2_dict"":{{""l2_1"":1,""l2_2"":""level2"",""l3_dict"":""System.Collections.Generic.Dictionary`2[System.String,System.Object]""}}}}}}", + PropertyName = "key", + Object = new Dictionary { { "l1_1", 1 }, { "l1_2", "level1" }, + { "l2_dict", new Dictionary { { "l2_1", 1 }, { "l2_2", "level2" }, + { "l3_dict", new Dictionary { { "l3_1", 1 }, { "l3_2", "level3" } } } } } } + }); + + theoryData.Add( + new JsonSerializerTheoryData("DictionaryLevel1") + { + Json = $@"{{""key"":{{""l1_1"":1,""l1_2"":""level1""}}}}", + PropertyName = "key", + Object = new Dictionary { { "l1_1", 1 }, { "l1_2", "level1" } }, + }); + + theoryData.Add( + new JsonSerializerTheoryData("ListLevel3") + { + Json = $@"{{""key"":[1,""string"",1.23,[3,""stringLevel2"",6.52,""System.Collections.Generic.List`1[System.Object]""]]}}", + PropertyName = "key", + Object = new List { 1, "string", 1.23, + new List { 3, "stringLevel2", 6.52, + new List { 3, "stringLevel2", 6.52 } } } + }); + + theoryData.Add( + new JsonSerializerTheoryData("ListLevel2") + { + Json = @$"{{""key"":[1,""string"",1.23,[3,""stringLevel2"",6.52]]}}", + PropertyName = "key", + Object = new List { 1, "string", 1.23, new List { 3, "stringLevel2", 6.52 } }, + }); + + theoryData.Add( + new JsonSerializerTheoryData("ListLevel1") + { + Json = @$"{{""key"":[1,""string"",1.23]}}", + PropertyName = "key", + Object = new List { 1, "string", 1.23 }, + }); + + return theoryData; + } + } + /// /// This test is designed to ensure that JsonDeserialize and Utf8Reader are consistent and /// that we understand the differences with newtonsoft. @@ -178,9 +301,9 @@ public void Serialize(JsonSerializerTheoryData theoryData) new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - #if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - #endif +#endif }); string serialize = JsonTestClassSerializer.Serialize( diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerTheoryData.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerTheoryData.cs index 08647ac4a5..db4c9321c2 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerTheoryData.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonSerializerTheoryData.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections; using System.Collections.Generic; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens.Json.Tests; @@ -28,6 +27,10 @@ public JsonSerializerTheoryData(string testId) : base(testId) { } public JsonTestClass JsonTestClass { get; set; } + public string PropertyName { get; set; } + + public object Object { get; set; } + public IDictionary Serializers { get; set; } = new Dictionary(); } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonTestClassSerializer.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonTestClassSerializer.cs index 4c4cda0c66..b6c8164b72 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonTestClassSerializer.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonTestClassSerializer.cs @@ -65,11 +65,11 @@ public static JsonTestClass Deserialize(ref Utf8JsonReader reader, IDictionary objects = new List(); - if (JsonSerializerPrimitives.ReadObjects(ref reader, objects, "ListObject", _className) == null) + if (ReadObjects(ref reader, objects, "ListObject", _className) == null) { throw LogHelper.LogExceptionMessage( new ArgumentNullException( @@ -132,7 +132,7 @@ public static JsonTestClass Deserialize(ref Utf8JsonReader reader, IDictionary ReadObjects(ref Utf8JsonReader reader, IList objects, string propertyName, string className) + { + _ = objects ?? throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(objects))); + + // returning null keeps the same logic as JsonSerialization.ReadObject + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartArray, false)) + throw LogHelper.LogExceptionMessage( + JsonSerializerPrimitives.CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); + + while (reader.Read()) + { + if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndArray, true)) + break; + + objects.Add(JsonSerializerPrimitives.ReadJsonElement(ref reader)); + } + + return objects; + } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs index ac2379efc4..bfcbc6b83c 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs @@ -11,6 +11,7 @@ namespace Microsoft.IdentityModel.Tokens.Json.Tests { public class JsonUtilities { + static IList _arrayDataAsObjectList = new List { "value1", "value2" }; static string _arrayData = @"[""value1"",""value2""]"; static string _objectData = @"{""Object"":""string""}"; static string DP = "ErP3OpudePAY3uGFSoF16Sde69PnOra62jDEZGnPx_v3nPNpA5sr-tNc8bQP074yQl5kzSFRjRlstyW0TpBVMP0ocbD8RsN4EKsgJ1jvaSIEoP87OxduGkim49wFA0Qxf_NyrcYUnz6XSidY3lC_pF4JDJXg5bP_x0MUkQCTtQE"; @@ -136,7 +137,7 @@ public static void SetAdditionalData(IDictionary dictionary, str SetAdditionalDataNumbers(dictionary); SetAdditionalDataValues(dictionary); dictionary["Object"] = CreateJsonElement(_objectData); - dictionary["Array"] = CreateJsonElement(_arrayData); + dictionary["Array"] = _arrayDataAsObjectList; if (key != null) dictionary[key] = obj; } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonWebKeySetSerializationTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonWebKeySetSerializationTests.cs index 8b95dff9a1..6832242b45 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonWebKeySetSerializationTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonWebKeySetSerializationTests.cs @@ -36,8 +36,6 @@ public void Serialize(JsonWebKeySetTheoryData theoryData) context.Diffs.Add("========================================="); } - // TODO - when a 8.0 target introduced we will need to adjust as JsonSerializer will be able to set collections without setters - // compare our utf8Reader with expected value if (!IdentityComparer.AreEqual(jsonWebKeySetUtf8Reader, theoryData.JsonWebKeySet, context)) { diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs index 2fba074c2c..5a6127c5d6 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Security.Claims; using System.Threading.Tasks; +using System.Text.Json; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json.Linq; @@ -1047,6 +1048,7 @@ public void ClaimSourceAndClaimName() payload.Add(claimNames, JsonClaims.ClaimNamesAsDictionary); payload.Add("iss", Default.Issuer); + string payloadString = payload.SerializeToJson(); JwtSecurityToken jwtToken = new JwtSecurityToken(new JwtHeader(), payload); JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler(); string encodedJwt = jwtHandler.WriteToken(new JwtSecurityToken(new JwtHeader(), payload)); @@ -1073,7 +1075,7 @@ public void ClaimSourceAndClaimName() var cp = jwtHandler.ValidateToken(encodedJwt, validationParameters, out validatedToken); IdentityComparer.AreEqual( cp.FindFirst(typeof(Entity).ToString()), - new Claim(typeof(Entity).ToString(), JsonExtensions.SerializeToJson(Entity.Default), JsonClaimValueTypes.Json, Default.Issuer, Default.Issuer, cp.Identity as ClaimsIdentity ), + new Claim(typeof(Entity).ToString(), JsonSerializer.Serialize(Entity.Default), JsonClaimValueTypes.Json, Default.Issuer, Default.Issuer, cp.Identity as ClaimsIdentity ), context); TestUtilities.AssertFailIfErrors(context.Diffs); } @@ -1105,9 +1107,9 @@ public void RoleClaims() expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Iss, Default.Issuer, ClaimValueTypes.String, Default.Issuer)); expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Aud, Default.Audience, ClaimValueTypes.String, Default.Issuer)); - expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(expire).ToString(), ClaimValueTypes.Integer, Default.Issuer)); - expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(utcNow).ToString(), ClaimValueTypes.Integer, Default.Issuer)); - expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(utcNow).ToString(), ClaimValueTypes.Integer, Default.Issuer)); + expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(expire).ToString(), ClaimValueTypes.Integer32, Default.Issuer)); + expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(utcNow).ToString(), ClaimValueTypes.Integer32, Default.Issuer)); + expectedIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(utcNow).ToString(), ClaimValueTypes.Integer32, Default.Issuer)); CompareContext context = new CompareContext { IgnoreType = true }; IdentityComparer.AreEqual(principal.Claims, expectedIdentity.Claims, context); diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JsonExtensionsTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JsonExtensionsTests.cs deleted file mode 100644 index af4dcb2376..0000000000 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JsonExtensionsTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Newtonsoft.Json; -using Xunit; - -namespace System.IdentityModel.Tokens.Jwt.Tests -{ - public class JsonExtensionsTests - { - [Fact] - public void JsonWithDuplicateNames() - { - try - { - - string json = @"{""tag"":""value1"", ""tag"": ""value2""}"; - var jsonObject = JsonExtensions.DeserializeFromJson(json); - } - catch(Exception ex) - { - Assert.Equal(typeof(ArgumentException), ex.GetType()); - Assert.Contains("Property with the same name already exists on object.", ex.Message); - } - } - - [Fact] - public void MalformedJson() - { - Assert.Throws(() => JsonExtensions.DeserializeFromJson(@"{""tag"":""value""}ABCD")); - } - } -} diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtDerivedTestTypes.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtDerivedTestTypes.cs index 5df8ee0d26..eb3b5c51eb 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtDerivedTestTypes.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtDerivedTestTypes.cs @@ -13,8 +13,6 @@ namespace System.IdentityModel.Tokens.Jwt.Tests /// public class DerivedJwtSecurityToken : JwtSecurityToken { - // TODO - need to add tests for delegates. - public DerivedJwtSecurityToken(string encodedJwt) : base(encodedJwt) { diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtPayloadTest.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtPayloadTest.cs index ec12caba16..7a2e4feac8 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtPayloadTest.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtPayloadTest.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -94,7 +95,7 @@ public void JwtPayloadUnicodeMapping() string json2 = payload.SerializeToJson(); Assert.Equal(json, json2); - JwtPayload retrievePayload = JwtPayload.Deserialize(json); + JwtPayload retrievePayload = new JwtPayload(json); Assert.Equal(retrievePayload.Iss, issuer); json = unicodePayload.Base64UrlEncode(); @@ -165,7 +166,7 @@ public void TestDateTimeClaim() { JwtPayload jwtPayload = new JwtPayload(); var dateTime = new DateTime(2020, 1, 1, 1, 1, 1, 1); - jwtPayload.Add("dateTime", dateTime); + jwtPayload.Add("dateTime", dateTime.ToUniversalTime()); var dateTimeClaim = jwtPayload.Claims.First(); Assert.True(string.Equals(dateTimeClaim.ValueType, ClaimValueTypes.DateTime), "dateTimeClaim.Type != ClaimValueTypes.DateTime"); @@ -182,12 +183,14 @@ public void TestClaimWithLargeExpValue() } [Theory, MemberData(nameof(PayloadDataSet))] - public void RoundTrip(List claims, JwtPayload payloadSetDirect, JwtPayload payloadSetUsingDeserialize) +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public void RoundTrip(string name, List claims, JwtPayload payloadDirect, JwtPayload payloadUsingNewtonsoft) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters { var context = new CompareContext(); var payload = new JwtPayload(claims); - var encodedPayload = payload.SerializeToJson(); - var payloadDeserialized = JwtPayload.Deserialize(encodedPayload); + var payloadAsJson = payload.SerializeToJson(); + var payloadDeserialized = new JwtPayload(payloadAsJson); var instanceContext = new CompareContext { PropertiesToIgnoreWhenComparing = new Dictionary> @@ -196,20 +199,29 @@ public void RoundTrip(List claims, JwtPayload payloadSetDirect, JwtPayloa } }; - IdentityComparer.AreEqual(payload, payloadDeserialized, instanceContext); + IEnumerable payloadDeserializedClaims = payloadDeserialized.Claims; + IEnumerable payloadClaims = payload.Claims; + + if (!IdentityComparer.AreEqual(payload, payloadDeserialized, instanceContext)) + { + instanceContext.AddDiff("payload != payloadUsingNewtonsoft"); + instanceContext.AddDiff("******************************"); + } + context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", nameof(payload), nameof(payloadDeserialized)), instanceContext); instanceContext.Diffs.Clear(); - IdentityComparer.AreEqual(payload, payloadSetDirect, instanceContext); - context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", nameof(payload), nameof(payloadSetDirect)), instanceContext); + IdentityComparer.AreEqual(payload, payloadDirect, instanceContext); + context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", nameof(payload), nameof(payloadDirect)), instanceContext); - instanceContext.Diffs.Clear(); - IdentityComparer.AreEqual(payload, payloadSetUsingDeserialize, instanceContext); - context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", nameof(payload), nameof(payloadSetUsingDeserialize)), instanceContext); + // skipping as Newtonsoft doesn't understand how to work with a JsonElement + //instanceContext.Diffs.Clear(); + //IdentityComparer.AreEqual(payload, payloadUsingNewtonsoft, instanceContext); + //context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", nameof(payload), nameof(payloadUsingNewtonsoft)), instanceContext); instanceContext.Diffs.Clear(); IdentityComparer.AreEqual(payload.Claims, claims, instanceContext); - context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", nameof(payload.Claims), nameof(claims)), instanceContext); + context.Merge(string.Format(CultureInfo.InvariantCulture, "AreEqual({0}, {1})", "payload.Claims", "claims, parameter"), instanceContext); instanceContext.Diffs.Clear(); CheckClaimsTypeParsing(payload.Claims, instanceContext); @@ -222,7 +234,7 @@ public void RoundTrip(List claims, JwtPayload payloadSetDirect, JwtPayloa TestUtilities.AssertFailIfErrors(context); } - public static TheoryData, JwtPayload, JwtPayload> PayloadDataSet + public static TheoryData, JwtPayload, JwtPayload> PayloadDataSet { get { @@ -232,32 +244,34 @@ public static TheoryData, JwtPayload, JwtPayload> PayloadDataSet var longMinValue = long.MinValue.ToString(); var longValue = ((long)int.MaxValue + 100).ToString(); - var dataset = new TheoryData, JwtPayload, JwtPayload>(); + var dataset = new TheoryData, JwtPayload, JwtPayload>(); SetDataSet( + "Test1", new List { new Claim("ClaimValueTypes.String", "ClaimValueTypes.String.Value", ClaimValueTypes.String), - new Claim("ClaimValueTypes.Boolean.true", "true", ClaimValueTypes.Boolean), - new Claim("ClaimValueTypes.Boolean.false", "false", ClaimValueTypes.Boolean), + new Claim("ClaimValueTypes.Boolean.true", "True", ClaimValueTypes.Boolean), + new Claim("ClaimValueTypes.Boolean.false", "False", ClaimValueTypes.Boolean), new Claim("ClaimValueTypes.Double", "123.4", ClaimValueTypes.Double), - new Claim("ClaimValueTypes.int.MaxValue", intMaxValue, ClaimValueTypes.Integer), - new Claim("ClaimValueTypes.int.MinValue", intMinValue, ClaimValueTypes.Integer), + new Claim("ClaimValueTypes.int.MaxValue", intMaxValue, ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes.int.MinValue", intMinValue, ClaimValueTypes.Integer32), new Claim("ClaimValueTypes.long.MaxValue", longMaxValue, ClaimValueTypes.Integer64), new Claim("ClaimValueTypes.long.MinValue", longMinValue, ClaimValueTypes.Integer64), new Claim("ClaimValueTypes.DateTime.IS8061", "2019-11-15T14:31:21.6101326Z", ClaimValueTypes.DateTime), new Claim("ClaimValueTypes.DateTime", "2019-11-15", ClaimValueTypes.String), new Claim("ClaimValueTypes.JsonClaimValueTypes.Json1", @"{""jsonProperty1"":""jsonvalue1""}", JsonClaimValueTypes.Json), new Claim("ClaimValueTypes.JsonClaimValueTypes.Json2", @"{""jsonProperty2"":""jsonvalue2""}", JsonClaimValueTypes.Json), - new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonArray", "1", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonArray", "2", ClaimValueTypes.Integer), + new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonArray", "1", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonArray", "2", ClaimValueTypes.Integer32), }, dataset); SetDataSet( + "Test2", new List { new Claim("aud", "http://test.local/api/", ClaimValueTypes.String, "http://test.local/api/"), - new Claim("exp", "1460647835", ClaimValueTypes.Integer, "http://test.local/api/"), + new Claim("exp", "1460647835", ClaimValueTypes.Integer32, "http://test.local/api/"), new Claim("emailaddress", "user1@contoso.com", ClaimValueTypes.String, "http://test.local/api/"), new Claim("emailaddress", "user2@contoso.com", ClaimValueTypes.String, "http://test.local/api/"), new Claim("name", "user", ClaimValueTypes.String, "http://test.local/api/"), @@ -267,28 +281,31 @@ public static TheoryData, JwtPayload, JwtPayload> PayloadDataSet dataset); SetDataSet( + "Test3", new List { - new Claim("ClaimValueTypes", "0", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes", "100", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes", "132", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes", "164", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes", "-100", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes", "-132", ClaimValueTypes.Integer), - new Claim("ClaimValueTypes", "-164", ClaimValueTypes.Integer), + new Claim("ClaimValueTypes", "0", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes", "100", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes", "132", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes", "164", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes", "-100", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes", "-132", ClaimValueTypes.Integer32), + new Claim("ClaimValueTypes", "-164", ClaimValueTypes.Integer32), new Claim("ClaimValueTypes", longValue, ClaimValueTypes.Integer64), new Claim("ClaimValueTypes", "132.64", ClaimValueTypes.Double), new Claim("ClaimValueTypes", "-132.64", ClaimValueTypes.Double), - new Claim("ClaimValueTypes", "true", ClaimValueTypes.Boolean), - new Claim("ClaimValueTypes", "false", ClaimValueTypes.Boolean), + new Claim("ClaimValueTypes", "True", ClaimValueTypes.Boolean), + new Claim("ClaimValueTypes", "False", ClaimValueTypes.Boolean), new Claim("ClaimValueTypes", "2019-11-15T14:31:21.6101326Z", ClaimValueTypes.DateTime), new Claim("ClaimValueTypes", "2019-11-15", ClaimValueTypes.String), new Claim("ClaimValueTypes", @"{""name3.1"":""value3.1""}", JsonClaimValueTypes.Json), - new Claim("ClaimValueTypes", @"[""status"",""feed""]", JsonClaimValueTypes.JsonArray), + new Claim("ClaimValueTypes", "status", ClaimValueTypes.String), + new Claim("ClaimValueTypes", "feed", ClaimValueTypes.String), }, dataset); SetDataSet( + "Test4", new List { new Claim("json3", @"{""name3.1"":""value3.1""}", JsonClaimValueTypes.Json), @@ -307,7 +324,7 @@ public static TheoryData, JwtPayload, JwtPayload> PayloadDataSet } } - private static void SetDataSet(List claims, TheoryData, JwtPayload, JwtPayload> dataset) + private static void SetDataSet(string name, List claims, TheoryData, JwtPayload, JwtPayload> dataset) { var payloadDirect = new JwtPayload(); var jobj = new JObject(); @@ -339,15 +356,15 @@ private static void SetDataSet(List claims, TheoryData, JwtPa break; case ClaimValueTypes.DateTime: - jsonValue = DateTime.Parse(claim.Value); + jsonValue = DateTime.Parse(claim.Value).ToUniversalTime(); break; case JsonClaimValueTypes.Json: - jsonValue = JObject.Parse(claim.Value); + jsonValue = JsonSerializerPrimitives.CreateJsonElement(claim.Value); break; case JsonClaimValueTypes.JsonArray: - jsonValue = JArray.Parse(claim.Value); + jsonValue = JsonSerializerPrimitives.CreateJsonElement(claim.Value); break; } @@ -389,8 +406,9 @@ private static void SetDataSet(List claims, TheoryData, JwtPa } var j = jobj.ToString(Formatting.None); - var payloadDeserialized = JwtPayload.Deserialize(j); - dataset.Add(claims, payloadDirect, payloadDeserialized); + var payloadUsingNewtonsoft = JwtPayload.Deserialize(j); + + dataset.Add(name, claims, payloadDirect, payloadUsingNewtonsoft); } private void CheckClaimsTypeParsing(IEnumerable claims, CompareContext context) @@ -447,6 +465,7 @@ private void CheckClaimsTypeParsing(IEnumerable claims, CompareContext co case JsonClaimValueTypes.Json: try { + object obj = Text.Json.JsonSerializer.Deserialize(claim.Value); JObject.Parse(claim.Value); } catch (Exception ex) @@ -460,6 +479,7 @@ private void CheckClaimsTypeParsing(IEnumerable claims, CompareContext co try { JArray.Parse(claim.Value); + object obj = Text.Json.JsonSerializer.Deserialize>(claim.Value); } catch (Exception ex) { diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs index 1b9a1d923f..f591bda0ab 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs @@ -227,7 +227,7 @@ public static TheoryData CreateJWEWithPayloadStringTheory [Theory, MemberData(nameof(CreateJWEUsingSecurityTokenDescriptorTheoryData))] public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryData) { - var context = TestUtilities.WriteHeader($"{this}.CreateJWEUsingSecurityTokenDescriptor", theoryData); + CompareContext context = TestUtilities.WriteHeader($"{this}.CreateJWEUsingSecurityTokenDescriptor", theoryData); theoryData.ValidationParameters.ValidateLifetime = false; try { @@ -236,22 +236,36 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa string jweFromJsonHandler = theoryData.JsonWebTokenHandler.CreateToken(theoryData.TokenDescriptor); - var claimsPrincipal = theoryData.JwtSecurityTokenHandler.ValidateToken(tokenFromTokenDescriptor, theoryData.ValidationParameters, out SecurityToken validatedTokenFromJwtHandler); - var validationResult = theoryData.JsonWebTokenHandler.ValidateToken(jweFromJsonHandler, theoryData.ValidationParameters); + var claimsPrincipalJwt = theoryData.JwtSecurityTokenHandler.ValidateToken(tokenFromTokenDescriptor, theoryData.ValidationParameters, out SecurityToken validatedTokenFromJwtHandler); + var validationResultJson = theoryData.JsonWebTokenHandler.ValidateToken(jweFromJsonHandler, theoryData.ValidationParameters); - if (validationResult.Exception != null && validationResult.IsValid) - context.Diffs.Add("validationResult.IsValid, validationResult.Exception != null"); + if (validationResultJson.Exception != null && validationResultJson.IsValid) + context.Diffs.Add("validationResultJson.IsValid, validationResultJson.Exception != null"); - IdentityComparer.AreEqual(validationResult.IsValid, theoryData.IsValid, context); - var validatedTokenFromJsonHandler = validationResult.SecurityToken; + IdentityComparer.AreEqual(validationResultJson.IsValid, theoryData.IsValid, context); + + var validatedTokenFromJsonHandler = validationResultJson.SecurityToken; var validationResult2 = theoryData.JsonWebTokenHandler.ValidateToken(tokenFromTokenDescriptor, theoryData.ValidationParameters); if (validationResult2.Exception != null && validationResult2.IsValid) + { context.Diffs.Add("validationResult2.IsValid, validationResult2.Exception != null"); + context.AddDiff("****************************************************************"); + } IdentityComparer.AreEqual(validationResult2.IsValid, theoryData.IsValid, context); - IdentityComparer.AreEqual(claimsPrincipal.Identity, validationResult.ClaimsIdentity, context); - IdentityComparer.AreEqual((validatedTokenFromJwtHandler as JwtSecurityToken).Claims, (validatedTokenFromJsonHandler as JsonWebToken).Claims, context); + + if (!IdentityComparer.AreEqual(claimsPrincipalJwt.Identity, validationResultJson.ClaimsIdentity, context)) + { + context.Diffs.Add("claimsPrincipalJwt.Identity != validationResultJson.ClaimsIdentity"); + context.Diffs.Add("*******************************************************************"); + } + + if (!IdentityComparer.AreEqual((validatedTokenFromJwtHandler as JwtSecurityToken).Claims, (validatedTokenFromJsonHandler as JsonWebToken).Claims, context)) + { + context.AddDiff("validatedTokenFromJwtHandler as JwtSecurityToken).Claims != (validatedTokenFromJsonHandler as JsonWebToken).Claims"); + context.AddDiff("******************************************************************************************************************"); + } theoryData.ExpectedException.ProcessNoException(context); context.PropertiesToIgnoreWhenComparing = new Dictionary> @@ -259,7 +273,7 @@ public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryDa { typeof(JsonWebToken), new List { "EncodedToken", "AuthenticationTag", "Ciphertext", "InitializationVector" } }, }; - IdentityComparer.AreEqual(validationResult2.SecurityToken as JwtSecurityToken, validationResult.SecurityToken as JwtSecurityToken, context); + IdentityComparer.AreEqual(validationResult2.SecurityToken as JwtSecurityToken, validationResultJson.SecurityToken as JwtSecurityToken, context); theoryData.ExpectedException.ProcessNoException(context); } catch (Exception ex) @@ -1080,7 +1094,7 @@ public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData) var claimsPrincipal = handler.ValidateToken(theoryData.JWECompressionString, theoryData.ValidationParameters, out var validatedToken); if (!claimsPrincipal.Claims.Any()) - context.Diffs.Add("claimsPrincipal.Claims is empty."); + context.Diffs.Add("claimsPrincipalJwt.Claims is empty."); theoryData.ExpectedException.ProcessNoException(context); } @@ -2056,7 +2070,7 @@ public void ValidateJWSWithConfig(JwtTheoryData theoryData) AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = theoryData.ValidationParameters.ConfigurationManager; new JwtSecurityTokenHandler().ValidateToken(theoryData.Token, theoryData.ValidationParameters, out _); if (theoryData.ShouldSetLastKnownConfiguration && theoryData.ValidationParameters.ConfigurationManager.LastKnownGoodConfiguration == null) - context.AddDiff("validationResult.IsValid, but the configuration was not set as the LastKnownGoodConfiguration"); + context.AddDiff("validationResultJson.IsValid, but the configuration was not set as the LastKnownGoodConfiguration"); theoryData.ExpectedException.ProcessNoException(context); } catch (Exception ex) diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestData.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestData.cs index 9811f74de4..30e73a3ce9 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestData.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtTestData.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.IdentityModel.TestUtils; using Newtonsoft.Json; +using System.Text.Json; using Xunit; namespace System.IdentityModel.Tokens.Jwt.Tests @@ -316,8 +317,8 @@ public static TheoryData InvalidEncodedSegmentsData(string errorS CanRead = true, TestId = nameof(EncodedJwts.InvalidPayload), Token = EncodedJwts.InvalidPayload, - ExpectedException = ExpectedException.ArgumentException(substringExpected: "IDX12723:", inner: typeof(JsonReaderException)) - }); + ExpectedException = new ExpectedException(typeof(ArgumentException), "IDX12723:", null, true) + }); ; return theoryData; }