Ocelot: Multiple values for single key in RouteClaimsRequirement

Created on 14 Jan 2019  路  11Comments  路  Source: ThreeMammals/Ocelot

New Feature

Allow claims to have an array value and not just a string value.

Motivation for New Feature

I have an application where I have multiple roles for my users. For endpoints that should only be reached by admins, I can use the following:

"RouteClaimsRequirement": {
    "Role": "Admin"
}

For endpoints that should only be reached by users, I can use the following:

"RouteClaimsRequirement": {
    "Role": "User"
}

For endpoints that should be reached by both users and admins I would like to use the following:

"RouteClaimsRequirement": {
    "Role": ["User", "Admin"]
}

When I tried adding this, all requests to the endpoint respond with a 404.

This should let the request go through if the request has a claim for the role user or admin. This is equivalent to the built-in asp .net core attribute:
[Authorize(Roles = "User, Admin")]

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles?view=aspnetcore-2.2

enhancement proposal

Most helpful comment

I've tried the override authorisation middleware method , but the claims are strictly converted in a Dictionary format before the middleware was called ;

This format break the Route because the value cannot be converted into a string;

"RouteClaimsRequirement": {
    "Role": ["User", "Admin"]
}

This format also don't work because the Role key in dictionary will be ovverride with the second value;

"RouteClaimsRequirement": {
    "Role": "Admin"
        "Role": "User" 
}

so.. My personal solution will be
Workaround example

"RouteClaimsRequirement": {
"Role": "Admin , User"
}

