CoreWCF icon indicating copy to clipboard operation
CoreWCF copied to clipboard

Basic Authentication - Request.Headers["Authorization"] is always null

Open enricofacchinetti opened this issue 3 years ago • 7 comments

Discussed in https://github.com/CoreWCF/CoreWCF/discussions/827

Originally posted by enricofacchinetti September 9, 2022 Hi, here is our code implementing basic authentication in CoreWCF. We are aware of the protocol's security issues and in the next future we will replace it; but in the short term we have to meet some backward compatibility requirements. In the Program.cs file, Just before to invoke the method app.UseServiceModel() we implemented a middleware to launch the basic authentication.

The method HandleAuthenticateAsync() is then invoked in a custom class inheriting AuthenticationHandler. But in this method, when it's up to fetch the Authorization header invoking Request.Headers["Authorization"], a null object is returned. We would like to point out that the other headers are present in the IHeaderDictionary Request.Headers.

We tried to set the BasicHttpSecurityMode to Transport and to TransportWithMessageCredential. But in both the cases, the result doesn't change. The client we use to call our CoreWCF service during our tests is SoapUI 5.7.0. We set the Authorization section of SoapUI 5.7.0. to Basic and we set the credentials. We tried to find something relevant in the documentation and in the forum but without success. Does anybody has any ideas on how to fix this problem? Thanks in advance for your precious help. E.

// Program.cs

` var builder = WebApplication.CreateBuilder();

builder.Configuration.AddJsonFile("libsettings.json", optional: true, reloadOnChange: true); builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); builder.Configuration.AddEnvironmentVariables();

builder.WebHost.ConfigureKestrel((context, options) => { options.AllowSynchronousIO = true; });

// Add WSDL support builder.Services.AddServiceModelServices(); builder.Services.AddServiceModelMetadata(); builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();

builder.Services.AddTransient((provider) => { var libSettings = builder.Configuration.GetSection("LibSettings").Get<LibSettings>(); var connString = builder.Configuration.GetConnectionString("MyConnString"); var contextOptions = new DbContextOptionsBuilder<TransApiContext>() .UseSqlServer(connString) .Options; return new MySoapServiceClass(libSettings, contextOptions); });

builder.Services.AddControllers();

if (ServicePointManager.SecurityProtocol.HasFlag(SecurityProtocolType.Tls12) == false) { ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; }

builder.Services.AddAuthentication("BasicAuthentication"). AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler> ("BasicAuthentication", null);

var app = builder.Build();

app.Use(async (context, next) => { // Only check for basic auth when path is for the TransportWithMessageCredential endpoint only if (context.Request.Path.StartsWithSegments("/myBaseAddress/myservice")) { // Check if currently authenticated var authResult = await context.AuthenticateAsync("BasicAuthentication"); if (authResult.None) { // If the client hasn't authenticated, send a challenge to the client and complete request await context.ChallengeAsync("BasicAuthentication"); return; } } // Call the next delegate/middleware in the pipeline. await next(context); });

app.UseServiceModel(builder => { builder.AddService<PayPerUse>(serviceOptions => { serviceOptions.DebugBehavior.IncludeExceptionDetailInFaults = true; serviceOptions.BaseAddresses.Add(new Uri("http://localhost/myBaseAddress")); serviceOptions.BaseAddresses.Add(new Uri("https://localhost/myBaseAddress")); serviceOptions.DebugBehavior.HttpsHelpPageEnabled = true; serviceOptions.DebugBehavior.HttpsHelpPageUrl = new Uri("https://localhost/myBaseAddress/help"); serviceOptions.DebugBehavior.HttpHelpPageUrl = new Uri("http://localhost/myBaseAddress/help"); }) .AddServiceEndpoint<MySoapServiceClass, IMySoapServiceClass>(new BasicHttpBinding(BasicHttpSecurityMode.Transport), "/myservice"); var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>(); serviceMetadataBehavior.HttpsGetEnabled = true; });

app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

app.Run();

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        try
        {
            var auth = (string)Request.Headers["Authorization"];

            // Authorization header is always null but the client sends it 
            if (string.IsNullOrEmpty(auth)) return Task.FromResult(AuthenticateResult.Fail("Invalid Credentials"));
        }
        catch
        {
            return Task.FromResult(AuthenticateResult.Fail("Error Occured.Authorization failed."));
        }
    }
}

`

enricofacchinetti avatar Sep 13 '22 07:09 enricofacchinetti

How are you hosting your service? If you are using IIS Express, you will need to modify the applicationhost.config file for IIS Express to allow Basic Authentication.

djmilligan avatar Sep 21 '22 20:09 djmilligan

builder.WebHost.ConfigureKestrel

spot the builder.WebHost.ConfigureKestrel in the OP's code, please.

DBJDBJ avatar Sep 22 '22 07:09 DBJDBJ

How are you hosting your service? If you are using IIS Express, you will need to modify the applicationhost.config file for IIS Express to allow Basic Authentication.

