Skip to content

Commit 5471249

Browse files
authored
SAML new model validation: Signature (#2958)
* Added XmlValidationError. Added ValidationError property to XmlValidationException to provide custom stack traces * Added alternative versions using ValidationParameters to XML signature validations * Added XmlValidationFailure to ValidationFailureType * Added refactored ValidateSignature method to SamlSecurityTokenHandler. Updated ValidateTokenAsync to call ValidateSignature. * Added tests to compare signature validation between the legacy and new path * Re-added API lost in merge to InternalAPI.Unshipped.txt
1 parent ba49516 commit 5471249

16 files changed

+649
-1
lines changed

src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateTokenAsync(
1010
Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames
1111
Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
1212
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.IssuerValidationFailed -> System.Diagnostics.StackFrame
13+
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.SignatureValidationFailed -> System.Diagnostics.StackFrame
14+
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateSignature(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.SecurityKey>
1315
static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationParameters>
1416
Microsoft.IdentityModel.Tokens.Saml2.SamlSecurityTokenHandler.ValidateTokenAsync(SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
1517
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame
@@ -20,6 +22,7 @@ static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.
2022
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame
2123
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame
2224
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame
25+
static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.ResolveTokenSigningKey(Microsoft.IdentityModel.Xml.KeyInfo tokenKeyInfo, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters) -> Microsoft.IdentityModel.Tokens.SecurityKey
2326
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame
2427
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame
2528
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Text;
7+
using Microsoft.IdentityModel.Xml;
8+
using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;
9+
10+
#nullable enable
11+
namespace Microsoft.IdentityModel.Tokens.Saml
12+
{
13+
public partial class SamlSecurityTokenHandler : SecurityTokenHandler
14+
{
15+
internal static ValidationResult<SecurityKey> ValidateSignature(
16+
SamlSecurityToken samlToken,
17+
ValidationParameters validationParameters,
18+
#pragma warning disable CA1801 // Review unused parameters
19+
CallContext callContext)
20+
#pragma warning restore CA1801 // Review unused parameters
21+
{
22+
if (samlToken is null)
23+
{
24+
return ValidationError.NullParameter(
25+
nameof(samlToken),
26+
new StackFrame(true));
27+
}
28+
29+
if (validationParameters is null)
30+
{
31+
return ValidationError.NullParameter(
32+
nameof(validationParameters),
33+
new StackFrame(true));
34+
}
35+
36+
// Delegate is set by the user, we call it and return the result.
37+
if (validationParameters.SignatureValidator is not null)
38+
return validationParameters.SignatureValidator(samlToken, validationParameters, null, callContext);
39+
40+
// If the user wants to accept unsigned tokens, they must implement the delegate
41+
if (samlToken.Assertion.Signature is null)
42+
return new XmlValidationError(
43+
new MessageDetail(
44+
TokenLogMessages.IDX10504,
45+
samlToken.Assertion.CanonicalString),
46+
ValidationFailureType.SignatureValidationFailed,
47+
typeof(SecurityTokenValidationException),
48+
new StackFrame(true));
49+
50+
IList<SecurityKey>? keys = null;
51+
SecurityKey? resolvedKey = null;
52+
bool keyMatched = false;
53+
54+
if (validationParameters.IssuerSigningKeyResolver is not null)
55+
{
56+
resolvedKey = validationParameters.IssuerSigningKeyResolver(
57+
samlToken.Assertion.CanonicalString,
58+
samlToken,
59+
samlToken.Assertion.Signature.KeyInfo?.Id,
60+
validationParameters,
61+
null,
62+
callContext);
63+
}
64+
else
65+
{
66+
resolvedKey = SamlTokenUtilities.ResolveTokenSigningKey(samlToken.Assertion.Signature.KeyInfo, validationParameters);
67+
}
68+
69+
if (resolvedKey is null)
70+
{
71+
if (validationParameters.TryAllIssuerSigningKeys)
72+
keys = validationParameters.IssuerSigningKeys;
73+
}
74+
else
75+
{
76+
keys = [resolvedKey];
77+
keyMatched = true;
78+
}
79+
80+
bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null;
81+
List<ValidationError> errors = new();
82+
StringBuilder keysAttempted = new();
83+
84+
if (keys is not null)
85+
{
86+
for (int i = 0; i < keys.Count; i++)
87+
{
88+
SecurityKey key = keys[i];
89+
ValidationResult<string> algorithmValidationResult = validationParameters.AlgorithmValidator(
90+
samlToken.Assertion.Signature.SignedInfo.SignatureMethod,
91+
key,
92+
samlToken,
93+
validationParameters,
94+
callContext);
95+
96+
if (!algorithmValidationResult.IsValid)
97+
{
98+
errors.Add(algorithmValidationResult.UnwrapError());
99+
}
100+
else
101+
{
102+
var validationError = samlToken.Assertion.Signature.Verify(
103+
key,
104+
validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory,
105+
callContext);
106+
107+
if (validationError is null)
108+
{
109+
samlToken.SigningKey = key;
110+
111+
return key;
112+
}
113+
else
114+
{
115+
errors.Add(validationError.AddStackFrame(new StackFrame()));
116+
}
117+
}
118+
119+
keysAttempted.Append(key.ToString());
120+
if (canMatchKey && !keyMatched && key.KeyId is not null && samlToken.Assertion.Signature.KeyInfo is not null)
121+
keyMatched = samlToken.Assertion.Signature.KeyInfo.MatchesKey(key);
122+
}
123+
}
124+
125+
if (canMatchKey && keyMatched)
126+
return new XmlValidationError(
127+
new MessageDetail(
128+
TokenLogMessages.IDX10514,
129+
keysAttempted.ToString(),
130+
samlToken.Assertion.Signature.KeyInfo,
131+
GetErrorStrings(errors),
132+
samlToken),
133+
ValidationFailureType.SignatureValidationFailed,
134+
typeof(SecurityTokenInvalidSignatureException),
135+
new StackFrame(true));
136+
137+
if (keysAttempted.Length > 0)
138+
return new XmlValidationError(
139+
new MessageDetail(
140+
TokenLogMessages.IDX10512,
141+
keysAttempted.ToString(),
142+
GetErrorStrings(errors),
143+
samlToken),
144+
ValidationFailureType.SignatureValidationFailed,
145+
typeof(SecurityTokenSignatureKeyNotFoundException),
146+
new StackFrame(true));
147+
148+
return new XmlValidationError(
149+
new MessageDetail(TokenLogMessages.IDX10500),
150+
ValidationFailureType.SignatureValidationFailed,
151+
typeof(SecurityTokenSignatureKeyNotFoundException),
152+
new StackFrame(true));
153+
}
154+
155+
private static string GetErrorStrings(List<ValidationError> errors)
156+
{
157+
StringBuilder sb = new();
158+
for (int i = 0; i < errors.Count; i++)
159+
{
160+
sb.AppendLine(errors[i].ToString());
161+
}
162+
163+
return sb.ToString();
164+
}
165+
}
166+
}
167+
#nullable restore

src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ internal async Task<ValidationResult<ValidatedToken>> ValidateTokenAsync(
6161
return issuerValidationResult.UnwrapError().AddStackFrame(StackFrames.IssuerValidationFailed);
6262
}
6363

64+
var signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext);
65+
if (!signatureValidationResult.IsValid)
66+
{
67+
StackFrames.SignatureValidationFailed ??= new StackFrame(true);
68+
return signatureValidationResult.UnwrapError().AddStackFrame(StackFrames.SignatureValidationFailed);
69+
}
70+
6471
return new ValidatedToken(samlToken, this, validationParameters);
6572
}
6673

src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ internal static class StackFrames
2424
internal static StackFrame? OneTimeUseValidationFailed;
2525

2626
internal static StackFrame? IssuerValidationFailed;
27+
internal static StackFrame? SignatureValidationFailed;
2728
}
2829
}
2930
}

