Ocelot icon indicating copy to clipboard operation
Ocelot copied to clipboard

Multiple values for single key in RouteClaimsRequirement

Open Stians92 opened this issue 6 years ago • 31 comments

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

Stians92 avatar Jan 14 '19 16:01 Stians92

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

philproctor avatar Jan 16 '19 14:01 philproctor

Is this fixed?

royayan1988 avatar Jun 01 '19 14:06 royayan1988

I'm interested too. Does another workaround exist?

kuzdu avatar Jun 08 '19 15:06 kuzdu

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

feugen24 avatar Jul 24 '19 08:07 feugen24

I've tried the override authorisation middleware method , but the claims are strictly converted in a Dictionary<string,string> 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:

  public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            var configuration = new OcelotPipelineConfiguration
            {
                AuthorizationMiddleware = 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

 private bool Authorize(HttpContext ctx)
        {
           DownstreamRoute route = (DownstreamRoute)ctx.Items["DownstreamRoute"];
            string key = route.AuthenticationOptions.AuthenticationProviderKey;

            if (key == null || key == "") return true;
            if (route.RouteClaimsRequirement.Count == 0) return true;
            else
            {
            else {
                //flag for authorization
                bool auth = false;

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

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

remeber to add in the ConfigureService method

 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<string , string> is very ugly and not very flexible)

//updated for last version of ocelot

arro000 avatar Sep 06 '19 15:09 arro000

((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)
                        {
                            return true;
                            // break;
                        }
                    }
                }

Example of Configuration

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

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

arro000 avatar Sep 06 '19 15:09 arro000

May I please know any update on this?

hrishikeshtt avatar May 04 '20 10:05 hrishikeshtt

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

MuhammadSohaibSultan avatar Jul 01 '20 05:07 MuhammadSohaibSultan

tokenValidationParameters

What is tokenValidationParameters?

pateljeel avatar Sep 25 '20 14:09 pateljeel

Is the instance of object with the bearer authentication rules, in my case for example:

 var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
            
                IssuerSigningKey = signingKey
            };

arro000 avatar Sep 25 '20 15:09 arro000

((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?

pateljeel avatar Sep 28 '20 15:09 pateljeel

Hi, is there any push new commit for this issue?

eeefbal avatar Jan 19 '21 07:01 eeefbal

@arro000 Should used PreAuthorizationMiddleware and AuthorizationMiddleware in OcelotPipelineConfiguration to check RequirementClaims?

ntruongvux avatar Apr 28 '21 06:04 ntruongvux

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

Is it clear when this enhancement will be added.

I am asking this question, because it has been over two years since this enhancement was requested.

default-kaas avatar May 20 '21 12:05 default-kaas

I've tried the override authorisation middleware method , but the claims are strictly converted in a Dictionary<string,string> 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:

  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

 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

 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<string , string> is very ugly and not very flexible)

From where do you get the AuthorisationMiddleware, DownstreamContext and UnauthorisedError.

default-kaas avatar May 26 '21 07:05 default-kaas

I made this script for ocelot version 13, I updated my comment making it compatible with the latest version 17 AuthorisationMiddleware replaced by AuthorizationMiddleware DownstreamContext = replaced by DownstreamRoute route = (DownstreamRoute)ctx.Items["DownstreamRoute"]; you can get route information from HttpContext UnauthorizedError from Ocelot.Authorization but i commented that line because HttpContext dosn't have Error resposne list like DownstreamContext

arro000 avatar Sep 27 '21 08:09 arro000

No? Still no update on the topic ? (if we can avoid the workaround, would be better)

Simkiw avatar Jan 21 '22 09:01 Simkiw

Still no update on the topic ?

SistemasInfinitos avatar Feb 15 '22 23:02 SistemasInfinitos

Any progess on this issue? Ocelot works great for authorization regarding client apps (via audience, scopes etc.) but for anything user related via claims (roles etc) we seem to be lacking proper config tools. A fleshed out version of 'RouteClaimsRequirement' would be greatly appreciated.

olssonp avatar Jul 05 '22 15:07 olssonp

Hello, I created a PR https://github.com/ThreeMammals/Ocelot/pull/1596

To implement the possibility of using a list of claims, you must implement the IRouteClaimsRequirementComparer interface and register it in the IServiceCollection before AddOcelot

For example:

public class AnyRouteClaimRequirement : IRouteClaimsRequirementComparer
{
    public bool Compare(List<string> userClaims, string claim)
    {
        return claim.Split(',').Any(splitRequired => userClaims.Contains(splitRequired));
    }
}

And

builder.Services.AddSingleton<IRouteClaimRequirement, AnyRouteClaimRequirement>();
builder.Services.AddOcelot();

bogd4nov1van avatar Aug 31 '22 17:08 bogd4nov1van

Hello from me. Is this problem fixed? I came across this topic https://github.com/ThreeMammals/Ocelot/issues/746 which says that mr. Pallister fixed the problem, but I cannot understand if so what is the proper syntax to register multiple roles. Can anyone help with this one?

andreuwz avatar Nov 01 '22 13:11 andreuwz

@Stians92 Mr. Stian,

Do you have any solutions to fix this your opened issue over 4 years ago? Pay attention that the issue reporter can prioritize solutions and resolve any discussions.

raman-m avatar May 10 '23 17:05 raman-m

Hi,

I was able to make a few changes on this and get this to work for static claims by updating the ClaimsAuthorizer.cs.

The changes are basically updating all the Dictionary<string, string> routeClaimsRequirement to Dictionary<string, string[]> routeClaimsRequirement and then updating the ClaimsAuthorizer.cs's Authorize() to

            public Response<bool> Authorize(
            ClaimsPrincipal claimsPrincipal,
            Dictionary<string, string[]> routeClaimsRequirement,
            List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues
        )
        {
            foreach (var required in routeClaimsRequirement)
            {
                var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key);

                if (values.IsError)
                {
                    return new ErrorResponse<bool>(values.Errors);
                }

                if (values.Data != null)
                {
                    // dynamic claim
                    var match = Regex.Match(required.Value, @"^{(?<variable>.+)}$"); <-- how this line is supposed to be changed?
                    ...........
                    ...........
                 }
                    else
                    {
                        // static claim
                        var authorized = required.Value.Any(x=> values.Data.Contains(x));
                        if (!authorized)
                        {
                            return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
                                       $"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}"));
                        }
                    }
                }
                else
                {
                    return new ErrorResponse<bool>(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}"));
                }
            }

            return new OkResponse<bool>(true);
        }

However, I am confused what should be the approach on the Dynamic claims? This doesn't seem to be applicable to the dynamic claim section, in my opinion.

I did change it to


var match = Regex.Match(required.Value!.FirstOrDefault() ?? string.Empty, "^{(?<variable>.+)}$");

which basically takes the first configured dynamic placeholder and moves on but am not sure if this is a good approach. Let me know your thoughts.

asrasya-kosha avatar Mar 30 '24 23:03 asrasya-kosha

@asrasya-kosha Will you open a PR?

raman-m avatar Apr 01 '24 10:04 raman-m

@raman-m yes, that is the intention. Thanks.

asrasya-kosha avatar Apr 01 '24 17:04 asrasya-kosha

@asrasya-kosha commented on Apr 1

You are assigned! Good luck and lots of inspiration!

raman-m avatar Apr 02 '24 12:04 raman-m