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..be1e0e6396 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; @@ -27,12 +28,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; } /// @@ -46,14 +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 == validationParameters.ValidAudience || 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 == audience); if (found) { return true; 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/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/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs b/src/Security/src/Authentication.CloudFoundryCore/ServiceCollectionExtensions.cs index 04b03fc4a9..7ac40365dc 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, configurer: null); /// /// Adds options and services to use Cloud Foundry container identity certificates /// /// Service collection public static void AddCloudFoundryContainerIdentity(this IServiceCollection services) + => AddCloudFoundryContainerIdentity(services, configurer: null); + + /// + /// Adds options and services to use Cloud Foundry container identity certificates + /// + /// Service collection + /// Used to configure the + public static void AddCloudFoundryContainerIdentity(this IServiceCollection services, Action configurer) { 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"; + configurer?.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 @@ -85,17 +106,27 @@ public static void AddCloudFoundryCertificateAuth(this IServiceCollection servic /// 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 + /// + /// Service collection + /// An identifier for this authentication mechanism. Default value is + /// 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(); + services.AddCloudFoundryContainerIdentity(configureCertificateForwarding); services .AddAuthentication(authenticationScheme) - .AddCloudFoundryIdentityCertificate(authenticationScheme, configurer); + .AddCloudFoundryIdentityCertificate(authenticationScheme, configureMutualTlsAuthentication); services.AddAuthorization(cfg => { 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..ddfcce56d2 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 result = cftv.ValidateAudience(audiences, null, null); + Assert.True(result); + + audiences = new[] { "invalid-audience" }; + result = cftv.ValidateAudience(audiences, null, null); + Assert.False(result); + } + + [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 result = cftv.ValidateAudience(audiences, null, validationParametersSingleAudience); + Assert.True(result, "Valid from single audience in TokenValidationParameters"); + + var validationParametersListOfAudiences = new TokenValidationParameters + { + ValidAudiences = new[] { "some-api" } + }; + result = cftv.ValidateAudience(audiences, null, validationParametersListOfAudiences); + Assert.True(result, "Valid from audience list in TokenValidationParameters"); + + audiences = new[] { "invalid-audience" }; + 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 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 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