Reputation: 18127
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": "[email protected]",
"role": "Administrator",
"nbf": 1595460189,
"exp": 1596064989,
"iat": 1595460189
}
Upvotes: 4
Views: 3400
Reputation: 18127
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"
}
Upvotes: 7