Skip to content
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

Merged
merged 5 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,12 @@ public OpenIdConnectConfiguration(string json)
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = OpenIdProviderMetadataNames.TokenEndpoint, Required = Required.Default)]
public override string TokenEndpoint { get; set; }

/// <summary>
/// This base class property is not used in OpenIdConnect.
/// </summary>
[JsonIgnore]
public override string ActiveTokenEndpoint { get; set; }
Copy link
Member

@brentschmaltz brentschmaltz Jun 6, 2023

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".

Copy link
Contributor

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?


/// <summary>
/// Gets the collection of 'token_endpoint_auth_methods_supported'.
/// </summary>
Expand Down
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
};
}
Copy link
Contributor

@TimHannMSFT TimHannMSFT Jun 9, 2023

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

@TimHannMSFT TimHannMSFT Jun 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this can throw ArgumentNullException too. That would be a bit odd since in other cases where there's unexpected input it's returned as a failed ConfigurationValidationResult. Is this intentional? #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

@dannybtsai dannybtsai Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should catch other types of exceptions too?
  2. Should break or continue? #Pending

Copy link
Contributor Author

@victorm-hernandez victorm-hernandez Jun 14, 2023

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -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'.";
Copy link
Contributor

@TimHannMSFT TimHannMSFT Jun 9, 2023

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Microsoft.IdentityModel.Protocols.WsFederation
{
/// <summary>
/// Constants for WsFederation.
/// As defined in the http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html
/// </summary>
public static class WsFederationConstants
{
Expand Down Expand Up @@ -93,6 +94,7 @@ public static class Elements
public const string KeyDescriptor = "KeyDescriptor";
public const string RoleDescriptor = "RoleDescriptor";
public const string PassiveRequestorEndpoint = "PassiveRequestorEndpoint";
public const string SecurityTokenServiceEndpoint = "SecurityTokenServiceEndpoint";
public const string SpssoDescriptor = "SPSSODescriptor";
}

Expand Down
Loading