authenticationsignalrasp.net-core-3.1.net-8.0ocelot

After migrating the project from .NET Core 3.1 to .NET 8.0, I'm getting http 401 unauthorized


I migrated my project from .NET Core 3.1 to .NET 8 and the Ocelot library to the latest version.

Before migrating the project authentication functionality was working fine. After migrating the project to .NET 8.0, I get an error

Unauthorized 401

on this URL: https://http://localhost:3000/swaggerfiles.json

Here is my Startup.cs class:

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using Constellation.APIGateway.Authentication.Http;
using Constellation.APIGateway.Handlers;
using Constellation.APIGateway.Swagger;
using Constellation.Authentication.Entities;
using Constellation.Common.Bus.Events;
using Constellation.Common.Exceptions;
using Constellation.Common.Kafka;
using Constellation.Common.Swagger;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using NSwag.AspNetCore;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Polly;

namespace Constellation.APIGateway
{
    public class Startup
    {
        private readonly IConfiguration configuration;
        private readonly ILoggerFactory loggerFactory;

        public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
        {
            this.configuration = configuration;
            this.loggerFactory = loggerFactory;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("AllowAll",
                    builder =>
                    {
                        builder
                        .AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .WithExposedHeaders("content-range")
                        .WithExposedHeaders("content-length")
                        .WithExposedHeaders("date")
                        .WithExposedHeaders("server")
                        .WithExposedHeaders("status")
                        .WithExposedHeaders("strict-transport-security")
                        .WithExposedHeaders("partition-offsets");
                    });
            });

            services.AddControllers().AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
            services.AddKafka(configuration, loggerFactory);
            services.AddScoped<IEventHandler<SaltChanged>, SaltEventHandler>();
            services.AddOpenApiDocument(settings => SwaggerController.SetOpenApiDocumentGeneratorSettings(settings, configuration));
            services.AddSwaggerDocument(settings => SwaggerController.SetSwaggerDocumentGeneratorSettings(settings, configuration));
            services.AddAuthentication(options => { }).AddHttpAuthentication("HttpAuthentication", o => { });
            services.AddOcelot()
                    .AddSingletonDefinedAggregator<SwaggerFilesAggregator>()
                    .AddSingletonDefinedAggregator<OpenApiFilesAggregator>()
                    .AddSingletonDefinedAggregator<AuthorizationsAggregator>()
                    .AddSingletonDefinedAggregator<RolesAggregator>()
                    .AddSingletonDefinedAggregator<GroupsAggregator>()
                    .AddPolly();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IConfiguration configuration)
        {
            app.UseOpenApi();
            app.UseSwaggerUi3(settings => SwaggerController.SetSettings(settings, configuration));
            app.UseReDoc(options =>
            {
                options.Path = "/redoc";
                options.DocumentPath = "/swaggerfiles.json";
            });

            var rewriteOptions = new RewriteOptions();
            rewriteOptions.AddRedirect("^$", "swagger");
            app.UseRewriter(rewriteOptions);
            app.UseAuthentication();
            app.UseCors("AllowAll");
            app.UseWebSockets();
            
            app.UseOcelot().Wait();
        }
    }
}

This is my AddHttpAuthentication extension method:

public static AuthenticationBuilder AddHttpAuthentication(this AuthenticationBuilder builder, string authenticationScheme, Action<HttpAuthenticationOptions> configureOptions)
{
    return builder.AddScheme<HttpAuthenticationOptions, HttpAuthenticationHandler>(authenticationScheme, configureOptions);
}

HttpAuthentionOptions:

public class HttpAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string HandlerKey = "HttpAuthentication";
    public string Scheme => HandlerKey;
    public StringValues AuthKey { get; set; }
}

HttpAuthenticationHandler:

public class HttpAuthenticationHandler : AuthenticationHandler<HttpAuthenticationOptions>
{
    private readonly IConfiguration configuration;

    public HttpAuthenticationHandler(IOptionsMonitor<HttpAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IConfiguration configuration)
        : base(options, logger, encoder, clock)
    {
        Console.WriteLine("Constructor -> HttpAuthenticationHandler");
        this.configuration = configuration;
    }

    /// <summary>
    /// Called when an Authentication by this authenticator is needed
    /// </summary>
    /// <returns>Task containing the result of the Authentication try</returns>
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        Console.WriteLine("Inside Method -> HandleAuthenticateAsync");
        try
        {
            if (SaltContainer.Salt == null)
                SaltContainer.SetSaltFromAuthenticationService(configuration);
        }
        catch (Exception e)
        {
          await SetErrorResponse($"Retrieving salt from authentication service failed: {e.Message} at {e.StackTrace}",
                                 HttpStatusCode.InternalServerError);
                                 
          return AuthenticateResult.Fail(e);
        }

        if (!Request.Headers.ContainsKey("Authorization") && !Request.Query.ContainsKey("Authorization")) 
        {
            await SetUnauthorizedResponse("Authorization token was not set");
            return AuthenticateResult.Fail("Authorization token was not set");
        }

        var token = Request.Headers.ContainsKey("Authorization") ? Request.Headers["Authorization"] : Request.Query["Authorization"];
        var authenticationResult = AuthenticationService.IsClientAuthenticated(token);

        if (authenticationResult.Failure != null)
            await SetUnauthorizedResponse(authenticationResult.Failure.Message);

        return authenticationResult;
    }

    private async Task SetUnauthorizedResponse(string message)
    {
        await SetErrorResponse(message, HttpStatusCode.Unauthorized);
    }

    private async Task SetErrorResponse(string message, HttpStatusCode statusCode)
    {
        var error = new ErrorResponse(statusCode, message);
        var bytes = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(error, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
        Context.Response.ContentType = "application/json";
        Context.Response.StatusCode = (int)statusCode;
        await Context.Response.Body.WriteAsync(bytes);
    }
}

Solution

  • After upgrading our microservice from .NET Core 3.0 to .NET 8.0, we encountered an issue where the **Ocelot** library applied authentication to all routes by default. To resolve this, I implemented a custom authentication scheme and introduced a new authentication scheme, NoHttpAuthentication, within the APIGateway project.

    Key steps included:

    1. Custom Authentication Scheme: Developed and integrated a custom authentication handler to handle routes that do not require HTTP authentication.
    2. Configuration Updates: Modified both the ocelot.global.json file and Startup.cs to support the new authentication scheme and ensure that routes behave as expected without unnecessary authentication.

    This approach allowed for greater control over route-level authentication, ensuring that only the necessary routes are authenticated while others can bypass authentication as required.

    Ocelot.global.json

    {
        "Aggregates":
        [
          {
                "ReRouteKeys": [
                    ".....",
                ],
                "UpstreamPathTemplate": "/.....",
                "Aggregator": "FilesAggregator",
                "AuthenticationOptions": {
                    "AuthenticationProviderKey": "NoHttpAuthentication"
                }
          }
        ]
    }
    

    Startup.cs

    services.AddAuthentication(options => { })
            .AddScheme<AuthenticationSchemeOptions, NoHttpAuthenticationHandler>(
                "NoHttpAuthentication", 
                options => { }
            );
    

    NoHttpAuthenticationHandler.cs

    public class NoHttpAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public NoHttpAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
            ILoggerFactory logger, 
            UrlEncoder encoder, 
            ISystemClock clock) 
            : base(options, logger, encoder, clock)
        {
        }
    
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // No authentication needed, return success
            var authenticationTicket = new AuthenticationTicket(
                new ClaimsPrincipal(), 
                new AuthenticationProperties(), 
                "NoHttpAuthentication");
    
            return AuthenticateResult.Success(authenticationTicket);
        }
    }