In the authorisation middleware method , i will parse the string with a regex pattern to obtain the single role value:
```c#
public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var configuration = new OcelotPipelineConfiguration
{
AuthorisationMiddleware = async (ctx, next) =>
{
if (this.Authorize(ctx))
{
await next.Invoke();

                }
                else {
                    ctx.Errors.Add(new UnauthorisedError($"Fail to authorize"));
                }

            }
        };
        .
        .
        .
        await app.UseOcelot(configuration);
  }
The logic of Authorize Method
```c#
 private bool Authorize(DownstreamContext ctx)
        {
            if (ctx.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey == null) return true;
            else {
                //flag for authorization
                bool auth = false;

                //where are stored the claims of the jwt token
                Claim[] claims = ctx.HttpContext.User.Claims.ToArray<Claim>();

                //where are stored the required claims for the route
                Dictionary<string, string> required = ctx.DownstreamReRoute.RouteClaimsRequirement;
                .
                .
                ((AUTHORIZATION LOGIC))
                .
                .
                return auth;
           }

remeber to add in the ConfigureService method
c# services.AddAuthorization(); services.AddAuthentication() .AddJwtBearer("TestKey", x => { // x.RequireHttpsMetadata = false; x.TokenValidationParameters = tokenValidationParameters; });

(I Still working on my Authorization logic that will implement the multiple claims with And/Or logic with regex of strings , but the claims data structure implemented with Dictionary is very ugly and not very flexible)

All 11 comments

I believe adding more sophisticated route claims requirements in general would help with this, will discuss with the team best approaches.

Is this fixed?

I'm interested too. Does another workaround exist?

as workaround override authorisation middleware for claims or policy based with claims in policies as suggested here

I've tried the override authorisation middleware method , but the claims are strictly converted in a Dictionary format before the middleware was called ;

This format break the Route because the value cannot be converted into a string;

"RouteClaimsRequirement": {
    "Role": ["User", "Admin"]
}

This format also don't work because the Role key in dictionary will be ovverride with the second value;

"RouteClaimsRequirement": {
    "Role": "Admin"
        "Role": "User" 
}

so.. My personal solution will be
Workaround example

"RouteClaimsRequirement": {
"Role": "Admin , User"
}

In the authorisation middleware method , i will parse the string with a regex pattern to obtain the single role value:
```c#
public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var configuration = new OcelotPipelineConfiguration
{
AuthorisationMiddleware = async (ctx, next) =>
{
if (this.Authorize(ctx))
{
await next.Invoke();

                }
                else {
                    ctx.Errors.Add(new UnauthorisedError($"Fail to authorize"));
                }

            }
        };
        .
        .
        .
        await app.UseOcelot(configuration);
  }
The logic of Authorize Method
```c#
 private bool Authorize(DownstreamContext ctx)
        {
            if (ctx.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey == null) return true;
            else {
                //flag for authorization
                bool auth = false;

                //where are stored the claims of the jwt token
                Claim[] claims = ctx.HttpContext.User.Claims.ToArray<Claim>();

                //where are stored the required claims for the route
                Dictionary<string, string> required = ctx.DownstreamReRoute.RouteClaimsRequirement;
                .
                .
                ((AUTHORIZATION LOGIC))
                .
                .
                return auth;
           }

remeber to add in the ConfigureService method
c# services.AddAuthorization(); services.AddAuthentication() .AddJwtBearer("TestKey", x => { // x.RequireHttpsMetadata = false; x.TokenValidationParameters = tokenValidationParameters; });

(I Still working on my Authorization logic that will implement the multiple claims with And/Or logic with regex of strings , but the claims data structure implemented with Dictionary is very ugly and not very flexible)

((AUTHORIZATION LOGIC)) Example
```c#
Regex reor = new Regex(@"[^,\s+$ ][^\,]*[^,\s+$ ]");
MatchCollection matches;

            Regex reand = new Regex(@"[^&\s+$ ][^\&]*[^&\s+$ ]");
            MatchCollection matchesand;
            int cont=0;
            foreach (KeyValuePair<string,string> claim in required)
            {
                matches = reor.Matches(claim.Value);
                foreach (Match match in matches)
                {
                    matchesand = reand.Matches(match.Value); 
            cont = 0;
                    foreach (Match m in matchesand)
                    {
                        foreach (Claim cl in claims) 
                        {
                            if (cl.Type == claim.Key)
                            {
                                if (cl.Value == m.Value)
                                {
                                      cont++;
                                }
                            }
                        }
                    }
                    if (cont == matchesand.Count) 
                    {
            auth= true;
            break;
        }
                }
            }

```

Example of Configuration

"RouteClaimsRequirement": {
    "Role": "User & IT , Admin"
}

Logic result :
IF((User and IT) or Admin)

May I please know any update on this?

by using @arro000 's trick I changed a few things to make it work for me on .Net Core 3.1 & Ocelot 16.0.1.
My answer on Stackoverflow.
https://stackoverflow.com/questions/60300349/how-to-check-claim-value-in-array-or-any-in-ocelot-gateway/62390542#62390542

tokenValidationParameters

What is tokenValidationParameters?

Is the instance of object with the bearer authentication rules, in my case for example:
```c#
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,

            IssuerSigningKey = signingKey
        };

((AUTHORIZATION LOGIC)) Example

                Regex reor = new Regex(@"[^,\s+$ ][^\,]*[^,\s+$ ]");
                MatchCollection matches;

                Regex reand = new Regex(@"[^&\s+$ ][^\&]*[^&\s+$ ]");
                MatchCollection matchesand;
                int cont=0;
                foreach (KeyValuePair<string,string> claim in required)
                {
                    matches = reor.Matches(claim.Value);
                    foreach (Match match in matches)
                    {
                        matchesand = reand.Matches(match.Value); 
              cont = 0;
                        foreach (Match m in matchesand)
                        {
                            foreach (Claim cl in claims) 
                            {
                                if (cl.Type == claim.Key)
                                {
                                    if (cl.Value == m.Value)
                                    {
                                        cont++;
                                    }
                                }
                            }
                        }
                        if (cont == matchesand.Count) 
                        {
              auth= true;
              break;
          }
                    }
                }

Example of Configuration

"RouteClaimsRequirement": {
"Role": "User & IT , Admin"
}

Logic result :
IF((User and IT) or Admin)

With this change do you still need to add [Authorize(Roles = "User, Admin")] to your controllers?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

FerAguilarR93 picture FerAguilarR93  路  6Comments

lucianoscastro picture lucianoscastro  路  3Comments

bremnes picture bremnes  路  3Comments

FerAguilarR93 picture FerAguilarR93  路  3Comments

piyey picture piyey  路  6Comments