Skip to content

Commit

Permalink
Added 'typ' header claim validation (#1275)
Browse files Browse the repository at this point in the history
  • Loading branch information
mafurman authored Oct 18, 2019
1 parent affefe0 commit cff3250
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 8 deletions.
26 changes: 24 additions & 2 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ private JObject CreateDefaultJWSHeader(SigningCredentials signingCredentials)
/// Creates an unsigned JWS (Json Web Signature).
/// </summary>
/// <param name="payload">A string containing JSON which represents the JWT token payload.</param>
/// <exception cref="ArgumentNullException">if <paramref name="payload"/> is null.</exception>
/// <returns>A JWS in Compact Serialization Format.</returns>
public virtual string CreateToken(string payload)
{
Expand All @@ -166,6 +167,25 @@ public virtual string CreateToken(string payload)
return CreateTokenPrivate(JObject.Parse(payload), null, null, null, null);
}

/// <summary>
/// Creates an unsigned JWS (Json Web Signature).
/// </summary>
/// <param name="payload">A string containing JSON which represents the JWT token payload.</param>
/// <param name="additionalHeaderClaims">Defines the dictionary containing any custom header claims that need to be added to the JWT token header.</param>
/// <exception cref="ArgumentNullException">if <paramref name="payload"/> is null.</exception>
/// <exception cref="ArgumentNullException">if <paramref name="additionalHeaderClaims"/> is null.</exception>
/// <returns>A JWS in Compact Serialization Format.</returns>
public virtual string CreateToken(string payload, IDictionary<string, object> additionalHeaderClaims)
{
if (string.IsNullOrEmpty(payload))
throw LogHelper.LogArgumentNullException(nameof(payload));

if (additionalHeaderClaims == null)
throw LogHelper.LogArgumentNullException(nameof(additionalHeaderClaims));

return CreateTokenPrivate(JObject.Parse(payload), null, null, null, additionalHeaderClaims);
}

/// <summary>
/// Creates a JWS (Json Web Signature).
/// </summary>
Expand Down Expand Up @@ -222,10 +242,10 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor)
if (tokenDescriptor == null)
throw LogHelper.LogArgumentNullException(nameof(tokenDescriptor));

if ((tokenDescriptor.Subject == null || !tokenDescriptor.Subject.Claims.Any())
if ((tokenDescriptor.Subject == null || !tokenDescriptor.Subject.Claims.Any())
&& (tokenDescriptor.Claims == null || !tokenDescriptor.Claims.Any()))
LogHelper.LogWarning(LogMessages.IDX14114, nameof(SecurityTokenDescriptor), nameof(SecurityTokenDescriptor.Subject), nameof(SecurityTokenDescriptor.Claims));

JObject payload;
if (tokenDescriptor.Subject != null)
payload = JObject.FromObject(JwtTokenUtilities.CreateDictionaryFromClaims(tokenDescriptor.Subject.Claims));
Expand Down Expand Up @@ -1120,6 +1140,8 @@ private TokenValidationResult ValidateTokenPayload(JsonWebToken jsonWebToken, To
ValidateToken(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters);
}
Validators.ValidateIssuerSecurityKey(jsonWebToken.SigningKey, jsonWebToken, validationParameters);

JwtTokenUtilities.ValidateTokenType(jsonWebToken.Typ, validationParameters);

return new TokenValidationResult
{
Expand Down
35 changes: 35 additions & 0 deletions src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
Expand Down Expand Up @@ -322,5 +323,39 @@ private static long ParseTimeValue(JToken jToken, string claimName)

throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX14300, claimName, jToken.ToString(), typeof(long))));
}

