Skip to content

Commit 92a00d9

Browse files
FuPingFrancoFranco FungiNinjakllysng
authored
Validate Issuer Using New Validation Model in Saml2SecurityTokenHandler (#2929)
* Initial changes to include Issuer validation to Saml2SecurityTokenHandler * Clean-up * Cache issuer validation failed stackframe * Use WsFedConfig instead * Addressing PR feedback * Update src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs Co-authored-by: kellyyangsong <[email protected]> --------- Co-authored-by: Franco Fung <[email protected]> Co-authored-by: Ignacio Inglese <[email protected]> Co-authored-by: kellyyangsong <[email protected]>
1 parent 3ac0fb3 commit 92a00d9

File tree

6 files changed

+265
-6
lines changed

6 files changed

+265
-6
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions
99
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml.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>>
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>>
12+
static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationParameters>
1213
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>>
1314
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame
1415
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame
@@ -22,6 +23,7 @@ static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrame
2223
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame
2324
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame
2425
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AudienceValidationFailed -> System.Diagnostics.StackFrame
26+
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.IssuerValidationFailed -> System.Diagnostics.StackFrame
2527
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.LifetimeValidationFailed -> System.Diagnostics.StackFrame
2628
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame
2729
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,34 @@ internal static async Task<TokenValidationParameters> PopulateValidationParamete
154154
return validationParametersCloned;
155155

156156
}
157+
158+
/// <summary>
159+
/// Fetches current configuration from the ConfigurationManager of <paramref name="validationParameters"/>
160+
/// and populates ValidIssuers and IssuerSigningKeys.
161+
/// </summary>
162+
/// <param name="validationParameters"> the token validation parameters to update.</param>
163+
/// <param name="cancellationToken"></param>
164+
/// <returns> New ValidationParameters with ValidIssuers and IssuerSigningKeys updated.</returns>
165+
internal static async Task<ValidationParameters> PopulateValidationParametersWithCurrentConfigurationAsync(
166+
ValidationParameters validationParameters,
167+
CancellationToken cancellationToken)
168+
{
169+
if (validationParameters.ConfigurationManager == null)
170+
{
171+
return validationParameters;
172+
}
173+
174+
var currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken).ConfigureAwait(false);
175+
var validationParametersCloned = validationParameters.Clone();
176+
177+
validationParametersCloned.ValidIssuers.Add(currentConfiguration.Issuer);
178+
179+
foreach (SecurityKey key in currentConfiguration.SigningKeys)
180+
{
181+
validationParametersCloned.IssuerSigningKeys.Add(key);
182+
}
183+
184+
return validationParametersCloned;
185+
}
157186
}
158187
}

