Ocelot doesn't handle correctly RouteClaimsRequirement with a key as an Url

Created on 7 Nov 2018  路  5Comments  路  Source: ThreeMammals/Ocelot

While creating JWT for a user in my authentication service I use System.Security.Claims.ClaimTypes static class with defined string constants for various claims. So ClaimTypes.Role == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role":

var claims = new List<Claim>
            {
                new Claim("ID", user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username)
            };
claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));

Then, when for some ReRoute in RouteClaimsRequirement I write:
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" : "Admin"

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

such ReRoute just disappears somewhere in the guts of middleware (I didn't manage to track down where this happens) and a request results in 404 because a reroute is not found:

Error Code: UnableToFindDownstreamRouteError Message: Unable to find downstream route for path: /api/entities/, verb: POST errors found in ResponderMiddleware. Setting error response for request path:/api/entities/, request method: POST

When I use my own claim type like "Role", this works fine. So I assume there are some issues with serialization/deserialization of a string containing colons or slashes, basically as any url.

Specifications

  • Version: 12.0.1
bug needs validation

Most helpful comment

If we configure RouteClaimsRequirement like this:

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

The real result looks like:

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

That why claims checking is failed. So my solution is do not use colons. Here is my explain:

  1. Create a decorator of ClaimsAuthoriser. In the method Authorise, we have to preprocess routeClaimsRequirement before pass it to real authorizer. For safety, I suggest to clone it and transforming on the copy.
  2. Decorate the IClaimsAuthoriser is registered in container.
  3. Edit in configuration, replace colon to your own define character.

Here is pseudocode that I use / to replace :

Decorator
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);
    }
}
Decorate IClaimsAuthoriser service
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;
    }
}

// then add it to service

services.AddOcelot();
services.DecorateClaimAuthoriser(); // put it right after registering Ocelot
Update ocelot config
"RouteClaimsRequirement": {
  "http///schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin"
}

Ocelot.16.0.1

All 5 comments

I'm having this problem as well and was about to open a new issue before I found this one. I am using ClaimsIdentity.DefaultRoleClaimType.

claims.AddRange((await this.userManager.GetRolesAsync(user)).Select(o => new Claim(ClaimsIdentity.DefaultRoleClaimType, o)));

I have verified that my claim is working as intended by inspecting my token with https://jwt.io/.

Commenting to bring attention to this issue.

After doing a lot of digging in the Ocelot code it looks like this isn't caused by Ocelot, but is a limitation in how Microsoft parses json files.

I also found this relevant issue on Stackoverflow: https://stackoverflow.com/questions/50788190/is-there-a-way-to-escape-colon-in-appsetting-json-dictionary-key-in-aspnetcore-c

Our authentication system has colons in all claim keys due to its naming scheme, so it looks like we can't use claims with Ocelot.

If we configure RouteClaimsRequirement like this:

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

The real result looks like:

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

That why claims checking is failed. So my solution is do not use colons. Here is my explain:

  1. Create a decorator of ClaimsAuthoriser. In the method Authorise, we have to preprocess routeClaimsRequirement before pass it to real authorizer. For safety, I suggest to clone it and transforming on the copy.
  2. Decorate the IClaimsAuthoriser is registered in container.
  3. Edit in configuration, replace colon to your own define character.

Here is pseudocode that I use / to replace :

Decorator
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);
    }
}
Decorate IClaimsAuthoriser service
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;
    }
}

// then add it to service

services.AddOcelot();
services.DecorateClaimAuthoriser(); // put it right after registering Ocelot
Update ocelot config
"RouteClaimsRequirement": {
  "http///schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin"
}

Ocelot.16.0.1

Big thanks @tmvan I started on your path and find your solution midway. Great approach and thanks for your sharing !

Was this page helpful?
0 / 5 - 0 ratings