Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c85b7c1
ScopesAuthorizer refactoring
mehyaa May 31, 2021
5090cbc
Useless block removed
mehyaa May 31, 2021
63f8973
Update ScopesAuthorizer.cs
mehyaa Aug 11, 2023
3c9fc3d
Fix ScopesAuthorizer
mehyaa Aug 11, 2023
a05514c
Fix ScopesAuthorizer
mehyaa Aug 11, 2023
d8510a5
Refactoring
mehyaa Aug 14, 2023
659cb1e
Fix
mehyaa Aug 14, 2023
708c321
Fix errors and messages
raman-m Aug 24, 2023
f1f613f
Tests fixed
mehyaa Sep 14, 2023
ecb22c7
Test names corrected
mehyaa Sep 14, 2023
528db09
Tiny change
mehyaa Sep 15, 2023
d9fd3bd
Revert variable names to their previous identifiers to compare the ch…
raman-m Nov 5, 2024
d9e8e74
Merge branch 'develop' into patch-1
raman-m Nov 6, 2024
cf3c146
Merge branch 'develop' into patch-1
mehyaa Nov 19, 2025
78911ec
Refactored scope authorization logic to match the intended feature
mehyaa Nov 19, 2025
c850b70
Add unit tests for space-separated scope handling in ScopesAuthorizer
mehyaa Nov 20, 2025
f2068bc
Merge branch 'develop' into patch-1
mehyaa Dec 5, 2025
ee763a4
Add acceptance tests for space-separated scope handling in Authorizat…
mehyaa Dec 5, 2025
4870da1
Refactor ScopesAuthorizer
mehyaa Dec 6, 2025
7a189a3
Fix AuthorizationTests acceptance tests for space-separated scopes ha…
mehyaa Dec 6, 2025
813aae8
Added missing changes to AuthorizationTests
mehyaa Dec 6, 2025
b5e6846
Update ScopesAuthorizer.cs
mehyaa Dec 6, 2025
62cc7f9
Refactor AuthorizationTests to simplify space-separated scope tests a…
mehyaa Dec 6, 2025
9ef215c
Update src/Ocelot/Authorization/ScopesAuthorizer.cs
raman-m Dec 6, 2025
7adba7b
adding reference to rfc 8693, the scope claim is a json string
ggnaegi Dec 6, 2025
876326b
Improve code coverage
raman-m Dec 7, 2025
3bca045
Code review by @raman-m
raman-m Dec 7, 2025
54e2d9a
Update docs
raman-m Dec 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/Ocelot/Authorization/ScopesAuthorizer.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using System.Security.Claims;

namespace Ocelot.Authorization;

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

private readonly IClaimsParser _claimsParser;

public ScopesAuthorizer(IClaimsParser claimsParser)
{
_claimsParser = claimsParser;
Expand All @@ -28,7 +29,17 @@ public Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> ro
return new ErrorResponse<bool>(values.Errors);
}

var userScopes = values.Data;
IList<string> userScopes = values.Data;

if (userScopes.Count == 1)
{
var scope = userScopes[0];

if (scope.Contains(' '))
{
userScopes = scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
}
}

var matchesScopes = routeAllowedScopes.Intersect(userScopes);

Expand Down
109 changes: 109 additions & 0 deletions test/Ocelot.AcceptanceTests/AuthorizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,115 @@ public async Task Should_return_200_OK_with_global_allowed_scopes()
await ThenTheResponseBodyAsync();
}

[Fact]
[Trait("Feature", "Space-separated scopes")]
public async Task Should_return_200_OK_with_space_separated_scope_single_match()
{
var port = PortFinder.GetRandomPort();
var route = GivenAuthRoute(port);
route.AuthenticationOptions.AllowedScopes = ["api", "api.read", "api.write"];
var configuration = GivenConfiguration(route);
await GivenThereIsAnIdentityServer();
GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from space-separated test");

GivenThereIsAConfiguration(configuration);
GivenOcelotIsRunning(WithAspNetIdentityAuthentication);

// Generate token with space-separated scopes
await GivenIHaveAToken(scope: "api.read api.write openid");
GivenIHaveAddedATokenToMyRequest();
await WhenIGetUrlOnTheApiGateway("/");
ThenTheStatusCodeShouldBe(HttpStatusCode.OK);
await ThenTheResponseBodyShouldBeAsync("Hello from space-separated test");
}

[Fact]
[Trait("Feature", "Space-separated scopes")]
public async Task Should_return_200_OK_with_space_separated_scope_multiple_matches()
{
var port = PortFinder.GetRandomPort();
var route = GivenAuthRoute(port);
route.AuthenticationOptions.AllowedScopes = ["api.read", "api.write"];
var configuration = GivenConfiguration(route);
await GivenThereIsAnIdentityServer();
GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Multiple scopes matched");

GivenThereIsAConfiguration(configuration);
GivenOcelotIsRunning(WithAspNetIdentityAuthentication);

// Generate token with space-separated scopes that includes both allowed scopes
await GivenIHaveAToken(scope: "api.read api.write openid offline_access");
GivenIHaveAddedATokenToMyRequest();
await WhenIGetUrlOnTheApiGateway("/");
ThenTheStatusCodeShouldBe(HttpStatusCode.OK);
await ThenTheResponseBodyShouldBeAsync("Multiple scopes matched");
}