src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2NameIdentifier.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public string Value
104104
get { return _value; }
105105
set
106106
{
107-
if (string.IsNullOrEmpty(value))
107+
if (string.IsNullOrEmpty(value)) //NOTE: We can remove this check and let our issuer validator handle this.
108108
throw LogArgumentNullException(nameof(value));
109109

110110
_value = value;

src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Microsoft.IdentityModel.Tokens.Saml;
89

910
#nullable enable
1011
namespace Microsoft.IdentityModel.Tokens.Saml2
@@ -14,15 +15,11 @@ namespace Microsoft.IdentityModel.Tokens.Saml2
1415
/// </summary>
1516
public partial class Saml2SecurityTokenHandler : SecurityTokenHandler
1617
{
17-
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
1818
internal async Task<ValidationResult<ValidatedToken>> ValidateTokenAsync(
19-
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
2019
Saml2SecurityToken samlToken,
2120
ValidationParameters validationParameters,
2221
CallContext callContext,
23-
#pragma warning disable CA1801 // Review unused parameters
2422
CancellationToken cancellationToken)
25-
#pragma warning restore CA1801 // Review unused parameters
2623
{
2724
if (samlToken is null)
2825
{
@@ -40,14 +37,32 @@ internal async Task<ValidationResult<ValidatedToken>> ValidateTokenAsync(
4037
StackFrames.TokenValidationParametersNull);
4138
}
4239

43-
var conditionsResult = ValidateConditions(samlToken, validationParameters, callContext);
40+
validationParameters = await SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(validationParameters, cancellationToken).ConfigureAwait(false);
41+
42+
var conditionsResult = ValidateConditions(
43+
samlToken,
44+
validationParameters,
45+
callContext);
4446

4547
if (!conditionsResult.IsValid)
4648
{
4749
StackFrames.AssertionConditionsValidationFailed ??= new StackFrame(true);
4850
return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed);
4951
}
5052

53+
ValidationResult<ValidatedIssuer> validatedIssuerResult = await validationParameters.IssuerValidatorAsync(
54+
samlToken.Issuer,
55+
samlToken,
56+
validationParameters,
57+
callContext,
58+
cancellationToken).ConfigureAwait(false);
59+
60+
if (!validatedIssuerResult.IsValid)
61+
{
62+
StackFrames.IssuerValidationFailed ??= new StackFrame(true);
63+
return validatedIssuerResult.UnwrapError().AddStackFrame(StackFrames.IssuerValidationFailed);
64+
}
65+
5166
return new ValidatedToken(samlToken, this, validationParameters);
5267
}
5368

src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal static class StackFrames
1919
internal static StackFrame? AudienceValidationFailed;
2020
internal static StackFrame? AssertionNull;
2121
internal static StackFrame? AssertionConditionsNull;
22+
internal static StackFrame? IssuerValidationFailed;
2223
internal static StackFrame? AssertionConditionsValidationFailed;
2324
internal static StackFrame? LifetimeValidationFailed;
2425
internal static StackFrame? OneTimeUseValidationFailed;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.IdentityModel.Protocols.WsFederation;
7+
using Microsoft.IdentityModel.TestUtils;
8+
using Microsoft.IdentityModel.Tokens.Saml2;
9+
using Xunit;
10+
11+
namespace Microsoft.IdentityModel.Tokens.Saml.Tests
12+
{
13+
#nullable enable
14+
public partial class Saml2SecurityTokenHandlerTests
15+
{
16+
[Theory, MemberData(nameof(ValidateTokenAsync_IssuerTestCases), DisableDiscoveryEnumeration = true)]
17+
public async Task ValidateTokenAsync_IssuerComparison(ValidateTokenAsyncIssuerTheoryData theoryData)
18+
{
19+
var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_IssuerComparison", theoryData);
20+
21+
var saml2Token = CreateTokenWithIssuer(theoryData.TokenIssuer);
22+
23+
var tokenValidationParameters = CreateTokenValidationParametersForIssuerValidationOnly(
24+
saml2Token,
25+
theoryData.NullTokenValidationParameters,
26+
theoryData.ValidationParametersIssuer,
27+
theoryData.ConfigurationIssuer);
28+
29+
Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler();
30+
31+
// Validate token using TokenValidationParameters
32+
TokenValidationResult tokenValidationResult =
33+
await saml2TokenHandler.ValidateTokenAsync(saml2Token.Assertion.CanonicalString, tokenValidationParameters);
34+
35+
// Validate token using ValidationParameters.
36+
ValidationResult<ValidatedToken> validationResult =
37+
await saml2TokenHandler.ValidateTokenAsync(
38+
saml2Token,
39+
theoryData.ValidationParameters!,
40+
theoryData.CallContext,
41+
CancellationToken.None);
42+
43+
// Ensure validity of the results match the expected result.
44+
if (tokenValidationResult.IsValid != validationResult.IsValid)
45+
{
46+
context.AddDiff($"tokenValidationResult.IsValid != validationResult.IsSuccess");
47+
theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context);
48+
theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context);
49+
}
50+
else
51+
{
52+
if (tokenValidationResult.IsValid)
53+
{
54+
// Verify validated tokens from both paths match.
55+
ValidatedToken validatedToken = validationResult.UnwrapResult();
56+
IdentityComparer.AreEqual(validatedToken.SecurityToken, tokenValidationResult.SecurityToken, context);
57+
}
58+
else
59+
{
60+
// Verify the exception provided by both paths match.
61+
var tokenValidationResultException = tokenValidationResult.Exception;
62+
theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context);
63+
var validationResultException = validationResult.UnwrapError().GetException();
64+
theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context);
65+
}
66+
67+
TestUtilities.AssertFailIfErrors(context);
68+
}
69+
}
70+
71+
public static TheoryData<ValidateTokenAsyncIssuerTheoryData> ValidateTokenAsync_IssuerTestCases
72+
{
73+
get
74+
{
75+
var theoryData = new TheoryData<ValidateTokenAsyncIssuerTheoryData>();
76+
77+
theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Valid_IssuerIsValidIssuer")
78+
{
79+
TokenIssuer = Default.Issuer,
80+
ValidationParametersIssuer = Default.Issuer,
81+
ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer),
82+
});
83+
84+
theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Valid_IssuerIsConfigurationIssuer")
85+
{
86+
TokenIssuer = Default.Issuer,
87+
ConfigurationIssuer = Default.Issuer,
88+
ValidationParameters = CreateValidationParameters(configurationIssuer: Default.Issuer),
89+
});
90+
91+
theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsNotValid")
92+
{
93+
TokenIssuer = "InvalidIssuer",
94+
ValidationParametersIssuer = Default.Issuer,
95+
ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer),
96+
ExpectedIsValid = false,
97+
ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10205:"),
98+
ExpectedExceptionValidationParameters = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10212:")
99+
});
100+
101+
theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsWhitespace")
102+
{
103+
//This test will cover the case where the issuer is null or empty as well since, we do not allow tokens to be created with null or empty issuer.
104+
TokenIssuer = " ",
105+
ValidationParametersIssuer = Default.Issuer,
106+
ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer),
107+
ExpectedIsValid = false,
108+
ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:")
109+
});
110+
111+
theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Invalid_NoValidIssuersProvided")
112+
{
113+
TokenIssuer = Default.Issuer,
114+
ValidationParametersIssuer = string.Empty,
115+
ValidationParameters = CreateValidationParameters(),
116+
ExpectedIsValid = false,
117+
ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10204:"),
118+
ExpectedExceptionValidationParameters = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:")
119+
});
120+
121+
return theoryData;
122+
123+
static ValidationParameters CreateValidationParameters(
124+
string? validIssuer = null,
125+
string? configurationIssuer = null)
126+
{
127+
ValidationParameters validationParameters = new ValidationParameters();
128+
129+
// Skip all validations except issuer
130+
validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation;
131+
validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation;
132+
validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation;
133+
validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation;
134+
validationParameters.SignatureValidator = SkipValidationDelegates.SkipSignatureValidation;
135+
136+
return validationParameters;
137+
}
138+
}
139+
}
140+
141+
public class ValidateTokenAsyncIssuerTheoryData : TheoryDataBase
142+
{
143+
public ValidateTokenAsyncIssuerTheoryData(string testId) : base(testId) { }
144+
145+
internal ValidationParameters? ValidationParameters { get; set; }
146+
147+
internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected;
148+
149+
internal bool ExpectedIsValid { get; set; } = true;
150+
151+
public bool NullTokenValidationParameters { get; internal set; } = false;
152+
153+
public string? TokenIssuer { get; set; }
154+
155+
public string? ValidationParametersIssuer { get; set; } = null;
156+
157+
public string? ConfigurationIssuer { get; set; } = null;
158+
}
159+
160+
private static Saml2SecurityToken CreateTokenWithIssuer(string? issuer)
161+
{
162+
Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler();
163+
164+
SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor
165+
{
166+
SigningCredentials = Default.AsymmetricSigningCredentials,
167+
Audience = Default.Audience,
168+
Issuer = issuer,
169+
Subject = Default.SamlClaimsIdentity
170+
};
171+
172+
return (Saml2SecurityToken)saml2TokenHandler.CreateToken(securityTokenDescriptor);
173+
}
174+
175+
private static TokenValidationParameters? CreateTokenValidationParametersForIssuerValidationOnly(
176+
Saml2SecurityToken saml2SecurityToken,
177+
bool nullTokenValidationParameters,
178+
string? validIssuer,
179+
string? configurationIssuer)
180+
{
181+
if (nullTokenValidationParameters)
182+
{
183+
return null;
184+
}
185+
186+
var tokenValidationParameters = new TokenValidationParameters()
187+
{
188+
ValidateAudience = false,
189+
ValidateIssuer = false,
190+
ValidateLifetime = false,
191+
ValidateTokenReplay = false,
192+
ValidateIssuerSigningKey = false,
193+
IssuerSigningKey = Default.AsymmetricSigningKey,
194+
ValidAudiences = [Default.Audience],
195+
ValidIssuer = validIssuer,
196+
SignatureValidator = delegate (string token, TokenValidationParameters validationParameters)
197+
{
198+
return saml2SecurityToken;
199+
}
200+
};
201+
202+
if (configurationIssuer is not null)
203+
{
204+
var validConfig = new WsFederationConfiguration() { Issuer = configurationIssuer };
205+
tokenValidationParameters.ConfigurationManager = new MockConfigurationManager<WsFederationConfiguration>(validConfig);
206+
}
207+
208+
return tokenValidationParameters;
209+
}
210+
}
211+
}
212+
#nullable restore

0 commit comments

Comments
 (0)