From a068fc60b6d9247f705d7e59176b68b3c40632c9 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Fri, 22 Aug 2025 15:03:34 -0500 Subject: [PATCH 1/5] Enhance support for local, non-https UAA with OIDC and JWT Allow configuration-binding for MetadataAddress and RequireHttpsMetadata Allow more places to set valid audience, only set ValidateAudience if one or more audience has been configured (MSFT always runs it if present) --- .../CloudFoundryHelper.cs | 18 ++++---- .../CloudFoundryTokenValidator.cs | 12 +++--- .../CloudFoundryJwtBearerOptions.cs | 10 +++-- .../CloudFoundryOpenIdConnectConfigurer.cs | 2 + .../CloudFoundryHelperTest.cs | 16 ++++++++ .../CloudFoundryTokenValidatorTest.cs | 41 +++++++++++++++++++ .../CloudFoundryJwtBearerOptionsTest.cs | 4 +- ...CloudFoundryOpenIdConnectConfigurerTest.cs | 13 +++++- 8 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryHelper.cs b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryHelper.cs index 7709557806..3bc4470dd0 100644 --- a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryHelper.cs +++ b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryHelper.cs @@ -62,19 +62,17 @@ public static TokenValidationParameters GetTokenValidationParameters(TokenValida var tokenValidator = new CloudFoundryTokenValidator(options ?? new AuthServerOptions()); parameters.IssuerValidator = tokenValidator.ValidateIssuer; - parameters.AudienceValidator = tokenValidator.ValidateAudience; - - CloudFoundryTokenKeyResolver tkr; - if (options is null) - { - tkr = new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates); - } - else + if (!string.IsNullOrEmpty(parameters.ValidAudience) || parameters.ValidAudiences != null) { - tkr = new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates, options.ClientTimeout); + parameters.ValidateAudience = true; + parameters.AudienceValidator = tokenValidator.ValidateAudience; } - parameters.IssuerSigningKeyResolver = tkr.ResolveSigningKey; + var tokenKeyResolver = options is null + ? new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates) + : new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates, options.ClientTimeout); + + parameters.IssuerSigningKeyResolver = tokenKeyResolver.ResolveSigningKey; return parameters; } diff --git a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs index 4c42242d59..1f48ae9488 100644 --- a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs +++ b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs @@ -27,12 +27,7 @@ public CloudFoundryTokenValidator(AuthServerOptions options = null) /// The issuer, if valid, else public virtual string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters) { - if (issuer.Contains("uaa")) - { - return issuer; - } - - return null; + return issuer.Contains("uaa") ? issuer : null; } /// @@ -51,6 +46,11 @@ public virtual bool ValidateAudience(IEnumerable audiences, SecurityToke return true; } + if (validationParameters != null && (audience.Equals(validationParameters.ValidAudience) || validationParameters.ValidAudiences?.Contains(audience) == true)) + { + return true; + } + if (_options.AdditionalAudiences != null) { var found = _options.AdditionalAudiences.Any(x => x.Equals(audience)); diff --git a/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryJwtBearerOptions.cs b/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryJwtBearerOptions.cs index c1ef7ef5e7..ecd0672008 100644 --- a/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryJwtBearerOptions.cs +++ b/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryJwtBearerOptions.cs @@ -24,7 +24,7 @@ public CloudFoundryJwtBearerOptions() public bool Validate_Certificates { get; set; } = true; /// - /// Gets or sets a value indicating whether gets a value indicating whether to validate auth server certificate + /// Gets or sets a value indicating whether to validate auth server certificate /// public bool ValidateCertificates { @@ -39,7 +39,11 @@ public bool ValidateCertificates public void SetEndpoints(string authDomain) { - JwtKeyUrl = (!string.IsNullOrWhiteSpace(authDomain)) ? - authDomain + CloudFoundryDefaults.JwtTokenUri : JwtKeyUrl; + if (string.IsNullOrEmpty(JwtKeyUrl) || JwtKeyUrl == $"http://{CloudFoundryDefaults.OAuthServiceUrl}{CloudFoundryDefaults.JwtTokenUri}") + { + JwtKeyUrl = !string.IsNullOrWhiteSpace(authDomain) + ? authDomain + CloudFoundryDefaults.JwtTokenUri + : JwtKeyUrl; + } } } \ No newline at end of file diff --git a/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryOpenIdConnectConfigurer.cs b/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryOpenIdConnectConfigurer.cs index 4af5df0629..62ab14128d 100644 --- a/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryOpenIdConnectConfigurer.cs +++ b/src/Security/src/Authentication.CloudFoundryCore/CloudFoundryOpenIdConnectConfigurer.cs @@ -43,6 +43,8 @@ internal static void Configure(SsoServiceInfo si, OpenIdConnectOptions oidcOptio oidcOptions.BackchannelHttpHandler = CloudFoundryHelper.GetBackChannelHandler(cfOptions.ValidateCertificates); oidcOptions.CallbackPath = cfOptions.CallbackPath; oidcOptions.ClaimsIssuer = cfOptions.ClaimsIssuer; + oidcOptions.MetadataAddress = cfOptions.MetadataAddress; + oidcOptions.RequireHttpsMetadata = cfOptions.RequireHttpsMetadata; oidcOptions.ResponseType = cfOptions.ResponseType; oidcOptions.SaveTokens = cfOptions.SaveTokens; oidcOptions.SignInScheme = cfOptions.SignInScheme; diff --git a/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryHelperTest.cs b/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryHelperTest.cs index b6a018a9ab..3d0a5ef48e 100644 --- a/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryHelperTest.cs +++ b/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryHelperTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using Microsoft.IdentityModel.Tokens; using System; using System.Text.Json; using Xunit; @@ -25,12 +26,27 @@ public void GetTokenValidationParameters_ReturnsExpected() { var parameters = CloudFoundryHelper.GetTokenValidationParameters(null, "https://foo.bar.com/keyurl", null, false); Assert.False(parameters.ValidateAudience, "Audience validation should not be enabled by default"); + Assert.Null(parameters.AudienceValidator); Assert.True(parameters.ValidateIssuer, "Issuer validation should be enabled by default"); Assert.NotNull(parameters.IssuerValidator); Assert.True(parameters.ValidateLifetime, "Token lifetime validation should be enabled by default"); Assert.NotNull(parameters.IssuerSigningKeyResolver); } + [Fact] + public void GetTokenValidationParameters_ValidatesAudienceWhenProvided() + { + var tokenValidationParameters = + new TokenValidationParameters { ValidAudience = "some-api", ValidAudiences = new[] { "another-audience" } }; + var parameters = CloudFoundryHelper.GetTokenValidationParameters(tokenValidationParameters, "https://foo.bar.com/keyurl", null, false); + + Assert.True(parameters.ValidateAudience, "Audience validation should be enabled when ValidAudience or ValidAudiences are provided"); + Assert.NotNull(parameters.AudienceValidator); + Assert.True(parameters.AudienceValidator(new[] { "some-api" }, null, tokenValidationParameters), "Validates single audience"); + Assert.True(parameters.AudienceValidator(new[] { "another-audience" }, null, tokenValidationParameters), "Validates from list of audiences"); + Assert.False(parameters.AudienceValidator(new[] { "invalid-audience" }, null, tokenValidationParameters), "Unlisted audience is not valid"); + } + [Fact] public void GetExpTime_FindsTime() { diff --git a/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs b/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs index ec70ea02dc..6d63f4f8da 100644 --- a/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs +++ b/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using Microsoft.IdentityModel.Tokens; using Xunit; namespace Steeltoe.Security.Authentication.CloudFoundry.Test; @@ -19,4 +20,44 @@ public void ValidateIssuer_ValidatesCorrectly() Assert.NotNull(uaaResult); Assert.Null(foobarResult); } + + [Fact] + public void ValidateAudience_ValidatesFromAuthServerOptionsCorrectly() + { + var cftv = new CloudFoundryTokenValidator(new AuthServerOptions + { + ClientId = "test-client", + AdditionalAudiences = new[] { "additional-audience" } + }); + var audiences = new[] { "profile", "some-api", "additional-audience" }; + var validAudience = cftv.ValidateAudience(audiences, null, null); + Assert.True(validAudience); + + audiences = new[] { "invalid-audience" }; + var invalidAudience = cftv.ValidateAudience(audiences, null, null); + Assert.False(invalidAudience); + } + + [Fact] + public void ValidateAudience_ValidatesFromTokenValidationParameters() + { + var cftv = new CloudFoundryTokenValidator(); + var audiences = new[] { "profile", "some-api", "additional-audience" }; + var validationParametersSingleAudience = new TokenValidationParameters { ValidAudience = "some-api" }; + var validAudience = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.True(validAudience, "Valid from single audience in TokenValidationParameters"); + + var validationParametersListOfAudiences = new TokenValidationParameters + { + ValidAudiences = new[] { "some-api" } + }; + validAudience = cftv.ValidateAudience(audiences, null, validationParametersListOfAudiences); + Assert.True(validAudience, "Valid from audience list in TokenValidationParameters"); + + audiences = new[] { "invalid-audience" }; + var invalidAudience = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.False(invalidAudience, "Invalid from single audience in TokenValidationParameters"); + invalidAudience = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.False(invalidAudience, "Invalid from audience list in TokenValidationParameters"); + } } \ No newline at end of file diff --git a/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryJwtBearerOptionsTest.cs b/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryJwtBearerOptionsTest.cs index c50305ca31..5565c03430 100644 --- a/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryJwtBearerOptionsTest.cs +++ b/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryJwtBearerOptionsTest.cs @@ -18,14 +18,14 @@ public static TheoryData SetEndpointsData() data.Add(string.Empty, DEFAULT_JWT_TOKEN_URL); data.Add(" ", DEFAULT_JWT_TOKEN_URL); - data.Add(default, DEFAULT_JWT_TOKEN_URL); + data.Add(null, DEFAULT_JWT_TOKEN_URL); data.Add(newDomain, newDomain + CloudFoundryDefaults.JwtTokenUri); return data; } [Fact] - public void DefaultConstructor_SetsupDefaultOptions() + public void DefaultConstructor_SetsUpDefaultOptions() { var opts = new CloudFoundryJwtBearerOptions(); diff --git a/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryOpenIdConnectConfigurerTest.cs b/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryOpenIdConnectConfigurerTest.cs index 997ed0e2e6..2544b674af 100644 --- a/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryOpenIdConnectConfigurerTest.cs +++ b/src/Security/test/Authentication.CloudFoundryCore.Test/CloudFoundryOpenIdConnectConfigurerTest.cs @@ -17,9 +17,18 @@ public class CloudFoundryOpenIdConnectConfigurerTest public void Configure_NoServiceInfo_ReturnsExpected() { var oidcOptions = new OpenIdConnectOptions(); + var cloudFoundryOptions = new CloudFoundryOpenIdConnectOptions + { + Authority = "http://localhost:8080/uaa", + MetadataAddress = "http://localhost:8080/.well-known/openid-configuration", + RequireHttpsMetadata = false, + ValidateCertificates = false + }; - CloudFoundryOpenIdConnectConfigurer.Configure(null, oidcOptions, new CloudFoundryOpenIdConnectOptions() { ValidateCertificates = false }); + CloudFoundryOpenIdConnectConfigurer.Configure(null, oidcOptions, cloudFoundryOptions); + Assert.Equal("http://localhost:8080/uaa", oidcOptions.Authority); + Assert.Equal("http://localhost:8080/.well-known/openid-configuration", oidcOptions.MetadataAddress); Assert.Equal(CloudFoundryDefaults.AuthenticationScheme, oidcOptions.ClaimsIssuer); Assert.Equal(CloudFoundryDefaults.ClientId, oidcOptions.ClientId); Assert.Equal(CloudFoundryDefaults.ClientSecret, oidcOptions.ClientSecret); @@ -28,6 +37,7 @@ public void Configure_NoServiceInfo_ReturnsExpected() Assert.Equal(CookieAuthenticationDefaults.AuthenticationScheme, oidcOptions.SignInScheme); Assert.False(oidcOptions.SaveTokens); Assert.NotNull(oidcOptions.BackchannelHttpHandler); + Assert.False(oidcOptions.RequireHttpsMetadata); } [Fact] @@ -49,5 +59,6 @@ public void Configure_WithServiceInfo_ReturnsExpected() Assert.Equal(CookieAuthenticationDefaults.AuthenticationScheme, oidcOptions.SignInScheme); Assert.False(oidcOptions.SaveTokens); Assert.Null(oidcOptions.BackchannelHttpHandler); + Assert.True(oidcOptions.RequireHttpsMetadata); } } \ No newline at end of file From 182e26887939daad86d6a68d4fa085c2e5b55dc3 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 27 Aug 2025 08:57:02 -0500 Subject: [PATCH 2/5] Allow customization of CertificateForwardingOptions --- .../AuthenticationBuilderExtensions.cs | 5 +- .../ServiceCollectionExtensions.cs | 34 +++++++++--- .../ServiceCollectionExtensionsTest.cs | 54 +++++++++++++++++-- 3 files changed, 79 insertions(+), 14 deletions(-) diff --git a/src/Security/src/Authentication.CloudFoundryCore/AuthenticationBuilderExtensions.cs b/src/Security/src/Authentication.CloudFoundryCore/AuthenticationBuilderExtensions.cs index c6a6a49505..2a65501fcc 100644 --- a/src/Security/src/Authentication.CloudFoundryCore/AuthenticationBuilderExtensions.cs +++ b/src/Security/src/Authentication.CloudFoundryCore/AuthenticationBuilderExtensions.cs @@ -282,10 +282,7 @@ public static AuthenticationBuilder AddCloudFoundryIdentityCertificate(this Auth /// configured to use application identity certificates public static AuthenticationBuilder AddCloudFoundryIdentityCertificate(this AuthenticationBuilder builder, string authenticationScheme, Action configurer) { - builder.AddMutualTls(authenticationScheme, options => - { - configurer?.Invoke(options); - }); + builder.AddMutualTls(authenticationScheme, configurer); return builder; } diff --git a/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs index 04b03fc4a9..2eb42f5d62 100644 --- a/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs +++ b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -23,13 +24,21 @@ public static class ServiceCollectionExtensions /// Application Configuration [Obsolete("The IConfiguration parameter is not used")] public static void AddCloudFoundryContainerIdentity(this IServiceCollection services, IConfiguration configuration) - => AddCloudFoundryContainerIdentity(services); + => AddCloudFoundryContainerIdentity(services, configureCertificateForwarding: null); /// /// Adds options and services to use Cloud Foundry container identity certificates /// /// Service collection public static void AddCloudFoundryContainerIdentity(this IServiceCollection services) + => AddCloudFoundryContainerIdentity(services, configureCertificateForwarding: null); + + /// + /// Adds options and services to use Cloud Foundry container identity certificates + /// + /// Service collection + /// Customize the + public static void AddCloudFoundryContainerIdentity(this IServiceCollection services, Action configureCertificateForwarding) { if (services == null) { @@ -43,7 +52,11 @@ public static void AddCloudFoundryContainerIdentity(this IServiceCollection serv services.AddSingleton(); services.AddHostedService(); services.AddSingleton(); - services.AddCertificateForwarding(opt => opt.CertificateHeader = "X-Forwarded-Client-Cert"); + services.AddCertificateForwarding(opt => + { + opt.CertificateHeader = "X-Forwarded-Client-Cert"; + configureCertificateForwarding?.Invoke(opt); + }); } /// @@ -68,7 +81,15 @@ public static void AddCloudFoundryCertificateAuth(this IServiceCollection servic /// Service collection /// Used to configure the public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, Action configurer) - => AddCloudFoundryCertificateAuth(services, CertificateAuthenticationDefaults.AuthenticationScheme, configurer); + => AddCloudFoundryCertificateAuth(services, CertificateAuthenticationDefaults.AuthenticationScheme, configurer, null); + + /// + /// Adds options and services for Cloud Foundry container identity certificates along with certificate-based authentication and authorization + /// + /// Service collection + /// Used to configure the + public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, Action configurer) + => AddCloudFoundryCertificateAuth(services, CertificateAuthenticationDefaults.AuthenticationScheme, null, configurer); /// /// Adds options and services for Cloud Foundry container identity certificates along with certificate-based authentication and authorization @@ -76,7 +97,7 @@ public static void AddCloudFoundryCertificateAuth(this IServiceCollection servic /// Service collection /// An identifier for this authentication mechanism. Default value is public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme) - => AddCloudFoundryCertificateAuth(services, authenticationScheme, null); + => AddCloudFoundryCertificateAuth(services, authenticationScheme, null, null); /// /// Adds options and services for Cloud Foundry container identity certificates along with certificate-based authentication and authorization @@ -84,14 +105,15 @@ public static void AddCloudFoundryCertificateAuth(this IServiceCollection servic /// Service collection /// An identifier for this authentication mechanism. Default value is /// Used to configure the - public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme, Action configurer) + /// Customize the + public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme, Action configurer, Action forwardingConfigurer) { if (services is null) { throw new ArgumentNullException(nameof(services)); } - services.AddCloudFoundryContainerIdentity(); + services.AddCloudFoundryContainerIdentity(forwardingConfigurer); services .AddAuthentication(authenticationScheme) diff --git a/src/Security/test/Authentication.CloudFoundryCore.Test/ServiceCollectionExtensionsTest.cs b/src/Security/test/Authentication.CloudFoundryCore.Test/ServiceCollectionExtensionsTest.cs index 5c4c3eb978..63e404773c 100644 --- a/src/Security/test/Authentication.CloudFoundryCore.Test/ServiceCollectionExtensionsTest.cs +++ b/src/Security/test/Authentication.CloudFoundryCore.Test/ServiceCollectionExtensionsTest.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -10,6 +12,7 @@ using Steeltoe.Common.Security; using Steeltoe.Security.Authentication.Mtls; using System; +using System.Security.Cryptography.X509Certificates; using Xunit; namespace Steeltoe.Security.Authentication.CloudFoundry.Test; @@ -39,11 +42,54 @@ public void AddCloudFoundryCertificateAuth_AddsServices() Assert.NotNull(provider.GetRequiredService>()); Assert.NotNull(provider.GetRequiredService()); Assert.NotNull(provider.GetRequiredService()); - var mtlsOpts = provider.GetRequiredService>(); - Assert.NotNull(mtlsOpts); + Assert.NotNull(provider.GetRequiredService>()); + } + + [Fact] + public void AddCloudFoundryCertificateAuth_SetsExpectedOptions() + { + var services = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + services.AddSingleton(config); + services.AddLogging(); + + services.AddCloudFoundryCertificateAuth(); + var provider = services.BuildServiceProvider(); + + var mutualTlsOptions = provider.GetRequiredService>(); + var namedTlsOptions = mutualTlsOptions.Get(CertificateAuthenticationDefaults.AuthenticationScheme); // confirm Events was set (in MutualTlsAuthenticationOptionsPostConfigurer.cs) vs being null by default - Assert.NotNull(mtlsOpts.Value.Events); - Assert.Null(new MutualTlsAuthenticationOptions().Events); + var defaultMTlsOptions = new MutualTlsAuthenticationOptions(); + Assert.NotNull(namedTlsOptions.Events); + Assert.Null(defaultMTlsOptions.Events); + Assert.Equal(defaultMTlsOptions.RevocationMode, namedTlsOptions.RevocationMode); + + var certificateForwardingOptions = provider.GetRequiredService>(); + Assert.Equal("X-Forwarded-Client-Cert", certificateForwardingOptions.Value.CertificateHeader); + } + + [Fact] + public void AddCloudFoundryCertificateAuth_AllowsOptionsCustomization() + { + var defaultMTlsOptions = new MutualTlsAuthenticationOptions(); + var services = new ServiceCollection(); + var config = new ConfigurationBuilder().AddInMemoryCollection().Build(); + services.AddSingleton(config); + services.AddLogging(); + + static void TlsOptionsConfigurer(MutualTlsAuthenticationOptions options) => options.RevocationMode = X509RevocationMode.NoCheck; + static void CertificateForwardingConfigurer(CertificateForwardingOptions options) => options.CertificateHeader = "some-custom-header"; + + services.AddCloudFoundryCertificateAuth(CertificateAuthenticationDefaults.AuthenticationScheme, TlsOptionsConfigurer, CertificateForwardingConfigurer); + var provider = services.BuildServiceProvider(); + + var mutualTlsOptions = provider.GetRequiredService>(); + var namedTlsOptions = mutualTlsOptions.Get(CertificateAuthenticationDefaults.AuthenticationScheme); + Assert.Equal(X509RevocationMode.NoCheck, namedTlsOptions.RevocationMode); + Assert.NotEqual(defaultMTlsOptions.RevocationMode, namedTlsOptions.RevocationMode); + + var certForwardOptions = provider.GetRequiredService>(); + Assert.Equal("some-custom-header", certForwardOptions.Value.CertificateHeader); } } \ No newline at end of file From 9514fbfc6d875dcea92bbf6ba1f6f44c23662658 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 27 Aug 2025 11:34:04 -0500 Subject: [PATCH 3/5] re-introduce previous signature --- .../ServiceCollectionExtensions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs index 2eb42f5d62..9775959f68 100644 --- a/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs +++ b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs @@ -99,6 +99,15 @@ public static void AddCloudFoundryCertificateAuth(this IServiceCollection servic public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme) => AddCloudFoundryCertificateAuth(services, authenticationScheme, null, null); + /// + /// Adds options and services for Cloud Foundry container identity certificates along with certificate-based authentication and authorization + /// + /// Service collection + /// An identifier for this authentication mechanism. Default value is + /// Used to configure the + public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme, Action configurer) + => AddCloudFoundryCertificateAuth(services, authenticationScheme, configurer, null); + /// /// Adds options and services for Cloud Foundry container identity certificates along with certificate-based authentication and authorization /// From f26f1553a64ce391d1870f38683975ae1cfc48c3 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 27 Aug 2025 11:43:24 -0500 Subject: [PATCH 4/5] pass string comparison param --- .../CloudFoundryTokenValidator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs index 1f48ae9488..b8536cab56 100644 --- a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs +++ b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.IdentityModel.Tokens; +using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; @@ -41,19 +42,19 @@ public virtual bool ValidateAudience(IEnumerable audiences, SecurityToke { foreach (var audience in audiences) { - if (audience.Equals(_options.ClientId)) + if (audience.Equals(_options.ClientId, StringComparison.Ordinal)) { return true; } - if (validationParameters != null && (audience.Equals(validationParameters.ValidAudience) || validationParameters.ValidAudiences?.Contains(audience) == true)) + if (validationParameters != null && (audience.Equals(validationParameters.ValidAudience, StringComparison.Ordinal) || validationParameters.ValidAudiences?.Contains(audience) == true)) { return true; } if (_options.AdditionalAudiences != null) { - var found = _options.AdditionalAudiences.Any(x => x.Equals(audience)); + var found = _options.AdditionalAudiences.Any(x => x.Equals(audience, StringComparison.Ordinal)); if (found) { return true; From 51da9b20385944f27c8d80b3ca43c22470a40b18 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Thu, 28 Aug 2025 08:27:07 -0500 Subject: [PATCH 5/5] PR feedback --- .../CloudFoundryTokenValidator.cs | 4 ++-- .../ServiceCollectionExtensions.cs | 20 ++++++++-------- .../CloudFoundryTokenValidatorTest.cs | 24 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs index b8536cab56..be1e0e6396 100644 --- a/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs +++ b/src/Security/src/Authentication.CloudFoundryBase/CloudFoundryTokenValidator.cs @@ -47,14 +47,14 @@ public virtual bool ValidateAudience(IEnumerable audiences, SecurityToke return true; } - if (validationParameters != null && (audience.Equals(validationParameters.ValidAudience, StringComparison.Ordinal) || validationParameters.ValidAudiences?.Contains(audience) == true)) + if (validationParameters != null && (audience == validationParameters.ValidAudience || validationParameters.ValidAudiences?.Contains(audience) == true)) { return true; } if (_options.AdditionalAudiences != null) { - var found = _options.AdditionalAudiences.Any(x => x.Equals(audience, StringComparison.Ordinal)); + var found = _options.AdditionalAudiences.Any(x => x == audience); if (found) { return true; diff --git a/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs index 9775959f68..7ac40365dc 100644 --- a/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs +++ b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs @@ -24,21 +24,21 @@ public static class ServiceCollectionExtensions /// Application Configuration [Obsolete("The IConfiguration parameter is not used")] public static void AddCloudFoundryContainerIdentity(this IServiceCollection services, IConfiguration configuration) - => AddCloudFoundryContainerIdentity(services, configureCertificateForwarding: null); + => AddCloudFoundryContainerIdentity(services, configurer: null); /// /// Adds options and services to use Cloud Foundry container identity certificates /// /// Service collection public static void AddCloudFoundryContainerIdentity(this IServiceCollection services) - => AddCloudFoundryContainerIdentity(services, configureCertificateForwarding: null); + => AddCloudFoundryContainerIdentity(services, configurer: null); /// /// Adds options and services to use Cloud Foundry container identity certificates /// /// Service collection - /// Customize the - public static void AddCloudFoundryContainerIdentity(this IServiceCollection services, Action configureCertificateForwarding) + /// Used to configure the + public static void AddCloudFoundryContainerIdentity(this IServiceCollection services, Action configurer) { if (services == null) { @@ -55,7 +55,7 @@ public static void AddCloudFoundryContainerIdentity(this IServiceCollection serv services.AddCertificateForwarding(opt => { opt.CertificateHeader = "X-Forwarded-Client-Cert"; - configureCertificateForwarding?.Invoke(opt); + configurer?.Invoke(opt); }); } @@ -113,20 +113,20 @@ public static void AddCloudFoundryCertificateAuth(this IServiceCollection servic /// /// Service collection /// An identifier for this authentication mechanism. Default value is - /// Used to configure the - /// Customize the - public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme, Action configurer, Action forwardingConfigurer) + /// Used to configure the + /// Used to configure the + public static void AddCloudFoundryCertificateAuth(this IServiceCollection services, string authenticationScheme, Action configureMutualTlsAuthentication, Action configureCertificateForwarding) { if (services is null) { throw new ArgumentNullException(nameof(services)); } - services.AddCloudFoundryContainerIdentity(forwardingConfigurer); + services.AddCloudFoundryContainerIdentity(configureCertificateForwarding); services .AddAuthentication(authenticationScheme) - .AddCloudFoundryIdentityCertificate(authenticationScheme, configurer); + .AddCloudFoundryIdentityCertificate(authenticationScheme, configureMutualTlsAuthentication); services.AddAuthorization(cfg => { diff --git a/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs b/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs index 6d63f4f8da..ddfcce56d2 100644 --- a/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs +++ b/src/Security/test/Authentication.CloudFoundryBase.Test/CloudFoundryTokenValidatorTest.cs @@ -30,12 +30,12 @@ public void ValidateAudience_ValidatesFromAuthServerOptionsCorrectly() AdditionalAudiences = new[] { "additional-audience" } }); var audiences = new[] { "profile", "some-api", "additional-audience" }; - var validAudience = cftv.ValidateAudience(audiences, null, null); - Assert.True(validAudience); + var result = cftv.ValidateAudience(audiences, null, null); + Assert.True(result); audiences = new[] { "invalid-audience" }; - var invalidAudience = cftv.ValidateAudience(audiences, null, null); - Assert.False(invalidAudience); + result = cftv.ValidateAudience(audiences, null, null); + Assert.False(result); } [Fact] @@ -44,20 +44,20 @@ public void ValidateAudience_ValidatesFromTokenValidationParameters() var cftv = new CloudFoundryTokenValidator(); var audiences = new[] { "profile", "some-api", "additional-audience" }; var validationParametersSingleAudience = new TokenValidationParameters { ValidAudience = "some-api" }; - var validAudience = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); - Assert.True(validAudience, "Valid from single audience in TokenValidationParameters"); + var result = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.True(result, "Valid from single audience in TokenValidationParameters"); var validationParametersListOfAudiences = new TokenValidationParameters { ValidAudiences = new[] { "some-api" } }; - validAudience = cftv.ValidateAudience(audiences, null, validationParametersListOfAudiences); - Assert.True(validAudience, "Valid from audience list in TokenValidationParameters"); + result = cftv.ValidateAudience(audiences, null, validationParametersListOfAudiences); + Assert.True(result, "Valid from audience list in TokenValidationParameters"); audiences = new[] { "invalid-audience" }; - var invalidAudience = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); - Assert.False(invalidAudience, "Invalid from single audience in TokenValidationParameters"); - invalidAudience = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); - Assert.False(invalidAudience, "Invalid from audience list in TokenValidationParameters"); + result = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.False(result, "Invalid from single audience in TokenValidationParameters"); + result = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.False(result, "Invalid from audience list in TokenValidationParameters"); } } \ No newline at end of file