-
Notifications
You must be signed in to change notification settings - Fork 397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(feature): Add ActiveTokenEndpoint to WsFederationConfiguration #2100
Changes from all commits
7181cd3
c87f6d3
8335438
5783795
286745f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System; | ||
using System.Security.Cryptography; | ||
using System.Security.Cryptography.X509Certificates; | ||
using Microsoft.IdentityModel.Tokens; | ||
using Microsoft.IdentityModel.Xml; | ||
using static Microsoft.IdentityModel.Logging.LogHelper; | ||
TimHannMSFT marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
namespace Microsoft.IdentityModel.Protocols.WsFederation | ||
{ | ||
/// <summary> | ||
/// Defines a class for validating the WsFederationConfiguration. | ||
/// </summary> | ||
public class WsFederationConfigurationValidator : IConfigurationValidator<WsFederationConfiguration> | ||
{ | ||
/// <summary> | ||
/// Validates a WsFederationConfiguration. | ||
/// </summary> | ||
/// <param name="configuration">WsFederationConfiguration to validate</param> | ||
/// <returns>A <see cref="ConfigurationValidationResult"/> containing the validation result.</returns> | ||
/// <exception cref="ArgumentNullException">If the provided configuration is null</exception> | ||
public ConfigurationValidationResult Validate(WsFederationConfiguration configuration) | ||
{ | ||
if (configuration == null) | ||
throw LogArgumentNullException(nameof(configuration)); | ||
|
||
if (string.IsNullOrWhiteSpace(configuration.Issuer)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22700, | ||
Succeeded = false | ||
}; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider doing all these checks up front and use StringBuilder to build out an error message. In the current model a developer may face one issue, update, face another issue, update...etc. Getting all the errors up front allows for more actionable errors/fewer iterations of debugging. Also less code maintenance costs IMO since if we decide to add another check it doesn't require another log message. #WontFix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current code assume some of the checks before it. While is not a bad idea it requires a refactor of the whole method. If you dont mind I would like to postpone this improvement. |
||
|
||
if (configuration.Signature == null) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22701, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (configuration.Signature.KeyInfo == null) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22702, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(configuration.Signature.SignatureValue)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22703, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (configuration.Signature.SignedInfo == null) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22704, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(configuration.Signature.SignedInfo.SignatureMethod)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22705, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (configuration.Signature.SignedInfo.References == null || configuration.Signature.SignedInfo.References.Count == 0) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22706, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(configuration.ActiveTokenEndpoint)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22707, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (!Uri.IsWellFormedUriString(configuration.ActiveTokenEndpoint, UriKind.Absolute)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22708, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(configuration.TokenEndpoint)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22709, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (!Uri.IsWellFormedUriString(configuration.TokenEndpoint, UriKind.Absolute)) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22710, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
if (configuration.SigningKeys == null || configuration.SigningKeys.Count == 0) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22711, | ||
Succeeded = false | ||
}; | ||
} | ||
|
||
// Get the thumbprint of the cert used to sign the metadata | ||
string signingKeyId = string.Empty; | ||
var signatureX509Data = configuration.Signature.KeyInfo.X509Data.GetEnumerator(); | ||
|
||
if (signatureX509Data.MoveNext()) | ||
{ | ||
var signatureCertData = signatureX509Data.Current.Certificates.GetEnumerator(); | ||
if (signatureCertData.MoveNext() && !string.IsNullOrWhiteSpace(signatureCertData.Current)) | ||
{ | ||
X509Certificate2 cert = null; | ||
|
||
try | ||
{ | ||
cert = new X509Certificate2(Convert.FromBase64String(signatureCertData.Current)); | ||
signingKeyId = cert.Thumbprint; | ||
} | ||
catch (CryptographicException) | ||
{ | ||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22712, | ||
Succeeded = false | ||
}; | ||
} | ||
finally | ||
{ | ||
if (cert != null) | ||
{ | ||
((IDisposable)cert).Dispose(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// We know the key used to sign the doc is part of the token signing keys as per the spec. | ||
// http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html#_Toc223174958:~:text=%3C/fed%3ATargetScopes%20%3E-,3.1.15%20%5BSignature%5D%20Property,-The%20OPTIONAL%20%5BSignature | ||
// If the metadata is for a token issuer then the key used to sign issued tokens SHOULD | ||
// be used to sign this document. This means that if a <fed:TokenSigningKey> is specified, | ||
// it SHOULD be used to sign this document. | ||
|
||
foreach (SecurityKey key in configuration.SigningKeys) | ||
{ | ||
if (key == null || key.CryptoProviderFactory == null || signingKeyId != key.KeyId) | ||
continue; | ||
|
||
try | ||
{ | ||
configuration.Signature.Verify(key, key.CryptoProviderFactory); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this can throw There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good catch. On the our scenario this is not a real problem. If we deserialize the configuration using the metadataSerializer class the security key always use the default crypto provider and this value is never null. Having said that, it is possible to modify this property before calling the validator. We need to handle that scenario. I will update the logic to check if there is a crypto provider before attempting to verify. |
||
|
||
return new ConfigurationValidationResult | ||
{ | ||
Succeeded = true | ||
}; | ||
} | ||
catch (XmlValidationException) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although the only exception expected is this, the cryptoprovider may potentially throw any other exception. We could eat the exception and try to continue however given that we checked for the key thumbprint we do not expect any other key to be valid. We do not expect multiple entries with the same thumbprint and different crypto provider factory. In my opinion the best thing to do is to let that exception bubble up. The other alternative I see would be to update the ConfigurationValidationResult object to include exception information, given that that object is shared across ConfigurationValidators I would prefer to not change the pattern. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do want to try rest of the keys in the list if one fails correct? In that case, like Danny suggested, it should be continue. |
||
{ | ||
// We know the signature is invalid at this point | ||
break; | ||
} | ||
} | ||
|
||
return new ConfigurationValidationResult | ||
{ | ||
ErrorMessage = LogMessages.IDX22713, | ||
Succeeded = false | ||
}; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,17 +23,35 @@ internal static class LogMessages | |
internal const string IDX22904 = "IDX22904: Wresult does not contain a 'RequestedSecurityToken' element."; | ||
|
||
// xml metadata messages | ||
internal const string IDX22800 = "IDX22800: Exception thrown while reading WsFedereationMetadata. Element '{0}'. Caught exception: '{1}'."; | ||
internal const string IDX22801 = "IDX22801: entityID attribute is not found in EntityDescriptor element in metadata file."; | ||
internal const string IDX22800 = "IDX22800: Exception thrown while reading WsFederationMetadata. Element '{0}'. Caught exception: '{1}'."; | ||
internal const string IDX22801 = "IDX22801: 'entityID' attribute is not found in EntityDescriptor element in metadata file."; | ||
internal const string IDX22802 = "IDX22802: Current name '{0} and namespace '{1}' do not match the expected name '{2}' and namespace '{3}'."; | ||
internal const string IDX22803 = "IDX22803: Token reference address is missing in SecurityTokenServiceEndpoint in metadata file."; | ||
internal const string IDX22804 = "IDX22804: Security token type role descriptor is expected."; | ||
internal const string IDX22806 = "IDX22806: Key descriptor for signing is missing in security token service type RoleDescriptor."; | ||
internal const string IDX22807 = "IDX22807: Token endpoint is missing in security token service type RoleDescriptor."; | ||
internal const string IDX22803 = "IDX22803: Token reference address is missing in 'PassiveRequestorEndpoint' in metadata file."; | ||
internal const string IDX22804 = "IDX22804: 'SecurityTokenServiceTypeRoleDescriptor' is expected."; | ||
internal const string IDX22806 = "IDX22806: Key descriptor for signing is missing in 'SecurityTokenServiceTypeRoleDescriptor'."; | ||
internal const string IDX22807 = "IDX22807: Token endpoint is missing in 'SecurityTokenServiceTypeRoleDescriptor'."; | ||
internal const string IDX22808 = "IDX22808: 'Use' attribute is missing in KeyDescriptor."; | ||
internal const string IDX22810 = "IDX22810: 'Issuer' value is missing in wsfederationconfiguration."; | ||
internal const string IDX22811 = "IDX22811: 'TokenEndpoint' value is missing in wsfederationconfiguration."; | ||
internal const string IDX22812 = "IDX22812: Element: '{0}' was an empty element. 'TokenEndpoint' value is missing in wsfederationconfiguration."; | ||
internal const string IDX22813 = "IDX22813: 'ActiveTokenEndpoint' is missing in 'SecurityTokenServiceTypeRoleDescriptor'."; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: [consistency] this is in single quotes here and SecurityTokenServiceEndpoint is not in the next log. Also IDX22806, IDX22807 talk about "token service type RoleDescriptor". I think the way you have it is more clear probably but good to have consistency too for ease of understanding. #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, will update. |
||
internal const string IDX22814 = "IDX22814: Token reference address is missing in 'SecurityTokenServiceEndpoint' in metadata."; | ||
|
||
// WsFederationConfigurationValidator messages | ||
internal const string IDX22700 = "IDX22700: The Issuer property is null or empty."; | ||
internal const string IDX22701 = "IDX22701: The Signature property is null."; | ||
internal const string IDX22702 = "IDX22702: The Signature's KeyInfo property is null."; | ||
internal const string IDX22703 = "IDX22703: The Signature's SignatureValue property is null or empty."; | ||
internal const string IDX22704 = "IDX22704: The Signature.SignedInfo property is null or empty."; | ||
internal const string IDX22705 = "IDX22705: The Signature.SignedInfo.SignatureMethod property is null or empty."; | ||
internal const string IDX22706 = "IDX22706: The Signature.SignedInfo.References property is null or an empty collection."; | ||
internal const string IDX22707 = "IDX22707: The ActiveTokenEndpoint property is not defined."; | ||
internal const string IDX22708 = "IDX22708: The ActiveTokenEndpoint property is not a valid URI."; | ||
internal const string IDX22709 = "IDX22709: The TokenEndpoint property is not defined."; | ||
internal const string IDX22710 = "IDX22710: The TokenEndpoint property is not a valid URI."; | ||
internal const string IDX22711 = "IDX22711: The SigningKeys is null or an empty collection."; | ||
internal const string IDX22712 = "IDX22712: Could not identify the thumbprint of the key used to sign the metadata."; | ||
internal const string IDX22713 = "IDX22713: Metadata signature validation failed."; | ||
|
||
#pragma warning restore 1591 | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,13 +21,23 @@ public List<KeyInfo> KeyInfos | |
} = new List<KeyInfo>(); | ||
|
||
/// <summary> | ||
/// Token endpoint | ||
/// Passive Requestor Token endpoint | ||
/// fed:PassiveRequestorEndpoint, https://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html#:~:text=fed%3ASecurityTokenServiceType/fed%3APassiveRequestorEndpoint | ||
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a line or link on what passive requestor is. |
||
/// </summary> | ||
public string TokenEndpoint | ||
{ | ||
get; | ||
set; | ||
} | ||
|
||
/// <summary> | ||
/// Active Requestor Token Endpoint | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, can we add some description on what active requestor is. |
||
/// fed:SecurityTokenServiceType, http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html#:~:text=fed%3ASecurityTokenSerivceEndpoint | ||
/// </summary> | ||
public string ActiveTokenEndpoint | ||
{ | ||
get; | ||
set; | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will modify this in our 7.x release when we will use the utf8Reader and set the ActiveTokenEndpoint to the "token_endpoint" and PassiveTokenEndpoint to "authorization_endpoint".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we keeping track of the changes we're intending to make/are in the process of making somewhere? We'll definitely want to make sure we have really clear release notes when we go to 7.x so it's clear who's impacted.
This comment is more an FYI for the community then? There's no action for this PR correct?