/// <summary>
/// Validates the 'typ' claim of the JWT token header.
/// </summary>
/// <param name="type">The value of the 'typ' header claim."/></param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <exception cref="ArgumentNullException">If <paramref name="validationParameters"/> is null or whitespace.</exception>
/// <exception cref="SecurityTokenInvalidTypeException">If <paramref name="type"/> is null or whitespace and <see cref="TokenValidationParameters.ValidTypes"/> is not null.</exception>
/// <exception cref="SecurityTokenInvalidTypeException">If <paramref name="type"/> failed to match <see cref="TokenValidationParameters.ValidTypes"/>.</exception>
/// <remarks>An EXACT match is required. <see cref="StringComparison.Ordinal"/> (case sensitive) is used for comparing <paramref name="type"/> against <see cref="TokenValidationParameters.ValidTypes"/>.</remarks>
internal static void ValidateTokenType(string type, TokenValidationParameters validationParameters)
{
if (validationParameters == null)
throw LogHelper.LogArgumentNullException(nameof(validationParameters));

if (validationParameters.ValidTypes == null || validationParameters.ValidTypes.Count() == 0)
{
LogHelper.LogInformation(TokenLogMessages.IDX10254);
return;
}

if (string.IsNullOrEmpty(type))
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidTypeException(TokenLogMessages.IDX10256) { InvalidType = null });

if (!validationParameters.ValidTypes.Contains(type, StringComparer.Ordinal))
{
throw LogHelper.LogExceptionMessage(
new SecurityTokenInvalidTypeException(LogHelper.FormatInvariant(TokenLogMessages.IDX10257, type, Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidTypes)))
{ InvalidType = type }); ;
}

// if it reaches here, token type was succcessfully validated.
LogHelper.LogInformation(TokenLogMessages.IDX10258, type);
}
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal static class LogMessages
internal const string IDX14114 = "IDX14114: Both '{0}.{1}' and '{0}.{2}' are null or empty.";
// internal const string IDX14115 = "IDX14115:";
internal const string IDX14116 = "IDX14116: ''{0}' cannot contain the following claims: '{1}'. These values are added by default (if necessary) during security token creation.";

// logging
internal const string IDX14200 = "IDX14200: Creating raw signature using the signature credentials.";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//------------------------------------------------------------------------------

using System;