src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,29 @@ internal static SecurityKey ResolveTokenSigningKey(KeyInfo tokenKeyInfo, TokenVa
4747
return null;
4848
}
4949

50+
/// <summary>
51+
/// Returns a <see cref="SecurityKey"/> to use when validating the signature of a token.
52+
/// </summary>
53+
/// <param name="tokenKeyInfo">The <see cref="KeyInfo"/> field of the token being validated</param>
54+
/// <param name="validationParameters">The <see cref="ValidationParameters"/> to be used for validating the token.</param>
55+
/// <returns>Returns a <see cref="SecurityKey"/> to use for signature validation.</returns>
56+
/// <remarks>If key fails to resolve, then null is returned</remarks>
57+
internal static SecurityKey ResolveTokenSigningKey(KeyInfo tokenKeyInfo, ValidationParameters validationParameters)
58+
{
59+
if (tokenKeyInfo is null || validationParameters.IssuerSigningKeys is null)
60+
return null;
61+
62+
for (int i = 0; i < validationParameters.IssuerSigningKeys.Count; i++)
63+
{
64+
if (tokenKeyInfo.MatchesKey(validationParameters.IssuerSigningKeys[i]))
65+
return validationParameters.IssuerSigningKeys[i];
66+
}
67+
68+
return null;
69+
}
70+
71+
72+
5073
/// <summary>
5174
/// Creates <see cref="Claim"/>'s from <paramref name="claimsCollection"/>.
5275
/// </summary>

src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedStr
3232
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoTokenAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType
3333
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoValidationParameterAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType
3434
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAlgorithmValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType
35+
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.XmlValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType

