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);
}
}
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:
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);
}
}