namespace Microsoft.IdentityModel.Tokens
{
#if DESKTOPNET45
[Serializable]
#endif
/// <summary>
/// This exception is thrown when the token type ('typ' header claim) of a JWT token is invalid.
/// </summary>
public class SecurityTokenInvalidTypeException : SecurityTokenValidationException
{
/// <summary>
/// Gets or sets the invalid type that created the validation exception.
/// </summary>
public string InvalidType { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidTypeException"/> class.
/// </summary>
public SecurityTokenInvalidTypeException()
: base()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidTypeException"/> class.
/// </summary>
/// <param name="message">Additional information to be included in the exception and displayed to user.</param>
public SecurityTokenInvalidTypeException(string message)
: base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidTypeException"/> class.
/// </summary>
/// <param name="message">Additional information to be included in the exception and displayed to user.</param>
/// <param name="innerException">A <see cref="Exception"/> that represents the root cause of the exception.</param>
public SecurityTokenInvalidTypeException(string message, Exception innerException)
: base(message, innerException)
{
}

#if DESKTOPNET45
/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidTypeException"/> class.
/// </summary>
/// <param name="info">the <see cref="SerializationInfo"/> that holds the serialized object data.</param>
/// <param name="context">The contextual information about the source or destination.</param>
protected SecurityTokenInvalidTypeException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endif

}
}
4 changes: 4 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ internal static class LogMessages
public const string IDX10252 = "IDX10252: RequireSignedTokens property on ValidationParameters is set to false and the issuer signing key is null. Exiting without validating the issuer signing key.";
public const string IDX10253 = "IDX10253: RequireSignedTokens property on ValidationParameters is set to true, but the issuer signing key is null.";
public const string IDX10254 = "IDX10254: '{0}.{1}' failed. The virtual method '{2}.{3}' returned null. If this method was overridden, ensure a valid '{4}' is returned.";
public const string IDX10255 = "IDX10255: ValidTypes property on ValidationParameters is either null or empty. Exiting without validating the token type.";
public const string IDX10256 = "IDX10256: Unable to validate the token type. TokenValidationParameters.ValidTypes is set, but the 'typ' header claim is null or empty.";
public const string IDX10257 = "IDX10257: Token type validation failed. Type: '{0}'. Did not match: validationParameters.TokenTypes: '{1}'.";
public const string IDX10258 = "IDX10258: Token type validated. Type: '{0}'.";

// 10500 - SignatureValidation
public const string IDX10500 = "IDX10500: Signature validation failed. No security keys were provided to validate the signature.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class SecurityTokenDescriptor

/// <summary>
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the JWT token header.
/// The 'alg', 'kid', 'typ', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
/// will result in an exception being thrown.
/// <remarks> These claims are only added to the outer header (in case of a JWE).</remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,5 +571,12 @@ public string RoleClaimType
/// Gets or sets the <see cref="IEnumerable{String}"/> that contains valid issuers that will be used to check against the token's issuer.
/// </summary>
public IEnumerable<string> ValidIssuers { get; set; }

/// <summary>
/// Gets or sets the <see cref="IEnumerable{String}"/> that contains valid types that will be used to check against the JWT header's 'typ' claim.
/// If this property is not set, the 'typ' header claim will not be validated and all types will be accepted.
/// In the case of a JWE, this property will ONLY apply to the inner token header.
/// </summary>
public IEnumerable<string> ValidTypes { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,9 @@ protected ClaimsPrincipal ValidateTokenPayload(JwtSecurityToken jwtToken, TokenV
ValidateToken(jwtToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters, out _);
}
ValidateIssuerSecurityKey(jwtToken.SigningKey, jwtToken, validationParameters);

JwtTokenUtilities.ValidateTokenType(jwtToken.Header.Typ, validationParameters);

var identity = CreateClaimsIdentity(jwtToken, issuer, validationParameters);
if (validationParameters.SaveSigninToken)
identity.BootstrapContext = jwtToken.RawData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,22 @@ public void ValidateTokenClaims()
throw new SecurityTokenException("Token does not contain the correct value for the 'email' claim.");
}


[Theory, MemberData(nameof(ValidateTypeTheoryData))]
public void ValidateType(JwtTheoryData theoryData)
{
TestUtilities.WriteHeader($"{this}.ValidateType", theoryData);

var tokenValidationResult = new JsonWebTokenHandler().ValidateToken(theoryData.Token, theoryData.ValidationParameters);
if (tokenValidationResult.Exception != null)
theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception);
else
theoryData.ExpectedException.ProcessNoException();

}

public static TheoryData<JwtTheoryData> ValidateTypeTheoryData = JwtSecurityTokenHandlerTests.ValidateTypeTheoryData;

[Theory, MemberData(nameof(ValidateJwsTheoryData))]
public void ValidateJWS(JwtTheoryData theoryData)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public void Publics()
TokenValidationParameters validationParameters = new TokenValidationParameters();
Type type = typeof(TokenValidationParameters);
PropertyInfo[] properties = type.GetProperties();
if (properties.Length != 37)
Assert.True(false, "Number of properties has changed from 37 to: " + properties.Length + ", adjust tests");
if (properties.Length != 38)
Assert.True(false, "Number of properties has changed from 38 to: " + properties.Length + ", adjust tests");

TokenValidationParameters actorValidationParameters = new TokenValidationParameters();
SecurityKey issuerSigningKey = KeyingMaterial.DefaultX509Key_2048_Public;
Expand Down Expand Up @@ -144,8 +144,8 @@ public void GetSets()
TokenValidationParameters validationParameters = new TokenValidationParameters();
Type type = typeof(TokenValidationParameters);
PropertyInfo[] properties = type.GetProperties();
if (properties.Length != 37)
Assert.True(false, "Number of public fields has changed from 37 to: " + properties.Length + ", adjust tests");
if (properties.Length != 38)
Assert.True(false, "Number of public fields has changed from 38 to: " + properties.Length + ", adjust tests");

GetSetContext context =
new GetSetContext
Expand Down
Loading

0 comments on commit cff3250

Please sign in to comment.