src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ internal Exception GetException(Type exceptionType, Exception innerException)
118118
exception = new SecurityTokenException(MessageDetail.Message);
119119
else if (exceptionType == typeof(SecurityTokenKeyWrapException))
120120
exception = new SecurityTokenKeyWrapException(MessageDetail.Message);
121+
else if (ExceptionType == typeof(SecurityTokenValidationException))
122+
exception = new SecurityTokenValidationException(MessageDetail.Message);
121123
else
122124
{
123125
// Exception type is unknown
@@ -175,6 +177,8 @@ internal Exception GetException(Type exceptionType, Exception innerException)
175177
exception = new SecurityTokenException(MessageDetail.Message, actualException);
176178
else if (exceptionType == typeof(SecurityTokenKeyWrapException))
177179
exception = new SecurityTokenKeyWrapException(MessageDetail.Message, actualException);
180+
else if (exceptionType == typeof(SecurityTokenValidationException))
181+
exception = new SecurityTokenValidationException(MessageDetail.Message, actualException);
178182
else
179183
{
180184
// Exception type is unknown

src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,11 @@ private class TokenDecryptionFailure : ValidationFailureType { internal TokenDec
110110
/// </summary>
111111
public static readonly ValidationFailureType InvalidSecurityToken = new InvalidSecurityTokenFailure("InvalidSecurityToken");
112112
private class InvalidSecurityTokenFailure : ValidationFailureType { internal InvalidSecurityTokenFailure(string name) : base(name) { } }
113+
114+
/// <summary>
115+
/// Defines a type that represents that an XML validation failed.
116+
/// </summary>
117+
public static readonly ValidationFailureType XmlValidationFailed = new XmlValidationFailure("XmlValidationFailed");
118+
private class XmlValidationFailure : ValidationFailureType { internal XmlValidationFailure(string name) : base(name) { } }
113119
}
114120
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using Microsoft.IdentityModel.Tokens;
7+
8+
namespace Microsoft.IdentityModel.Xml
9+
{
10+
internal class XmlValidationError : ValidationError
11+
{
12+
public XmlValidationError(
13+
MessageDetail messageDetail,
14+
ValidationFailureType validationFailureType,
15+
Type exceptionType,
16+
StackFrame stackFrame) :
17+
base(messageDetail, validationFailureType, exceptionType, stackFrame)
18+
{
19+
20+
}
21+
22+
internal override Exception GetException()
23+
{
24+
if (ExceptionType == typeof(XmlValidationException))
25+
{
26+
XmlValidationException exception = new(MessageDetail.Message, InnerException);
27+
exception.SetValidationError(this);
28+
return exception;
29+
}
30+
31+
return base.GetException();
32+
}
33+
}
34+
}

src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationException.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Runtime.Serialization;
7+
#pragma warning disable IDE0005 // Using directive is unnecessary.
8+
using System.Text;
9+
#pragma warning restore IDE0005 // Using directive is unnecessary.
10+
using Microsoft.IdentityModel.Tokens;
611

712
namespace Microsoft.IdentityModel.Xml
813
{
@@ -12,6 +17,11 @@ namespace Microsoft.IdentityModel.Xml
1217
[Serializable]
1318
public class XmlValidationException : XmlException
1419
{
20+
[NonSerialized]
21+
private string _stackTrace;
22+
23+
private ValidationError _validationError;
24+
1525
/// <summary>
1626
/// Initializes a new instance of the <see cref="XmlValidationException"/> class.
1727
/// </summary>
@@ -49,5 +59,43 @@ protected XmlValidationException(SerializationInfo info, StreamingContext contex
4959
: base(info, context)
5060
{
5161
}
62+
63+
/// <summary>
64+
/// Sets the <see cref="ValidationError"/> that caused the exception.
65+
/// </summary>
66+
/// <param name="validationError"></param>
67+
internal void SetValidationError(ValidationError validationError)
68+
{
69+
_validationError = validationError;
70+
}
71+
72+
/// <summary>
73+
/// Gets the stack trace that is captured when the exception is created.
74+
/// </summary>
75+
public override string StackTrace
76+
{
77+
get
78+
{
79+
if (_stackTrace == null)
80+
{
81+
if (_validationError == null)
82+
return base.StackTrace;
83+
#if NET8_0_OR_GREATER
84+
_stackTrace = new StackTrace(_validationError.StackFrames).ToString();
85+
#else
86+
StringBuilder sb = new();
87+
foreach (StackFrame frame in _validationError.StackFrames)
88+
{
89+
sb.Append(frame.ToString());
90+
sb.Append(Environment.NewLine);
91+
}
92+
93+
_stackTrace = sb.ToString();
94+
#endif
95+
}
96+
97+
return _stackTrace;
98+
}
99+
}
52100
}
53101
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Microsoft.IdentityModel.Xml.Reference.Verify(Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError
2+
Microsoft.IdentityModel.Xml.Signature.Verify(Microsoft.IdentityModel.Tokens.SecurityKey key, Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError
3+
Microsoft.IdentityModel.Xml.SignedInfo.Verify(Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError
4+
Microsoft.IdentityModel.Xml.XmlValidationError
5+
Microsoft.IdentityModel.Xml.XmlValidationError.XmlValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame) -> void
6+
Microsoft.IdentityModel.Xml.XmlValidationException.SetValidationError(Microsoft.IdentityModel.Tokens.ValidationError validationError) -> void
7+
override Microsoft.IdentityModel.Xml.XmlValidationError.GetException() -> System.Exception
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
override Microsoft.IdentityModel.Xml.XmlValidationException.StackTrace.get -> string

0 commit comments

Comments
 (0)