Ocelot compares an array of string to a single claim value when using RouteClaimsRequirement

Created on 7 Jun 2018  路  16Comments  路  Source: ThreeMammals/Ocelot

Actual Behavior / Motivation for New Feature

I wanted to check the user's role claim using RouteClaimsRequirement but Ocelot returns 403 for all requests.

    "RouteClaimsRequirement": {
      "Role": "roles.admin"
    }

It turns out that my user has multiple role claims and IdentityServer4 is saving them into the Role claim as a single array of strings. Ocelot tries to compare the array string to the authorization value as seen in the following logs:

warn: Ocelot.Responder.Middleware.ResponderMiddleware[0]
requestId: 0HLECCH4A1P6G:00000002, previousRequestId: no previous request id, message: Error Code: ClaimValueNotAuthorisedError Message: claim value: System.Collections.Generic.List`1[System.String] is not the same as required value: roles.admin for type: Role errors found in ResponderMiddleware. Setting error response for request path:/api/b, request method: GET

{
  "nbf": 1528369572,
  "exp": 1528373172,
  "iss": "identity.api",
  "aud": [
    "identity.api/resources",
    "XXXXX",
    "XXXXX"
  ],
  "client_id": "XXXXXX",
  "sub": "44282fc2-c493-426a-9d0c-dc3c0d0418ff",
  "auth_time": 1528369569,
  "idp": "local",
  "role": [
    "roles.admin",
    "roles.staff"
  ],
  "name": "admin",
  "email": "XXXX",
  "scope": [
    "openid",
    "profile",
    "XXX.scopes.full_access",
    "XXX.scopes.full_access"
  ],
  "amr": [
    "pwd"
  ]
}

Expected Behavior / New Feature

RouteClaimsRequirement should be able to check if the value matches any of the role claims. Additionally, supplying an array for value should match every single one.

Steps to Reproduce the Problem

  1. Using ASP.NET Identity and IdentityServer4, assign two or more roles to user.
  2. Log in using that user account.
  3. Check if Ocelot is able to route requests from web client.

The issue is somewhat similar to #240.

Specifications

  • Version: 7.0.4
  • Platform: ASP.NET Core 2.1, IdentityServer4 2.2.0
bug good first issue help wanted small effort

Most helpful comment

I've had the same problem (Ocelot v7.0.6). I've managed to solve it by setting in ocelot config:
"RouteClaimsRequirement": { "role": "superuser" }
Name of claim should be LOWERCASE as in my JWT token. Now it works how it should :)

All 16 comments

@ctee-cormant thanks for noticing this. Looks like a bug. I will look at it asap. If you want to have a go at fixing it yourself I would appreciate any help! 馃槃

@ctee-cormant which version of Ocelot are you using from NuGet?

Hi @TomPallister,

We're using 7.0.4 version of Ocelot.

@ctee-cormant I can't replicate this issue :( would you be able to paste your configuration json, startup.cs and program.cs. Also could you show me how the roles have been assigned to the user in IdentityServer?

@TomPallister I think it's important that the JWT token claim for role have an array value. I have a custom ProfileService that does the following:

    public class ProfileService : DefaultProfileService, IProfileService
    {
        UserManager<User> _userManager;

        public ProfileService(
            UserManager<User> userManager, 
            ILogger<ProfileService> logger) 
            : base(logger)
        {
            _userManager = userManager;
        }

        override async public Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            await base.GetProfileDataAsync(context);

            var user = _userManager.GetUserAsync(context.Subject).Result;

           context.IssuedClaims.Add(new Claim("role", "Administrator"));
           context.IssuedClaims.Add(new Claim("role", "Contributor"));
        }

        override async public Task IsActiveAsync(IsActiveContext context)
        {
            await base.IsActiveAsync(context);

            var user = _userManager.GetUserAsync(context.Subject).Result;

            context.IsActive = (user != null) && user.Enabled;
        }
    }

And here's my Ocelot configuration:

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "catalog.api",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/api/c/{everything}",
      "UpstreamHttpMethod": [],
      "RouteClaimsRequirement": {
        "Role": "Contributor"
      }
    },
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "users.api",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/api/u/{everything}",
      "UpstreamHttpMethod": [],
      "RouteClaimsRequirement": {
        "Role": "Administrator"
      }
    }
  ],
    "GlobalConfiguration": {
      "RequestIdKey": "OcRequestId",
      "AdministrationPath": "/administration"
    }
  }

@ctee-cormant I cannot replicate this issue at all :( I've made a test that replicates the issue using the same code as your ProfileService.

Can you paste your projects csproj file? I'm interested to see the dependencies and version of .net you are using?

Thanks @TomPallister for helping me with this. Here's my csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <UserSecretsId>XXXX</UserSecretsId>
    <DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
    <RootNamespace>Identity.API</RootNamespace>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.2" />
    <PackageReference Include="FluentValidation" Version="7.6.0" />
    <PackageReference Include="IdentityServer4" Version="2.2.0" />
    <PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.1.0" />
    <PackageReference Include="IdentityServer4.EntityFramework" Version="2.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.DataProtection.AzureStorage" Version="2.1.0" />
    <PackageReference Include="WindowsAzure.Storage" Version="9.2.0" />
  </ItemGroup>

</Project>

Thanks for this @ctee-cormant it might be something to do with netcoreapp2.1..I am still using 2.0 or it could be the indentity server packages. I will investigate further.

Are you on Windows, Mac or Linux?

@TomPallister I'm using Windows for development but we'll be deploying this to a kubernetes cluster for production.

@ctee-cormant thanks! I haven't tested on Windows yet. I will try ASAP!

@ctee-cormant I still havent managed to replicate this! Could you possibly try with netcoreapp2.0 instead of netcoreapp2.1 and see what happens?

Also would be super useful if you could upload your source code somewhere for me to look at.

Sorry this is taking a long time!

@ctee-cormant I'm sorry but I haven't been able to replicate this issue on windows using netcoreapp2.1.

I have a theory that when the claim is be stored something is calling ToString on a list and it is getting persisted like that in the token. I think this because Ocelot gets the claims like this

            values.AddRange(claims.Where(x => x.Type == claimType).Select(x => x.Value).ToList());

claims is of type System.Security.Claims.Claim and Value is of type string so I don't think it is possible that Ocelot is calling ToString() on a list. After the code above Ocelot doesn't do anything with the values property apart from a contains on it.

In short I don't think the problem is with Ocelot it is with whatever is putting the claims into the token.

I looked into this in more details and there are a few posts on similar topics around here, here and here.

I am going to close this issue for now as I cannot replicate it but if you are able to push the source code that has this issue somewhere I would be happy to take another look.

Hi @TomPallister! Sorry for not having replied sooner. I'll upload my code here on github when I get a chance. We're still early in development so multi role users have yet to come up again. Thanks again for taking the time to look into this and making our development easier with Ocelot! We really appreciate it.

@ctee-cormant ok cool! When you do push the code let me know! Good luck with everything!

I've had the same problem (Ocelot v7.0.6). I've managed to solve it by setting in ocelot config:
"RouteClaimsRequirement": { "role": "superuser" }
Name of claim should be LOWERCASE as in my JWT token. Now it works how it should :)

FWIW, I found that I was getting this error and thought that something was going wrong with my claims. However after implementing my own version of the ClaismAuthoriser so I could debug it - I found that the error message code was what was giving me that List.ToString() rubbish.

My error was that the list didn't contain my claim I was requiring, and not that the claims were in the wrong format.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

LuketaD2 picture LuketaD2  路  3Comments

TomPallister picture TomPallister  路  4Comments

ruimaciel picture ruimaciel  路  3Comments

mogliang picture mogliang  路  3Comments

mjrousos picture mjrousos  路  6Comments