Hi, thank you for your replay, the service is locally hosted on Kestrel server and in production on IIS (inline process).

enricofacchinetti avatar Sep 24 '22 17:09 enricofacchinetti

builder.WebHost.ConfigureKestrel

spot the builder.WebHost.ConfigureKestrel in the OP's code, please.

Hi, thank you very much for your reply; sorry for my inexperience on COREWCF library, but what do you mean with: "spot the builder.WebHost.ConfigureKestrel in the OP's code"? What is OP's code? Thanks. E. F.

enricofacchinetti avatar Sep 24 '22 17:09 enricofacchinetti

Any updates on this topic?

We're currently in the process of migrating from legacy WCF, but our proof of concept has stalled due to this issue.

Here are the snippets of our code that, so far, have been unable to read the credentials sent by the client.

app.Use(async (context, next) =>
{
    string authHeader = context.Request.Headers["Authorization"]; // This is empty
    ...    
    // Code goes on...
    ...
    await next(context);
});

The full code is pretty much the same of OP*, but we even tried to get the value back directly from the Use(Func) since the AuthenticationHandler was being nulled.

The client code

var factory = new ChannelFactory<IService>(binding, endpointAddress);
factory.Credentials.UserName.UserName = "USERNAME";
factory.Credentials.UserName.Password = "PASSWORD";

factory.Open();

var client = factory.CreateChannel();

var result = client.DoTheStuff(expectedObject);

// Code goes on...

*OP: Original Poster, the person that opened the issue in this case.

cobalto avatar May 15 '24 18:05 cobalto

The client won't send an Authorization header unless the server sends an auth challenge. You can use the package idunno.Authentication.Basic to achieve this. Here is some example documentation showing how to use is. From the CoreWCF side, you need to set the client credential type to Basic. E.g. by calling basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;. This causes CoreWCF to call httpContext.AuthenticateAsync("Basic"). If either there was no authentication headers (e.g. Authorization header missing, or missing for the requested scheme), or the authentication didn't succeed (might not be a failure as some auth schemes require multiple round trips), it causes httpContext.ChallengeAsync("Basic") to be called, which causes the authentication challenge to be sent to the client, then it completes the request.

There is a special value you can set for the ClientCredentialType with slightly different behavior. If you set the ClientCredentialType to InheritedFromHost, then the AuthenticateAsync and ChallengeAsync methods are called passing null as the scheme. This has the behavior of tell asp.net core to use the default configured auth mechanism. You would use this if there's some auth mechanism outside of the typical HTTP auth mechanisms that WCF/CoreWCF don't know about, but ASP.NET Core does. Bearer authentication (e.g. with JWT tokens) would fall into this category.

If you want to do things more manually like in the code snippets above, I think the piece you are missing is calling context.ChallengeAsync() which will trigger sending the auth challenge to the client which will cause it to send the Authentication header.

mconnew avatar May 15 '24 20:05 mconnew

@mconnew, thank you for the extremely fast response! 👀

Using the idunno.Authentication.Basic made it very easy to make it work. I even got some guidance on how to add certificate authentication, which is something we're also looking into.

@enricofacchinetti here is a minimal proof of concept sample that is working without requiring any shenanigans with the soap envelope and the credentials:

using ContractsMultiFramework;
using idunno.Authentication.Basic;
using System.Net;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddServiceModelServices();
builder.Services.AddServiceModelMetadata();
builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();

builder.Services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)
                .AddBasic(options =>
                {
                    options.Realm = "MyPersonService";
                    options.Events = new BasicAuthenticationEvents
                    {
                        OnValidateCredentials = context =>
                        {
                            // Configure your own credential validation logic here
                            if (context.Username == "admin" && context.Password == "password")
                            {
                                var claims = new[]
                                {
                                    new Claim(ClaimTypes.NameIdentifier, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                                };

                                context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                                context.Success();
                            }

                            return Task.CompletedTask;
                        }
                    };
                });

builder.Services.AddAuthorization();

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Loopback, 8080);
    options.Listen(IPAddress.Loopback, 8081, listenOptions =>
    {
        listenOptions.UseHttps();
    });
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseServiceModel(serviceBuilder =>
{
    serviceBuilder.AddService<PersonService>(serviceOptions =>
    {
        serviceOptions.DebugBehavior.IncludeExceptionDetailInFaults = true;
    });

    var basicHttpBinding = new BasicHttpBinding();
    basicHttpBinding.Security.Mode = BasicHttpSecurityMode.Transport;
    basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

    // Add your service endpoints
    serviceBuilder.AddServiceEndpoint<PersonService, IPersonService>(basicHttpBinding, "/PersonService.svc");
});

var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpGetEnabled = true;

serviceMetadataBehavior.HttpGetUrl = new Uri($"http://{IPAddress.Loopback}:8080/metadata");

app.Run();

cobalto avatar May 16 '24 11:05 cobalto