Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenIddict implementation, role section, override scope and role key #1431

Open
wants to merge 10 commits into
base: release/24.0
Choose a base branch
from
9 changes: 9 additions & 0 deletions src/Ocelot/Authorization/IRolesAuthorizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Ocelot.Responses;
using System.Security.Claims;

namespace Ocelot.Authorization;

public interface IRolesAuthorizer
{
Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeRequiredRole, string roleKey);
}
10 changes: 5 additions & 5 deletions src/Ocelot/Authorization/IScopesAuthorizer.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Ocelot.Responses;
using System.Security.Claims;

namespace Ocelot.Authorization
namespace Ocelot.Authorization;

public interface IScopesAuthorizer
{
public interface IScopesAuthorizer
{
Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes);
}
Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes,
string scopeKey);
}
37 changes: 35 additions & 2 deletions src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,33 @@ public class AuthorizationMiddleware : OcelotMiddleware
private readonly RequestDelegate _next;
private readonly IClaimsAuthorizer _claimsAuthorizer;
private readonly IScopesAuthorizer _scopesAuthorizer;
private readonly IRolesAuthorizer _rolesAuthorizer;

public AuthorizationMiddleware(RequestDelegate next,
IClaimsAuthorizer claimsAuthorizer,
IScopesAuthorizer scopesAuthorizer,
IRolesAuthorizer rolesAuthorizer,
IOcelotLoggerFactory loggerFactory)
: base(loggerFactory.CreateLogger<AuthorizationMiddleware>())
{
_next = next;
_claimsAuthorizer = claimsAuthorizer;
_scopesAuthorizer = scopesAuthorizer;
_rolesAuthorizer = rolesAuthorizer;
}

// Note roles is a duplicate of scopes - should refactor based on type
// Note scopes and roles are processed as OR
// TODO: Create logic to process policies that we use in the API
public async Task Invoke(HttpContext httpContext)
{
var downstreamRoute = httpContext.Items.DownstreamRoute();

var options = downstreamRoute.AuthenticationOptions;
if (!IsOptionsHttpMethod(httpContext) && IsAuthenticatedRoute(downstreamRoute))
{
Logger.LogInformation("route is authenticated scopes must be checked");

var authorized = _scopesAuthorizer.Authorize(httpContext.User, downstreamRoute.AuthenticationOptions.AllowedScopes);
var authorized = _scopesAuthorizer.Authorize(httpContext.User, options.AllowedScopes, options.ScopeKey);

if (authorized.IsError)
{
Expand All @@ -54,6 +60,33 @@ public async Task Invoke(HttpContext httpContext)
}
}

if (!IsOptionsHttpMethod(httpContext) && IsAuthenticatedRoute(downstreamRoute))
{
Comment on lines +63 to +64
Copy link
Member

@raman-m raman-m Apr 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you copy whole if-block above?

I strongly disapprove of any form of copying of blocks❗

Logger.LogInformation("route and scope is authenticated role must be checked");

var authorizedRole = _rolesAuthorizer.Authorize(httpContext.User, options.RequiredRole, options.RoleKey);

if (authorizedRole.IsError)
{
Logger.LogWarning("error authorizing user roles");

httpContext.Items.UpsertErrors(authorizedRole.Errors);
return;
}

if (IsAuthorized(authorizedRole))
{
Logger.LogInformation("user has the required role and is authorized calling next authorization checks");
}
else
{
Logger.LogWarning("user does not have the required role and is not authorized setting pipeline error");

httpContext.Items.SetError(new UnauthorizedError(
$"{httpContext.User.Identity.Name} unable to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}"));
}
}

if (!IsOptionsHttpMethod(httpContext) && IsAuthorizedRoute(downstreamRoute))
{
Logger.LogInformation("route is authorized");
Expand Down
44 changes: 44 additions & 0 deletions src/Ocelot/Authorization/RolesAuthorizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using System.Security.Claims;

namespace Ocelot.Authorization;

public class RolesAuthorizer : IRolesAuthorizer
{
private readonly IClaimsParser _claimsParser;

public RolesAuthorizer(IClaimsParser claimsParser)
{
_claimsParser = claimsParser;
}

public Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeRequiredRole, string roleKey)
{
if (routeRequiredRole == null || routeRequiredRole.Count == 0)
{
return new OkResponse<bool>(true);
}

roleKey ??= "role";

var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, roleKey);

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

var userRoles = values.Data;

var matchedRoles = routeRequiredRole.Intersect(userRoles).ToList(); // Note this is an OR

if (matchedRoles.Count == 0)
{
return new ErrorResponse<bool>(
new ScopeNotAuthorizedError($"no one user role: '{string.Join(",", userRoles)}' match with some allowed role: '{string.Join(",", routeRequiredRole)}'"));
}

return new OkResponse<bool>(true);
}
}
8 changes: 4 additions & 4 deletions src/Ocelot/Authorization/ScopesAuthorizer.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using System.Security.Claims;