[Fact]
[Trait("Feature", "Space-separated scopes")]
public async Task Should_return_403_with_space_separated_scope_no_match()
{
var port = PortFinder.GetRandomPort();
var route = GivenAuthRoute(port);
route.AuthenticationOptions.AllowedScopes = ["admin", "superuser"];
var configuration = GivenConfiguration(route);
await GivenThereIsAnIdentityServer();
GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Should not reach here");

GivenThereIsAConfiguration(configuration);
GivenOcelotIsRunning(WithAspNetIdentityAuthentication);

// Generate token with space-separated scopes that don't match allowed scopes
await GivenIHaveAToken(scope: "api.read api.write openid");
GivenIHaveAddedATokenToMyRequest();
await WhenIGetUrlOnTheApiGateway("/");
ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden);
}

[Fact]
[Trait("Feature", "Space-separated scopes")]
public async Task Should_return_200_OK_with_space_separated_scope_with_extra_spaces()
{
var port = PortFinder.GetRandomPort();
var route = GivenAuthRoute(port);
route.AuthenticationOptions.AllowedScopes = ["api", "api.read"];
var configuration = GivenConfiguration(route);
await GivenThereIsAnIdentityServer();
GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Extra spaces handled");

GivenThereIsAConfiguration(configuration);
GivenOcelotIsRunning(WithAspNetIdentityAuthentication);

// Generate token with space-separated scopes that have extra spaces
await GivenIHaveAToken(scope: " api.read api.write ");
GivenIHaveAddedATokenToMyRequest();
await WhenIGetUrlOnTheApiGateway("/");
ThenTheStatusCodeShouldBe(HttpStatusCode.OK);
await ThenTheResponseBodyShouldBeAsync("Extra spaces handled");
}

[Fact]
[Trait("Feature", "Space-separated scopes")]
public async Task Should_return_200_OK_with_single_scope_without_spaces()
{
var port = PortFinder.GetRandomPort();
var route = GivenAuthRoute(port);
route.AuthenticationOptions.AllowedScopes = ["api", "api.read"];
var configuration = GivenConfiguration(route);
await GivenThereIsAnIdentityServer();
GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Single scope no spaces");

GivenThereIsAConfiguration(configuration);
GivenOcelotIsRunning(WithAspNetIdentityAuthentication);

// Generate token with single scope (no spaces) - should not be affected
await GivenIHaveAToken(scope: "api.read");
GivenIHaveAddedATokenToMyRequest();
await WhenIGetUrlOnTheApiGateway("/");
ThenTheStatusCodeShouldBe(HttpStatusCode.OK);
await ThenTheResponseBodyShouldBeAsync("Single scope no spaces");
}

private static void Void() { }

//private async Task GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType)
Expand Down
99 changes: 98 additions & 1 deletion test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,104 @@ public void Should_not_match_scopes_and_return_error_result()
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new ErrorResponse<bool>(fakeError));
ThenTheFollowingIsReturned(result, new ErrorResponse<bool>(fakeError));
}

[Fact]
public void Should_split_space_separated_scope_and_match()
{
// Arrange
var principal = new ClaimsPrincipal();
var allowedScopes = new List<string> { "api.read", "api.write" };
var userScopes = new List<string> { "api.read api.write openid" }; // Space-separated scope claim
GivenTheParserReturns(new OkResponse<List<string>>(userScopes));

// Act
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));
}

[Fact]
public void Should_split_space_separated_scope_and_match_single_scope()
{
// Arrange
var principal = new ClaimsPrincipal();
var allowedScopes = new List<string> { "api.write" };
var userScopes = new List<string> { "api.read api.write openid" }; // Space-separated scope claim
GivenTheParserReturns(new OkResponse<List<string>>(userScopes));

// Act
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));
}

[Fact]
public void Should_split_space_separated_scope_but_not_match()
{
// Arrange
var fakeError = new FakeError();
var principal = new ClaimsPrincipal();
var allowedScopes = new List<string> { "admin" };
var userScopes = new List<string> { "api.read api.write openid" }; // Space-separated scope claim
GivenTheParserReturns(new OkResponse<List<string>>(userScopes));

// Act
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new ErrorResponse<bool>(fakeError));
}

[Fact]
public void Should_handle_multiple_scope_claims_without_splitting()
{
// Arrange
var principal = new ClaimsPrincipal();
var allowedScopes = new List<string> { "api.read" };
var userScopes = new List<string> { "api.read", "api.write" }; // Multiple separate claims
GivenTheParserReturns(new OkResponse<List<string>>(userScopes));

// Act
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));
}

[Fact]
public void Should_not_split_single_scope_without_spaces()
{
// Arrange
var principal = new ClaimsPrincipal();
var allowedScopes = new List<string> { "api.read" };
var userScopes = new List<string> { "api.read" }; // Single scope without spaces
GivenTheParserReturns(new OkResponse<List<string>>(userScopes));

// Act
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));
}

[Fact]
public void Should_handle_empty_string_after_splitting()
{
// Arrange
var principal = new ClaimsPrincipal();
var allowedScopes = new List<string> { "api.read" };
var userScopes = new List<string> { " api.read api.write " }; // Scope with extra spaces
GivenTheParserReturns(new OkResponse<List<string>>(userScopes));

// Act
var result = _authorizer.Authorize(principal, allowedScopes);

// Assert
ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));
}

private void GivenTheParserReturns(Response<List<string>> response)
Expand Down
Loading