From e4f8a0c3cd1d7d0e62a2e48be7bdc91a7fdddb24 Mon Sep 17 00:00:00 2001 From: Roja Ennam Date: Thu, 14 May 2020 12:18:07 -0700 Subject: [PATCH] Support for SecurityTokenDescriptor.Claims in JwtSecurity/Saml/Saml2 Tokens --- .../JwtTokenUtilities.cs | 7 +- .../Saml/LogMessages.cs | 1 + .../Saml/SamlAttribute.cs | 6 +- .../Saml/SamlSecurityTokenHandler.cs | 15 +- .../{ => Saml}/SamlTokenUtilities.cs | 98 +++++++- .../Saml2/Saml2SecurityTokenHandler.cs | 35 +-- .../LogMessages.cs | 2 + .../TokenUtilities.cs | 30 ++- .../JwtPayload.cs | 56 ++++- .../JwtSecurityTokenHandler.cs | 85 ++++++- .../Default.cs | 62 +++++ .../Saml2SecurityTokenHandlerTests.cs | 216 ++++++++++++++++- .../SamlSecurityTokenHandlerTests.cs | 220 +++++++++++++++++- .../JwtSecurityTokenHandlerTests.cs | 178 ++++++++++++++ 14 files changed, 962 insertions(+), 49 deletions(-) rename src/Microsoft.IdentityModel.Tokens.Saml/{ => Saml}/SamlTokenUtilities.cs (57%) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs index d73dd22fe4..9f75d762c7 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs @@ -65,7 +65,12 @@ public class JwtTokenUtilities JwtHeaderParameterNames.Zip }; - internal static Dictionary CreateDictionaryFromClaims(IEnumerable claims) + /// + /// Creates a dictionary from a list of Claim's. + /// + /// A list of claims. + /// A Dictionary representing claims. + internal static IDictionary CreateDictionaryFromClaims(IEnumerable claims) { var payload = new Dictionary(); diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs index b559bc109e..33b2bea13c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs @@ -43,6 +43,7 @@ internal static class LogMessages // signature creation / validation internal const string IDX11312 = "IDX11312: Unable to validate token. A SamlSamlAttributeStatement can only have one SamlAttribute of type 'Actor'. This special SamlAttribute is used in delegation scenarios."; internal const string IDX11313 = "IDX11313: Unable to process Saml attribute. A SamlSubject must contain either or both of Name and ConfirmationMethod."; + internal const string IDX11314 = "IDX11314: The AttributeValueXsiType of a SAML Attribute must be a string of the form 'prefix#suffix', where prefix and suffix are non-empty strings. Found: '{0}'"; // SamlSerializer reading internal const string IDX11100 = "IDX11100: Saml Only one element of type '{0}' is supported."; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlAttribute.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlAttribute.cs index 3e2eef525a..6eb9f56da1 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlAttribute.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlAttribute.cs @@ -91,16 +91,16 @@ public string AttributeValueXsiType int indexOfHash = value.IndexOf('#'); if (indexOfHash == -1) - throw LogExceptionMessage(new SecurityTokenInvalidAudienceException("value, SR.GetString(SR.ID4254)")); ; + throw LogExceptionMessage(new SecurityTokenInvalidAudienceException(FormatInvariant(LogMessages.IDX11314, value))); string prefix = value.Substring(0, indexOfHash); if (prefix.Length == 0) - throw LogExceptionMessage(new ArgumentException("value SR.GetString(SR.ID4254)")); + throw LogExceptionMessage(new ArgumentException(FormatInvariant(LogMessages.IDX11314, value))); string suffix = value.Substring(indexOfHash + 1); if (suffix.Length == 0) { - throw LogExceptionMessage(new ArgumentException("value, SR.GetString(SR.ID4254)")); + throw LogExceptionMessage(new ArgumentException(FormatInvariant(LogMessages.IDX11314, value))); } _attributeValueXsiType = value; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index a68bdfe25e..8a7b28e601 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -270,10 +270,12 @@ protected virtual SamlAttributeStatement CreateAttributeStatement(SamlSubject su if (tokenDescriptor == null) throw LogArgumentNullException(nameof(tokenDescriptor)); - if (tokenDescriptor.Subject != null) + IEnumerable claims = SamlTokenUtilities.GetAllClaims(tokenDescriptor.Claims, tokenDescriptor.Subject != null ? tokenDescriptor.Subject.Claims : null); + + if (claims != null && claims.Any()) { var attributes = new List(); - foreach (var claim in tokenDescriptor.Subject.Claims) + foreach (var claim in claims) { if (claim != null && claim.Type != ClaimTypes.NameIdentifier) { @@ -293,7 +295,7 @@ protected virtual SamlAttributeStatement CreateAttributeStatement(SamlSubject su } } - AddActorToAttributes(attributes, tokenDescriptor.Subject.Actor); + AddActorToAttributes(attributes, tokenDescriptor.Subject?.Actor); var consolidatedAttributes = ConsolidateAttributes(attributes); if (consolidatedAttributes.Count > 0) @@ -450,9 +452,12 @@ protected virtual SamlSubject CreateSubject(SecurityTokenDescriptor tokenDescrip var samlSubject = new SamlSubject(); Claim identityClaim = null; - if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Claims != null) + + IEnumerable claims = SamlTokenUtilities.GetAllClaims(tokenDescriptor.Claims, tokenDescriptor.Subject != null ? tokenDescriptor.Subject.Claims : null); + + if (claims != null && claims.Any()) { - foreach (var claim in tokenDescriptor.Subject.Claims) + foreach (var claim in claims) { if (claim.Type == ClaimTypes.NameIdentifier) { diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/SamlTokenUtilities.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs similarity index 57% rename from src/Microsoft.IdentityModel.Tokens.Saml/SamlTokenUtilities.cs rename to src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs index f00c169419..34c7442dbd 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/SamlTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs @@ -1,5 +1,4 @@ -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Xml; + //------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. @@ -27,11 +26,12 @@ // //------------------------------------------------------------------------------ -using System; +using System.Security.Claims; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.IdentityModel.Xml; +using System; +using Microsoft.IdentityModel.Logging; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace Microsoft.IdentityModel.Tokens.Saml @@ -39,7 +39,7 @@ namespace Microsoft.IdentityModel.Tokens.Saml /// /// A class which contains useful methods for processing saml tokens. /// - public class SamlTokenUtilities + internal class SamlTokenUtilities { /// /// Returns a to use when validating the signature of a token. @@ -102,5 +102,89 @@ internal static IEnumerable GetKeysForTokenSignatureValidation(stri } return null; } + + /// + /// Creates 's from . + /// + /// A dictionary that represents a set of claims. + /// A collection of 's created from the . + internal static IEnumerable CreateClaimsFromDictionary(IDictionary claimsCollection) + { + if (claimsCollection == null) + return null; + + var claims = new List(); + foreach (var claim in claimsCollection) + { + string claimType = claim.Key; + object claimValue = claim.Value; + if (claimValue != null) + { + var valueType = GetXsiTypeForValue(claimValue); + if (valueType == null && claimValue is IEnumerable claimList) + { + foreach (var item in claimList) + { + valueType = GetXsiTypeForValue(item); + if (valueType == null && item is IEnumerable) + throw new NotSupportedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10105, claimType)); + + claims.Add(new Claim(claimType, item.ToString(), valueType)); + } + } + else + { + claims.Add(new Claim(claimType, claimValue.ToString(), valueType)); + } + } + } + + return claims; + } + + /// + /// Merges and + /// + /// A dictionary of claims. + /// A collection of 's + /// A merged list of 's. + internal static IEnumerable GetAllClaims(IDictionary claims, IEnumerable subjectClaims) + { + if (claims == null) + return subjectClaims; + else + return TokenUtilities.MergeClaims(CreateClaimsFromDictionary(claims), subjectClaims); + } + + /// + /// Gets the value type of the from its value + /// + /// The value. + /// The value type of the . + internal static string GetXsiTypeForValue(object value) + { + if (value != null) + { + if (value is string) + return ClaimValueTypes.String; + + if (value is bool) + return ClaimValueTypes.Boolean; + + if (value is int) + return ClaimValueTypes.Integer32; + + if (value is long) + return ClaimValueTypes.Integer64; + + if (value is double) + return ClaimValueTypes.Double; + + if (value is DateTime) + return ClaimValueTypes.DateTime; + } + + return null; + } } } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs index dfc45c3ab4..20bc17c560 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs @@ -29,6 +29,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using System.Linq; using System.Security.Claims; using System.Text; using System.Xml; @@ -681,20 +682,26 @@ protected virtual Saml2AttributeStatement CreateAttributeStatement(SecurityToken throw LogArgumentNullException(nameof(tokenDescriptor.Subject)); var attributes = new List(); - foreach (Claim claim in tokenDescriptor.Subject.Claims) + + IEnumerable claims = SamlTokenUtilities.GetAllClaims(tokenDescriptor.Claims, tokenDescriptor.Subject != null ? tokenDescriptor.Subject.Claims : null); + + if (claims != null && claims.Any()) { - if (claim != null) + foreach (Claim claim in claims) { - switch (claim.Type) + if (claim != null) { - // TODO - should these really be filtered? - case ClaimTypes.AuthenticationInstant: - case ClaimTypes.AuthenticationMethod: - case ClaimTypes.NameIdentifier: - break; - default: - attributes.Add(CreateAttribute(claim)); - break; + switch (claim.Type) + { + // TODO - should these really be filtered? + case ClaimTypes.AuthenticationInstant: + case ClaimTypes.AuthenticationMethod: + case ClaimTypes.NameIdentifier: + break; + default: + attributes.Add(CreateAttribute(claim)); + break; + } } } } @@ -895,9 +902,11 @@ protected virtual Saml2Subject CreateSubject(SecurityTokenDescriptor tokenDescri string nameIdentifierSpProviderId = null; string nameIdentifierSpNameQualifier = null; - if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Claims != null) + IEnumerable claims = SamlTokenUtilities.GetAllClaims(tokenDescriptor.Claims, tokenDescriptor.Subject != null ? tokenDescriptor.Subject.Claims : null); + + if (claims != null && claims.Any()) { - foreach (var claim in tokenDescriptor.Subject.Claims) + foreach (var claim in claims) { if (claim.Type == ClaimTypes.NameIdentifier) { diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index a8bd1f0472..e17d72aa30 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -45,6 +45,7 @@ internal static class LogMessages public const string IDX10102 = "IDX10102: NameClaimType cannot be null or whitespace."; public const string IDX10103 = "IDX10103: RoleClaimType cannot be null or whitespace."; public const string IDX10104 = "IDX10104: TokenLifetimeInMinutes must be greater than zero. value: '{0}'"; + public const string IDX10105 = "IDX10105: ClaimValue that is a collection of collections is not supported. Such ClaimValue is found for ClaimType : '{0}'"; // token validation public const string IDX10204 = "IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null."; @@ -227,6 +228,7 @@ internal static class LogMessages public const string IDX10812 = "IDX10812: Unable to create a {0} from the properties found in the JsonWebKey: '{1}'."; public const string IDX10813 = "IDX10813: Unable to create a {0} from the properties found in the JsonWebKey: '{1}', Exception '{2}'."; + #pragma warning restore 1591 } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs b/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs index cb2274e960..414aecc182 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs @@ -28,8 +28,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Security.Claims; using Microsoft.IdentityModel.Logging; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; @@ -38,7 +37,7 @@ namespace Microsoft.IdentityModel.Tokens /// /// A class which contains useful methods for processing tokens. /// - public class TokenUtilities + internal class TokenUtilities { /// /// Returns all provided in validationParameters. @@ -55,5 +54,30 @@ internal static IEnumerable GetAllSigningKeys(TokenValidationParame foreach (SecurityKey key in validationParameters.IssuerSigningKeys) yield return key; } + + /// + /// Merges claims. If a claim with same type exists in both and , the one in claims will be kept. + /// + /// Collection of 's. + /// Collection of 's. + /// A Merged list of 's. + internal static IEnumerable MergeClaims(IEnumerable claims, IEnumerable subjectClaims) + { + if (claims == null) + return subjectClaims; + + if (subjectClaims == null) + return claims; + + List result = claims.ToList(); + + foreach (Claim claim in subjectClaims) + { + if (!claims.Any(i => i.Type == claim.Type)) + result.Add(claim); + } + + return result; + } } } diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs b/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs index 12f650ac9a..eedebf1387 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Security.Claims; using Microsoft.IdentityModel.Json; using Microsoft.IdentityModel.Json.Linq; @@ -83,7 +84,7 @@ public JwtPayload(string issuer, string audience, IEnumerable claims, Dat /// If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present. /// If issuedAt.HasValue is 'true' a { iat, 'value' } claim is added, overwriting any 'iat' claim in 'claims' if present. /// Comparison is set to - /// The 4 parameters: 'issuer', 'audience', 'notBefore', 'expires' take precednece over (s) in 'claims'. The values in 'claims' will be overridden. + /// The 4 parameters: 'issuer', 'audience', 'notBefore', 'expires' take precedence over (s) in 'claims'. The values will be overridden. /// If 'expires' <= 'notbefore'. public JwtPayload(string issuer, string audience, IEnumerable claims, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) : base(StringComparer.Ordinal) @@ -91,6 +92,45 @@ public JwtPayload(string issuer, string audience, IEnumerable claims, Dat if (claims != null) AddClaims(claims); + AddFirstPriorityClaims(issuer, audience, notBefore, expires, issuedAt); + } + + /// + /// Initializes a new instance of the class with claims added for each parameter specified. Default string comparer . + /// + /// If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' and 'claimCollection' if present. + /// If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' or 'claimCollection' if present. + /// If this value is not null then for each a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List<object> } will be created to contain the duplicate values. + /// If both and are not null then the values in claims will be combined with the values in claimsCollection. The values found in claimCollection take precedence over those found in claims, so any duplicate + /// values will be overridden. + /// If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' and 'claimcollection' if present. + /// If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' and 'claimcollection' if present. + /// If issuedAt.HasValue is 'true' a { iat, 'value' } claim is added, overwriting any 'iat' claim in 'claims' and 'claimcollection' if present. + /// Comparison is set to + /// The 4 parameters: 'issuer', 'audience', 'notBefore', 'expires' take precedence over (s) in 'claims' and 'claimcollection'. The values will be overridden. + /// If 'expires' <= 'notbefore'. + public JwtPayload(string issuer, string audience, IEnumerable claims, IDictionary claimsCollection, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + : base(StringComparer.Ordinal) + { + if (claims != null) + AddClaims(claims); + + if (claimsCollection != null && claimsCollection.Any()) + AddDictionaryClaims(claimsCollection); + + AddFirstPriorityClaims(issuer, audience, notBefore, expires, issuedAt); + } + + /// + /// Adds Nbf, Exp, Iat, Iss and Aud claims to payload + /// + /// If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in instance. + /// If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in instance. + /// If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in instance. + /// If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in instance. + /// If issuedAt.HasValue is 'true' a { iat, 'value' } claim is added, overwriting any 'iat' claim in instance. + internal void AddFirstPriorityClaims(string issuer, string audience, DateTime? notBefore, DateTime? expires, DateTime? issuedAt) + { if (expires.HasValue) { if (notBefore.HasValue) @@ -510,6 +550,20 @@ 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. + 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]; + } + internal static string GetClaimValueType(object obj) { if (obj == null) diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index b5fbc2221a..55d4a15678 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -359,7 +359,7 @@ public virtual string CreateEncodedJwt(SecurityTokenDescriptor tokenDescriptor) /// A Base64UrlEncoded string in 'Compact Serialization Format'. public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials) { - return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, null).RawData; + return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, null, null).RawData; } /// @@ -382,7 +382,31 @@ public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIde /// If 'expires' <= 'notBefore'. public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials) { - return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials).RawData; + return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, null).RawData; + } + + /// + /// Creates a JWT in 'Compact Serialization Format'. + /// + /// The issuer of the token. + /// The audience for this token. + /// The source of the (s) for this token. + /// Translated into 'epoch time' and assigned to 'nbf'. + /// Translated into 'epoch time' and assigned to 'exp'. + /// Translated into 'epoch time' and assigned to 'iat'. + /// Contains cryptographic material for signing. + /// Contains cryptographic material for encrypting. + /// A collection of (key,value) pairs representing (s) for this token. + /// If is not null, then a claim { actort, 'value' } will be added to the payload. for details on how the value is created. + /// See for details on how the HeaderParameters are added to the header. + /// See for details on how the values are added to the payload. + /// Each in the will map by applying . Modifying could change the outbound JWT. + /// + /// A Base64UrlEncoded string in 'Compact Serialization Format'. + /// If 'expires' <= 'notBefore'. + public virtual string CreateEncodedJwt(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, IDictionary claimCollection) + { + return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, claimCollection).RawData; } /// @@ -403,7 +427,8 @@ public virtual JwtSecurityToken CreateJwtSecurityToken(SecurityTokenDescriptor t tokenDescriptor.Expires, tokenDescriptor.IssuedAt, tokenDescriptor.SigningCredentials, - tokenDescriptor.EncryptingCredentials); + tokenDescriptor.EncryptingCredentials, + tokenDescriptor.Claims); } /// @@ -429,7 +454,34 @@ public virtual JwtSecurityToken CreateJwtSecurityToken(SecurityTokenDescriptor t /// If 'expires' <= 'notBefore'. public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials) { - return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials); + return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, null); + } + + /// + /// Creates a + /// + /// The issuer of the token. + /// The audience for this token. + /// The source of the (s) for this token. + /// The notbefore time for this token. + /// The expiration time for this token. + /// The issue time for this token. + /// Contains cryptographic material for generating a signature. + /// Contains cryptographic material for encrypting the token. + /// A collection of (key,value) pairs representing (s) for this token. + /// If is not null, then a claim { actort, 'value' } will be added to the payload. for details on how the value is created. + /// See for details on how the HeaderParameters are added to the header. + /// See for details on how the values are added to the payload. + /// Each on the added will have translated according to the mapping found in + /// . Adding and removing to will affect the name component of the Json claim. + /// is used to sign . + /// is used to encrypt or . + /// + /// A . + /// If 'expires' <= 'notBefore'. + public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, IDictionary claimCollection) + { + return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, encryptingCredentials, claimCollection); } /// @@ -453,7 +505,7 @@ public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer, string aud /// If 'expires' <= 'notBefore'. public virtual JwtSecurityToken CreateJwtSecurityToken(string issuer = null, string audience = null, ClaimsIdentity subject = null, DateTime? notBefore = null, DateTime? expires = null, DateTime? issuedAt = null, SigningCredentials signingCredentials = null) { - return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, null); + return CreateJwtSecurityTokenPrivate(issuer, audience, subject, notBefore, expires, issuedAt, signingCredentials, null, null); } /// @@ -474,10 +526,11 @@ public override SecurityToken CreateToken(SecurityTokenDescriptor tokenDescripto tokenDescriptor.Expires, tokenDescriptor.IssuedAt, tokenDescriptor.SigningCredentials, - tokenDescriptor.EncryptingCredentials); + tokenDescriptor.EncryptingCredentials, + tokenDescriptor.Claims); } - private JwtSecurityToken CreateJwtSecurityTokenPrivate(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials) + private JwtSecurityToken CreateJwtSecurityTokenPrivate(string issuer, string audience, ClaimsIdentity subject, DateTime? notBefore, DateTime? expires, DateTime? issuedAt, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, IDictionary claimCollection) { if (SetDefaultTimesOnTokenCreation && (!expires.HasValue || !issuedAt.HasValue || !notBefore.HasValue)) { @@ -493,7 +546,7 @@ private JwtSecurityToken CreateJwtSecurityTokenPrivate(string issuer, string aud } LogHelper.LogVerbose(LogMessages.IDX12721, (audience ?? "null"), (issuer ?? "null")); - JwtPayload payload = new JwtPayload(issuer, audience, (subject == null ? null : OutboundClaimTypeTransform(subject.Claims)), notBefore, expires, issuedAt); + JwtPayload payload = new JwtPayload(issuer, audience, (subject == null ? null : OutboundClaimTypeTransform(subject.Claims)), (claimCollection == null ? null : OutboundClaimTypeTransform(claimCollection)), notBefore, expires, issuedAt); JwtHeader header = signingCredentials == null ? new JwtHeader() : new JwtHeader(signingCredentials, OutboundAlgorithmMap); if (subject?.Actor != null) @@ -612,6 +665,22 @@ private IEnumerable OutboundClaimTypeTransform(IEnumerable claims) } } + private IDictionary OutboundClaimTypeTransform(IDictionary claimCollection) + { + var claims = new Dictionary(); + + foreach (string claimType in claimCollection.Keys) + { + if (_outboundClaimTypeMap.TryGetValue(claimType, out string type)) + claims[type] = claimCollection[claimType]; + + else + claims[claimType] = claimCollection[claimType]; + } + + return claims; + } + /// /// Converts a string into an instance of . /// diff --git a/test/Microsoft.IdentityModel.TestUtils/Default.cs b/test/Microsoft.IdentityModel.TestUtils/Default.cs index ccbb46d201..f38266ddfc 100644 --- a/test/Microsoft.IdentityModel.TestUtils/Default.cs +++ b/test/Microsoft.IdentityModel.TestUtils/Default.cs @@ -407,6 +407,50 @@ public static List PayloadClaims }; } + public static List PayloadJsonClaims + { + get => new List + { + 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.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), + new Claim("ClaimValueTypes.JsonClaimValueTypes.Json1", @"{""jsonProperty1"":""jsonvalue1""}", System.IdentityModel.Tokens.Jwt.JsonClaimValueTypes.Json), + new Claim("ClaimValueTypes.JsonClaimValueTypes.Json2", @"{""jsonProperty2"":""jsonvalue2""}", System.IdentityModel.Tokens.Jwt.JsonClaimValueTypes.Json), + new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonNull", "", System.IdentityModel.Tokens.Jwt.JsonClaimValueTypes.JsonNull), + new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonArray1", @"[1,2,3]", System.IdentityModel.Tokens.Jwt.JsonClaimValueTypes.JsonArray), + new Claim("ClaimValueTypes.JsonClaimValueTypes.JsonArray2", @"[1,""2"",3]", System.IdentityModel.Tokens.Jwt.JsonClaimValueTypes.JsonArray), + new Claim("ClaimValueTypes.JsonClaimValueTypes.Integer1", "1", ClaimValueTypes.Integer), + new Claim(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString(), ClaimValueTypes.String, Issuer, Issuer) + }; + } + + public static Dictionary PayloadJsonDictionary + { + get => new Dictionary() + { + { JwtRegisteredClaimNames.Aud, Audience }, + { JwtRegisteredClaimNames.Iss, Issuer }, + { "ClaimValueTypes.String", "ClaimValueTypes.String.Value" }, + { "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", 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""}") }, + { "ClaimValueTypes.JsonClaimValueTypes.JsonNull", "" }, + { "ClaimValueTypes.JsonClaimValueTypes.JsonArray1", JArray.Parse(@"[1,2,3]") }, + { "ClaimValueTypes.JsonClaimValueTypes.JsonArray2", JArray.Parse(@"[1,""2"",3]") }, + { "ClaimValueTypes.JsonClaimValueTypes.Integer1", 1 }, + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires).ToString() } + }; + } + public static ClaimsIdentity PayloadClaimsIdentity { get => new ClaimsIdentity(PayloadClaims, "AuthenticationTypes.Federation"); @@ -625,6 +669,24 @@ public static List SamlClaims }; } + /// + /// SamlClaims require the ability to split into name / namespace + /// + public static Dictionary SamlClaimsDictionary + { + get => new Dictionary() + { + { ClaimTypes.Country, "USA"}, + { ClaimTypes.NameIdentifier, "Bob" }, + { ClaimTypes.Email, "Bob@contoso.com" }, + { ClaimTypes.GivenName, "Bob" }, + { ClaimTypes.HomePhone, "555.1212" }, + { ClaimTypes.Role, new List{"Developer", "Sales" } }, + { ClaimTypes.StreetAddress, "123AnyWhereStreet\r\nSomeTown/r/nUSA" }, + { ClaimsIdentity.DefaultNameClaimType, "Jean-Sébastien" } + }; + } + /// /// SamlClaims require the ability to split into name / namespace /// diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs index ce9aa528fc..70c55d99ad 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs @@ -100,7 +100,7 @@ public void CanReadToken(Saml2TheoryData theoryData) public static TheoryData CanReadTokenTheoryData { - get => new TheoryData + get => new TheoryData { new Saml2TheoryData { @@ -365,7 +365,7 @@ public static TheoryData WriteTokenTheoryData Expires = Default.Expires, Issuer = Default.Issuer, SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), - Subject = new ClaimsIdentity(Default.SamlClaims) + Subject = new ClaimsIdentity(Default.SamlClaims), }; var validationParameters = new TokenValidationParameters @@ -481,7 +481,7 @@ public static TheoryData WriteTokenTheoryData public static TheoryData RoundTripActorTheoryData { - get => new TheoryData + get => new TheoryData { new Saml2TheoryData { @@ -931,7 +931,7 @@ public static TheoryData ValidateTokenTheoryData } }, new Saml2TheoryData - { + { Audiences = new List(), Token = ReferenceTokens.Saml2Token_NoAudienceRestrictions_NoSignature, ExpectedException = new ExpectedException(typeof(Saml2SecurityTokenException), "IDX13002:"), @@ -942,7 +942,7 @@ public static TheoryData ValidateTokenTheoryData ValidAudience = "spn:fe78e0b4-6fe7-47e6-812c-fb75cee266a4", ValidateLifetime = false, ValidateIssuer = false, - RequireSignedTokens = false + RequireSignedTokens = false } }, new Saml2TheoryData @@ -957,7 +957,7 @@ public static TheoryData ValidateTokenTheoryData ValidAudience = "spn:fe78e0b4-6fe7-47e6-812c-fb75cee266a4", ValidateLifetime = false, ValidateIssuer = false, - RequireSignedTokens = false + RequireSignedTokens = false }, }, new Saml2TheoryData @@ -1089,7 +1089,184 @@ public static TheoryData ValidateTokenTheoryData }; } } - } + + [Theory, MemberData(nameof(CreateSaml2TokenUsingTokenDescriptorTheoryData))] + public void CreateSaml2TokenUsingTokenDescriptor(CreateTokenTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.CreateSaml2TokenUsingTokenDescriptor", theoryData); + context.PropertiesToIgnoreWhenComparing = new Dictionary> + { + { typeof(Saml2Assertion), new List { "IssueInstant", "InclusiveNamespacesPrefixList", "Signature", "SigningCredentials", "CanonicalString" } }, + { typeof(Saml2SecurityToken), new List { "SigningKey" } }, + }; + + try + { + SecurityToken samlTokenFromSecurityTokenDescriptor = theoryData.Saml2SecurityTokenHandler.CreateToken(theoryData.TokenDescriptor) as Saml2SecurityToken; + string tokenFromTokenDescriptor = theoryData.Saml2SecurityTokenHandler.WriteToken(samlTokenFromSecurityTokenDescriptor); + + var claimsIdentityFromTokenDescriptor = theoryData.Saml2SecurityTokenHandler.ValidateToken(tokenFromTokenDescriptor, theoryData.ValidationParameters, out SecurityToken validatedTokenFromTokenDescriptor).Identity as ClaimsIdentity; + IdentityComparer.AreEqual(validatedTokenFromTokenDescriptor, samlTokenFromSecurityTokenDescriptor, context); + + theoryData.ExpectedException.ProcessNoException(context); + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData CreateSaml2TokenUsingTokenDescriptorTheoryData + { + get + { + var validationParameters = new TokenValidationParameters + { + AuthenticationType = "Federation", + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + IssuerSigningKey = KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256 + }; + return new TheoryData + { + new CreateTokenTheoryData + { + TestId = "NotSupportedClaimValue", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = new Dictionary() + { + { "https://www.listinlist.com", new List{ new List { "bob", new SecurityTokenDescriptor(), 12, 1.45 }, new List { "bob", new SecurityTokenDescriptor(), 12, 1.45 } } }, + }, + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParameters, + ExpectedException = ExpectedException.NotSupportedException("IDX10105:") + }, + new CreateTokenTheoryData + { + First = true, + TestId = "OnlySubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "BothIdenticalClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "MoreDictionaryClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + Subject = new ClaimsIdentity + ( + new List + { + new Claim(ClaimTypes.Country, "USA", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.NameIdentifier, "Bob", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer) + } + ) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "MoreSubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = new Dictionary() + { + { ClaimTypes.Email, "Bob@contoso.com" }, + { ClaimTypes.GivenName, "Bob" }, + { ClaimTypes.Role, "HR" } + }, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "RepeatingClaimTypes", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = new Dictionary() + { + { ClaimTypes.Email, "Alice@contoso.com" }, + { ClaimTypes.GivenName, "Alice" }, + { ClaimTypes.Role, "HR" } + }, + Subject = new ClaimsIdentity + ( + new List + { + new Claim(ClaimTypes.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.GivenName, "Bob", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.Country, "India", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer) + } + ) + }, + Saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(), + ValidationParameters = validationParameters + }, + }; + } + } + } public class Saml2SecurityTokenHandlerPublic : Saml2SecurityTokenHandler { @@ -1126,6 +1303,31 @@ public Saml2SecurityTokenPublic(Saml2Assertion assertion) { } } + + public class CreateTokenTheoryData : TheoryDataBase + { + public Dictionary AdditionalHeaderClaims { get; set; } + + public string Payload { get; set; } + + public string CompressionAlgorithm { get; set; } + + public CompressionProviderFactory CompressionProviderFactory { get; set; } + + public EncryptingCredentials EncryptingCredentials { get; set; } + + public bool IsValid { get; set; } = true; + + public SigningCredentials SigningCredentials { get; set; } + + public SecurityTokenDescriptor TokenDescriptor { get; set; } + + public Saml2SecurityTokenHandler Saml2SecurityTokenHandler { get; set; } + + public string SamlToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + } } #pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs index 841d91659d..547f19dc1d 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs @@ -874,7 +874,7 @@ public static TheoryData WriteTokenTheoryData Expires = Default.Expires, Issuer = Default.Issuer, SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), - Subject = new ClaimsIdentity(Default.SamlClaims) + Subject = new ClaimsIdentity(Default.SamlClaims), }; var validationParameters = new TokenValidationParameters @@ -1060,6 +1060,199 @@ public static TheoryData WriteTokenXmlTheoryData } } + [Theory, MemberData(nameof(CreateSamlTokenUsingTokenDescriptorTheoryData))] + public void CreateSamlTokenUsingTokenDescriptor(CreateTokenTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.CreateSamlTokenUsingTokenDescriptor", theoryData); + context.PropertiesToIgnoreWhenComparing = new Dictionary> + { + { typeof(SamlAssertion), new List { "IssueInstant", "InclusiveNamespacesPrefixList", "Signature", "SigningCredentials", "CanonicalString" } }, + { typeof(SamlSecurityToken), new List { "SigningKey" } }, + }; + + try + { + SecurityToken samlTokenFromSecurityTokenDescriptor = theoryData.SamlSecurityTokenHandler.CreateToken(theoryData.TokenDescriptor) as SamlSecurityToken; + string tokenFromTokenDescriptor = theoryData.SamlSecurityTokenHandler.WriteToken(samlTokenFromSecurityTokenDescriptor); + + var claimsIdentityFromTokenDescriptor = theoryData.SamlSecurityTokenHandler.ValidateToken(tokenFromTokenDescriptor, theoryData.ValidationParameters, out SecurityToken validatedTokenFromTokenDescriptor).Identity as ClaimsIdentity; + IdentityComparer.AreEqual(validatedTokenFromTokenDescriptor, samlTokenFromSecurityTokenDescriptor, context); + + theoryData.ExpectedException.ProcessNoException(context); + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData CreateSamlTokenUsingTokenDescriptorTheoryData + { + get + { + var validationParameters = new TokenValidationParameters + { + AuthenticationType = "Federation", + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + IssuerSigningKey = KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256 + }; + return new TheoryData + { + new CreateTokenTheoryData + { + TestId = "NotSupportedClaimValue", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = new Dictionary() + { + { "https://www.listinlist.com", new List{ new List { "bob", new SecurityTokenDescriptor(), 12, 1.45 }, new List { "bob", new SecurityTokenDescriptor(), 12, 1.45 } } }, + }, + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters, + ExpectedException = ExpectedException.NotSupportedException("IDX10105:") + }, + new CreateTokenTheoryData + { + First =true, + TestId = "NoSubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "OnlySubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "BothIdenticalClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "MoreDictionaryClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = Default.SamlClaimsDictionary, + Subject = new ClaimsIdentity + ( + new List + { + new Claim(ClaimTypes.Country, "USA", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.NameIdentifier, "Bob", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer) + } + ) + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "MoreSubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = new Dictionary() + { + { ClaimTypes.Email, "Bob@contoso.com" }, + { ClaimTypes.GivenName, "Bob" }, + { ClaimTypes.Role, "HR" } + }, + Subject = new ClaimsIdentity(Default.SamlClaims) + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters + }, + new CreateTokenTheoryData + { + TestId = "RepeatingClaimTypes", + TokenDescriptor = new SecurityTokenDescriptor + { + Audience = Default.Audience, + NotBefore = Default.NotBefore, + Expires = Default.Expires, + Issuer = Default.Issuer, + SigningCredentials = new SigningCredentials(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA256, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest), + EncryptingCredentials = null, + Claims = new Dictionary() + { + { ClaimTypes.Email, "Alice@contoso.com" }, + { ClaimTypes.GivenName, "Alice" }, + { ClaimTypes.Role, "HR" } + }, + Subject = new ClaimsIdentity + ( + new List + { + new Claim(ClaimTypes.Email, "Bob@contoso.com", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.GivenName, "Bob", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer), + new Claim(ClaimTypes.Country, "India", ClaimValueTypes.String, Default.Issuer, Default.OriginalIssuer) + } + ) + }, + SamlSecurityTokenHandler = new SamlSecurityTokenHandler(), + ValidationParameters = validationParameters + }, + }; + } + } + private class SamlSecurityTokenHandlerPublic : SamlSecurityTokenHandler { public IEnumerable CreateClaimsIdentitiesPublic(SamlSecurityToken samlToken, string issuer, TokenValidationParameters validationParameters) @@ -1077,6 +1270,31 @@ public string ValidateIssuerPublic(string issuer, SecurityToken token, TokenVali return base.ValidateIssuer(issuer, token, validationParameters); } } + + public class CreateTokenTheoryData : TheoryDataBase + { + public Dictionary AdditionalHeaderClaims { get; set; } + + public string Payload { get; set; } + + public string CompressionAlgorithm { get; set; } + + public CompressionProviderFactory CompressionProviderFactory { get; set; } + + public EncryptingCredentials EncryptingCredentials { get; set; } + + public bool IsValid { get; set; } = true; + + public SigningCredentials SigningCredentials { get; set; } + + public SecurityTokenDescriptor TokenDescriptor { get; set; } + + public SamlSecurityTokenHandler SamlSecurityTokenHandler { get; set; } + + public string SamlToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + } } } diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs index 392112316d..c05eeb39fe 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs @@ -32,6 +32,7 @@ using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Json.Linq; using Xunit; #pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant @@ -40,6 +41,156 @@ namespace System.IdentityModel.Tokens.Jwt.Tests { public class JwtSecurityTokenHandlerTests { + // Tests checks to make sure that the token string created by the JwtSecurityTokenHandler is consistent with the + // token string created by the JasonWebTokenHandler. + [Theory, MemberData(nameof(CreateJWEUsingSecurityTokenDescriptorTheoryData))] + public void CreateJWEUsingSecurityTokenDescriptor(CreateTokenTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.CreateJWEUsingSecurityTokenDescriptor", theoryData); + try + { + SecurityToken jweFromJwtHandler = theoryData.JwtSecurityTokenHandler.CreateToken(theoryData.TokenDescriptor); + string tokenFromTokenDescriptor = theoryData.JwtSecurityTokenHandler.WriteToken(jweFromJwtHandler); + + 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); + + IdentityComparer.AreEqual(validationResult.IsValid, theoryData.IsValid, context); + var validatedTokenFromJsonHandler = validationResult.SecurityToken; + var validationResult2 = theoryData.JsonWebTokenHandler.ValidateToken(tokenFromTokenDescriptor, theoryData.ValidationParameters); + IdentityComparer.AreEqual(validationResult.IsValid, theoryData.IsValid, context); + IdentityComparer.AreEqual(claimsPrincipal.Identity, validationResult.ClaimsIdentity, context); + IdentityComparer.AreEqual((validatedTokenFromJwtHandler as JwtSecurityToken).Claims, (validatedTokenFromJsonHandler as JsonWebToken).Claims, context); + + theoryData.ExpectedException.ProcessNoException(context); + context.PropertiesToIgnoreWhenComparing = new Dictionary> + { + { typeof(JsonWebToken), new List { "EncodedToken", "AuthenticationTag", "Ciphertext", "InitializationVector" } }, + }; + + IdentityComparer.AreEqual(validationResult2.SecurityToken as JwtSecurityToken, validationResult.SecurityToken as JwtSecurityToken, context); + theoryData.ExpectedException.ProcessNoException(context); + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData CreateJWEUsingSecurityTokenDescriptorTheoryData + { + get + { + var tokenHandler = new JwtSecurityTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + + tokenHandler.InboundClaimTypeMap.Clear(); + + var jsonTokenHandler = new JsonWebTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + + return new TheoryData + { + new CreateTokenTheoryData + { + First = true, + TestId = "IdenticalClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.X509AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = Default.PayloadJsonDictionary, + Subject = new ClaimsIdentity(Default.PayloadJsonClaims) + }, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "RepeatingAndAdditionalClaimsInSubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims =new Dictionary() + { + { "ClaimValueTypes.String", "ClaimValueTypes.String.Value" }, + { "ClaimValueTypes.Boolean.true", true }, + { "ClaimValueTypes.Double", 123.4 }, + { "ClaimValueTypes.DateTime.IS8061", DateTime.TryParse("2019-11-15T14:31:21.6101326Z", out DateTime dateTimeValue1) ? dateTimeValue1 : new DateTime()}, + { "ClaimValueTypes.DateTime", DateTime.TryParse("2019-11-15", out DateTime dateTimeValue2) ? dateTimeValue2 : new DateTime()}, + { "ClaimValueTypes.JsonClaimValueTypes.Json1", JObject.Parse(@"{""jsonProperty1"":""jsonvalue1""}") }, + { "ClaimValueTypes.JsonClaimValueTypes.JsonNull", "" }, + { "ClaimValueTypes.JsonClaimValueTypes.JsonArray1", JArray.Parse(@"[1,2,3,4]") }, + {"ClaimValueTypes.JsonClaimValueTypes.Integer1", 1 }, + }, + Subject = new ClaimsIdentity(Default.PayloadJsonClaims) + }, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters, + }, + new CreateTokenTheoryData + { + TestId = "NoSubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.AsymmetricSigningCredentials, + EncryptingCredentials = null, + Claims = Default.PayloadJsonDictionary, + }, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "OnlySubjectClaims", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.X509AsymmetricSigningCredentials, + EncryptingCredentials = null, + Subject = new ClaimsIdentity(Default.PayloadJsonClaims) + }, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + new CreateTokenTheoryData + { + TestId = "AdditionalClaimsInDictionary", + TokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = Default.PayloadJsonDictionary, + EncryptingCredentials = null, + Subject = new ClaimsIdentity + ( + new List + { + new Claim("BooleanValue", "true", ClaimValueTypes.Boolean), + new Claim("DoubleValue", "456.7", ClaimValueTypes.Double), + new Claim("DateTimeValue", "2020-03-15T14:31:21.6101326Z", ClaimValueTypes.DateTime) + } + ) + }, + JwtSecurityTokenHandler = tokenHandler, + JsonWebTokenHandler = jsonTokenHandler, + ValidationParameters = Default.AsymmetricSignTokenValidationParameters + }, + }; + } + } + [Theory, MemberData(nameof(ActorTheoryData))] public void Actor(JwtTheoryData theoryData) { @@ -2229,6 +2380,33 @@ public class ParametersCheckTheoryData : TheoryDataBase public JwtSecurityToken token { get; set; } = new JwtSecurityToken(); public TokenValidationParameters validationParameters { get; set; } = new TokenValidationParameters(); } + + public class CreateTokenTheoryData : TheoryDataBase + { + public Dictionary AdditionalHeaderClaims { get; set; } + + public string Payload { get; set; } + + public string CompressionAlgorithm { get; set; } + + public CompressionProviderFactory CompressionProviderFactory { get; set; } + + public EncryptingCredentials EncryptingCredentials { get; set; } + + public bool IsValid { get; set; } = true; + + public SigningCredentials SigningCredentials { get; set; } + + public SecurityTokenDescriptor TokenDescriptor { get; set; } + + public JsonWebTokenHandler JsonWebTokenHandler { get; set; } + + public JwtSecurityTokenHandler JwtSecurityTokenHandler { get; set; } + + public string JwtToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + } } #pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant