Search code examples
c#asp.net-coreocelot

Ocelot RouteClaimsRequirement does not recognize my claims and returns 403 Forbidden


I have configured ocelot in Linux containers with multiple micro service. For restricting some of the micro services I'm using RouteClaimsRequirement. I have administrator role as claim, but when I send token with role administrator the Ocelot returns 403 Forbidden, which is the HttpCode for not meeting the criteria in RouteClaimsRequirement. If I delete the RouteClaimsRequirment from ocelot.json everything is working.

   {
  "DownstreamPathTemplate": "/api/v1/product/{everything}",
  "DownstreamScheme": "https",
  "DownstreamHostAndPorts": [
    {
      "Host": "product",
      "Port": 443
    }
  ],
  "UpstreamPathTemplate": "/product/{everything}",
  "UpstreamHttpMethod": [ "Get", "Post", "Delete" ],
  "AuthenticationOptions": {
    "AuthenticationProviderKey": "Bearer",
    "AllowedScopes": []
  },
  "RouteClaimsRequirement": {  <---- Problem Part
    "Role": "Administrator"
  },
  "DangerousAcceptAnyServerCertificateValidator": true,
  "RateLimitOptions": {
    "ClientWhitelist": [],
    "EnableRateLimiting": true,
    "Period": "5s",
    "PeriodTimespan": 6,
    "Limit": 8
  }
}

Here how the ocelot project startup class looks like:

public void ConfigureServices(IServiceCollection services)
=> services
    .AddCors()
    .AddTokenAuthentication(Configuration)
    .AddOcelot();

public static IServiceCollection AddTokenAuthentication(
    this IServiceCollection services,
    IConfiguration
     configuration,
    JwtBearerEvents events = null)
{
    var secret = configuration
        .GetSection(nameof(ApplicationSettings))
        .GetValue<string>(nameof(ApplicationSettings.Secret));

    var key = Encoding.ASCII.GetBytes(secret);

    services
        .AddAuthentication(authentication =>
        {
            authentication.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            authentication.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(bearer =>
        {
            bearer.RequireHttpsMetadata = false;
            bearer.SaveToken = true;
            bearer.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false
            };

            if (events != null)
            {
                bearer.Events = events;
            }
        });

    services.AddHttpContextAccessor();
    services.AddScoped<ICurrentUserService, CurrentUserService>();

    return services;

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app
        .UseCors(options => options
            .AllowAnyOrigin()
            .AllowAnyHeader()
            .AllowAnyMethod())
        .UseAuthentication()
        .UseAuthorization()
        .UseOcelot().Wait();
}

Token generation looks like this:

    public string GenerateToken(User user, IEnumerable<string> roles = null)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(this.applicationSettings.Secret);

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id),
            new Claim(ClaimTypes.Name, user.Email)
        };

        if (roles != null)
        {
            claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
        }

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddDays(7),
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(key),
                SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        var encryptedToken = tokenHandler.WriteToken(token);

        return encryptedToken;
    }

Decrypted token:

{
  "nameid": "e18d5f1f-a315-435c-9e38-df9f2c77ad20",
  "unique_name": "test@aa.bg",
  "role": "Administrator",
  "nbf": 1595460189,
  "exp": 1596064989,
  "iat": 1595460189
}

Solution

  • After a lot of research in the code and debugging, I manage to find the problem. It came from the claim name, it is not role, it is http://schemas.microsoft.com/ws/2008/06/identity/claims/role, but when you write it down in ocelot.json it is recognized wrongly because of the colon (:).

    Midway researching it, I find an answer in Github of a person who tackled the same problem and I copied his solution. Here is the link to the solution.

    How the problem is handled: We are writing the URL with special symbol instead of : and after that we rewrite it to the proper one, so the ocelot.json configuration does not throw the problem.

    First you need to create IClaimsAuthoriser

    public class ClaimAuthorizerDecorator : IClaimsAuthoriser
    {
        private readonly ClaimsAuthoriser _authoriser;
    
        public ClaimAuthorizerDecorator(ClaimsAuthoriser authoriser)
        {
            _authoriser = authoriser;
        }
    
        public Response<bool> Authorise(ClaimsPrincipal claimsPrincipal, Dictionary<string, string> routeClaimsRequirement, List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues)
        {
            var newRouteClaimsRequirement = new Dictionary<string, string>();
            foreach (var kvp in routeClaimsRequirement)
            {
                if (kvp.Key.StartsWith("http$//"))
                {
                    var key = kvp.Key.Replace("http$//", "http://");
                    newRouteClaimsRequirement.Add(key, kvp.Value);
                }
                else
                {
                    newRouteClaimsRequirement.Add(kvp.Key, kvp.Value);
                }
            }
    
            return _authoriser.Authorise(claimsPrincipal, newRouteClaimsRequirement, urlPathPlaceholderNameAndValues);
        }
    }
    

    After that service collection extension is needed:

    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection DecorateClaimAuthoriser(this IServiceCollection services)
        {
            var serviceDescriptor = services.First(x => x.ServiceType == typeof(IClaimsAuthoriser));
            services.Remove(serviceDescriptor);
    
            var newServiceDescriptor = new ServiceDescriptor(serviceDescriptor.ImplementationType, serviceDescriptor.ImplementationType, serviceDescriptor.Lifetime);
            services.Add(newServiceDescriptor);
    
            services.AddTransient<IClaimsAuthoriser, ClaimAuthorizerDecorator>();
    
            return services;
        }
    }
    

    In start up you define the extension after adding the ocelot

        public void ConfigureServices(IServiceCollection services)
        {
            services
                .AddCors()
                .AddTokenAuthentication(Configuration)
                .AddOcelot().AddSingletonDefinedAggregator<DashboardAggregator>();
    
            services.DecorateClaimAuthoriser();
        }
    

    In the end you need to change the configurational JSON to this:

      "RouteClaimsRequirement": {
        "http$//schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator"
      }