namespace Ocelot.Authorization
{
public class ScopesAuthorizer : IScopesAuthorizer
{
private readonly IClaimsParser _claimsParser;
private const string Scope = "scope";

public ScopesAuthorizer(IClaimsParser claimsParser)
{
_claimsParser = claimsParser;
}

public Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes)
public Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes, string scopeKey)
{
if (routeAllowedScopes == null || routeAllowedScopes.Count == 0)
{
return new OkResponse<bool>(true);
}

var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, Scope);
scopeKey ??= "scope";
var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, scopeKey);

if (values.IsError)
{
Expand Down
61 changes: 48 additions & 13 deletions src/Ocelot/Configuration/AuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,55 @@ namespace Ocelot.Configuration
{
public sealed class AuthenticationOptions
{
public AuthenticationOptions(List<string> allowedScopes, string authenticationProviderKey)
{
AllowedScopes = allowedScopes;
AuthenticationProviderKey = authenticationProviderKey;
AuthenticationProviderKeys = Array.Empty<string>();
}

public AuthenticationOptions(FileAuthenticationOptions from)
{
AllowedScopes = from.AllowedScopes ?? new();
AuthenticationProviderKey = from.AuthenticationProviderKey ?? string.Empty;
AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? Array.Empty<string>();
BuildAuthenticationProviderKeys(from.AuthenticationProviderKey, from.AuthenticationProviderKeys);
PolicyName = from.PolicyName;
RequiredRole = from.RequiredRole;
ScopeKey = from.ScopeKey;
RoleKey = from.RoleKey;
}

public AuthenticationOptions(List<string> allowedScopes, string authenticationProviderKey, string[] authenticationProviderKeys)
{
AllowedScopes = allowedScopes ?? new();
AuthenticationProviderKey = authenticationProviderKey ?? string.Empty;
AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty<string>();
BuildAuthenticationProviderKeys(authenticationProviderKey, authenticationProviderKeys);
}

public AuthenticationOptions(List<string> allowedScopes, string[] authenticationProviderKeys, List<string> requiredRole, string scopeKey, string roleKey, string policyName)
{
AllowedScopes = allowedScopes;
BuildAuthenticationProviderKeys(null, authenticationProviderKeys);
PolicyName = policyName;
RequiredRole = requiredRole;
ScopeKey = scopeKey;
RoleKey = roleKey;
}

/// <summary>
/// Builds auth keys migrating legacy key to new ones.
/// </summary>
/// <param name="legacyKey">The legacy <see cref="AuthenticationProviderKey"/>.</param>
/// <param name="keys">New <see cref="AuthenticationProviderKeys"/> to build.</param>
private void BuildAuthenticationProviderKeys(string legacyKey, string[] keys)
{
keys ??= Array.Empty<string>();
if (string.IsNullOrEmpty(legacyKey))
{
AuthenticationProviderKeys = keys;
AuthenticationProviderKey = string.Empty;
return;
}

// Add legacy Key to new Keys array as the first element
var arr = new string[keys.Length + 1];
arr[0] = legacyKey;
Array.Copy(keys, 0, arr, 1, keys.Length);

// Update the object
AuthenticationProviderKeys = arr;
AuthenticationProviderKey = string.Empty;
}

public List<string> AllowedScopes { get; }
Expand All @@ -34,7 +64,7 @@ public AuthenticationOptions(List<string> allowedScopes, string authenticationPr
/// A <see langword="string"/> value of the scheme name.
/// </value>
[Obsolete("Use the " + nameof(AuthenticationProviderKeys) + " property!")]
public string AuthenticationProviderKey { get; }
public string AuthenticationProviderKey { get; private set; }

/// <summary>
/// Multiple authentication schemes registered in DI services with appropriate authentication providers.
Expand All @@ -45,6 +75,11 @@ public AuthenticationOptions(List<string> allowedScopes, string authenticationPr
/// <value>
/// An array of <see langword="string"/> values of the scheme names.
/// </value>
public string[] AuthenticationProviderKeys { get; }
public string[] AuthenticationProviderKeys { get; private set; }

public List<string> RequiredRole { get; }
public string ScopeKey { get; }
public string RoleKey { get; }
public string PolicyName { get; }
}
}
36 changes: 30 additions & 6 deletions src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,55 @@ namespace Ocelot.Configuration.Builder
public class AuthenticationOptionsBuilder
{
private List<string> _allowedScopes = new();
private string _authenticationProviderKey;
private List<string> _requiredRole = new();
private string[] _authenticationProviderKeys = Array.Empty<string>();
private string _roleKey;
private string _scopeKey;
private string _policyName;

public AuthenticationOptionsBuilder WithAllowedScopes(List<string> allowedScopes)
{
_allowedScopes = allowedScopes;
return this;
}


public AuthenticationOptionsBuilder WithRequiredRole(List<string> requiredRole)
{
_requiredRole = requiredRole;
return this;
}

[Obsolete("Use the " + nameof(WithAuthenticationProviderKeys) + " property!")]
public AuthenticationOptionsBuilder WithAuthenticationProviderKey(string authenticationProviderKey)
=> WithAuthenticationProviderKeys(authenticationProviderKey);

public AuthenticationOptionsBuilder WithAuthenticationProviderKeys(params string[] authenticationProviderKeys)
{
_authenticationProviderKey = authenticationProviderKey;
_authenticationProviderKeys = authenticationProviderKeys;
return this;
}

public AuthenticationOptionsBuilder WithAuthenticationProviderKeys(string[] authenticationProviderKeys)
public AuthenticationOptionsBuilder WithRoleKey(string roleKey)
{
_authenticationProviderKeys = authenticationProviderKeys;
_roleKey = roleKey;
return this;
}

public AuthenticationOptionsBuilder WithScopeKey(string scopeKey)
{
_scopeKey = scopeKey;
return this;
}

public AuthenticationOptionsBuilder WithPolicyName(string policyName)
{
_policyName = policyName;
return this;
}

public AuthenticationOptions Build()
{
return new AuthenticationOptions(_allowedScopes, _authenticationProviderKey, _authenticationProviderKeys);
return new AuthenticationOptions(_allowedScopes, _authenticationProviderKeys, _requiredRole, _scopeKey, _roleKey, _policyName);
}
}
}
10 changes: 4 additions & 6 deletions src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator
namespace Ocelot.Configuration.Creator;

public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator
{
public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator
{
public AuthenticationOptions Create(FileRoute route)
=> new(route?.AuthenticationOptions ?? new());
}
public AuthenticationOptions Create(FileRoute route) => new(route?.AuthenticationOptions ?? new());
}
13 changes: 11 additions & 2 deletions src/Ocelot/Configuration/File/FileAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public FileAuthenticationOptions()
{
AllowedScopes = new();
AuthenticationProviderKeys = Array.Empty<string>();
RequiredRole = new();
}

public FileAuthenticationOptions(FileAuthenticationOptions from)
Expand All @@ -19,13 +20,21 @@ public FileAuthenticationOptions(FileAuthenticationOptions from)

[Obsolete("Use the " + nameof(AuthenticationProviderKeys) + " property!")]
public string AuthenticationProviderKey { get; set; }

public string[] AuthenticationProviderKeys { get; set; }

public List<string> RequiredRole { get; set; }
public string ScopeKey { get; set; }
public string RoleKey { get; set; }
public string PolicyName { get; set; }

public override string ToString() => new StringBuilder()
.Append($"{nameof(AuthenticationProviderKey)}:'{AuthenticationProviderKey}',")
.Append($"{nameof(AuthenticationProviderKeys)}:[{string.Join(',', AuthenticationProviderKeys.Select(x => $"'{x}'"))}],")
.Append($"{nameof(AllowedScopes)}:[{string.Join(',', AllowedScopes.Select(x => $"'{x}'"))}]")
.Append($"{nameof(AllowedScopes)}:[{string.Join(',', AllowedScopes.Select(x => $"'{x}'"))}],")
.Append($"{nameof(RequiredRole)}:[").AppendJoin(',', RequiredRole).Append("],")
.Append($"{nameof(ScopeKey)}:[").AppendJoin(',', ScopeKey).Append("],")
.Append($"{nameof(RoleKey)}:[").AppendJoin(',', RoleKey).Append("],")
.Append($"{nameof(PolicyName)}:[").AppendJoin(',', PolicyName).Append(']')
.ToString();
}
}
1 change: 1 addition & 0 deletions src/Ocelot/DependencyInjection/OcelotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.TryAddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
Services.TryAddSingleton<IClaimsAuthorizer, ClaimsAuthorizer>();
Services.TryAddSingleton<IScopesAuthorizer, ScopesAuthorizer>();
Services.TryAddSingleton<IRolesAuthorizer, RolesAuthorizer>();
Services.TryAddSingleton<IAddClaimsToRequest, AddClaimsToRequest>();
Services.TryAddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
Services.TryAddSingleton<IAddQueriesToRequest, AddQueriesToRequest>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ public class AuthorizationMiddlewareTests : UnitTest
private readonly AuthorizationMiddleware _middleware;
private readonly RequestDelegate _next;
private readonly HttpContext _httpContext;
private readonly Mock<IRolesAuthorizer> _authRolesService;

public AuthorizationMiddlewareTests()
{
_httpContext = new DefaultHttpContext();
_authService = new Mock<IClaimsAuthorizer>();
_authScopesService = new Mock<IScopesAuthorizer>();
_authRolesService = new Mock<IRolesAuthorizer>();
_loggerFactory = new Mock<IOcelotLoggerFactory>();
_logger = new Mock<IOcelotLogger>();
_loggerFactory.Setup(x => x.CreateLogger<AuthorizationMiddleware>()).Returns(_logger.Object);
_next = context => Task.CompletedTask;
_middleware = new AuthorizationMiddleware(_next, _authService.Object, _authScopesService.Object, _loggerFactory.Object);
_middleware = new AuthorizationMiddleware(_next, _authService.Object, _authScopesService.Object, _authRolesService.Object, _loggerFactory.Object);
}

[Fact]
Expand Down