Skip to content

Commit

Permalink
fix after code review
Browse files Browse the repository at this point in the history
  • Loading branch information
jlukawska committed Jul 24, 2024
1 parent f03aa35 commit 919abc8
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 113 deletions.
65 changes: 65 additions & 0 deletions docs/features/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,71 @@ Finally, we would say that registering providers, initializing options, forwardi
If you're stuck or don't know what to do, just find inspiration in our `acceptance tests <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+MultipleAuthSchemesFeatureTests+language%3AC%23&type=code&l=C%23>`_
(currently for `Identity Server 4 <https://identityserver4.readthedocs.io/>`_ only) [#f3]_.

Global Authentication
-----------------------------------

If you want to configure ``AuthenticationOptions`` the same for all Routes, do it in GlobalConfiguration the same way as for Route. If there are ``AuthenticationOptions`` configured both for GlobalConfiguration and Route (``AuthenticationProviderKey`` or ``AuthenticationProviderKeys`` is set), the Route section has priority.

If you want to exclude route from global ``AuthenticationOptions``, you can do that by setting ``AllowAnonymous`` to true in the route ``AuthenticationOptions`` - then this route will not be authenticated.

In the following example:
* the first route will be authenticated with TestKeyGlobal provider key,
* the second one - with TestKey provider key,
* the others will not be authenticated.

.. code-block:: json
"Routes": [
{
"DownstreamPathTemplate": "/abc",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 54001
}],
"UpstreamPathTemplate": "/abc",
"UpstreamHttpMethod": [ "Get" ]
},
{
"DownstreamPathTemplate": "/def",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 54001
}],
"UpstreamPathTemplate": "/def",
"UpstreamHttpMethod": [ "Get" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "TestKey",
"AllowedScopes": []
}
},
{
"DownstreamPathTemplate": "/{action}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 54001
}],
"UpstreamPathTemplate": "/{action}",
"UpstreamHttpMethod": [ "Get" ],
"AuthenticationOptions": {
"AllowAnonymous": true
}
}
],
"GlobalConfiguration": {
"BaseUrl": "http://fake.test.com",
"AuthenticationOptions": {
"AuthenticationProviderKey": "TestKeyGlobal",
"AllowedScopes": []
}
}
**Note** If a route uses a global ``AuthenticationProviderKey`` (when ``AuthenticationProviderKey`` is not configured for route explicitly), it uses also global ``AllowedScopes``, even if ``AllowedScopes`` is configured for the route additionally.

JWT Tokens
----------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator
{
public AuthenticationOptions Create(FileAuthenticationOptions routeAuthOptions, FileAuthenticationOptions globalConfAuthOptions)
{
var routeAuthOptionsEmpty = routeAuthOptions?.HasProviderKey() != true;
var routeAuthOptionsEmpty = routeAuthOptions?.HasProviderKey != true;
var resultAuthOptions = routeAuthOptionsEmpty ? globalConfAuthOptions : routeAuthOptions;
return new(resultAuthOptions ?? new());
}
Expand Down
5 changes: 2 additions & 3 deletions src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;
using System.Runtime.CompilerServices;

namespace Ocelot.Configuration.Creator
{
Expand All @@ -13,8 +12,8 @@ public RouteOptions Create(FileRoute fileRoute, FileGlobalConfiguration globalCo
return new RouteOptionsBuilder().Build();
}

var isAuthenticated = !fileRoute.AuthenticationOptions?.AllowAnonymous == true && globalConfiguration?.AuthenticationOptions?.HasProviderKey() == true
|| fileRoute.AuthenticationOptions?.HasProviderKey() == true;
var isAuthenticated = fileRoute.AuthenticationOptions?.AllowAnonymous != true && globalConfiguration?.AuthenticationOptions?.HasProviderKey == true
|| fileRoute.AuthenticationOptions?.HasProviderKey == true;

var isAuthorized = fileRoute.RouteClaimsRequirement?.Any() == true;

Expand Down
2 changes: 1 addition & 1 deletion src/Ocelot/Configuration/File/FileAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public FileAuthenticationOptions(FileAuthenticationOptions from)

public string[] AuthenticationProviderKeys { get; set; }

public bool HasProviderKey() => !string.IsNullOrEmpty(AuthenticationProviderKey)
public bool HasProviderKey => !string.IsNullOrEmpty(AuthenticationProviderKey)
|| AuthenticationProviderKeys?.Any(k => !string.IsNullOrWhiteSpace(k)) == true;

public override string ToString() => new StringBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Validator
namespace Ocelot.Configuration.Validator;

public class FileAuthenticationOptionsValidator : AbstractValidator<FileAuthenticationOptions>
{
public class FileAuthenticationOptionsValidator : AbstractValidator<FileAuthenticationOptions>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

public FileAuthenticationOptionsValidator(IAuthenticationSchemeProvider authenticationSchemeProvider)
{
_authenticationSchemeProvider = authenticationSchemeProvider;
public FileAuthenticationOptionsValidator(IAuthenticationSchemeProvider authenticationSchemeProvider)
{
_authenticationSchemeProvider = authenticationSchemeProvider;

RuleFor(authOptions => authOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("AuthenticationOptions: {PropertyValue} is unsupported authentication provider");
}
RuleFor(authOptions => authOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("AuthenticationOptions: {PropertyValue} is unsupported authentication provider");
}

private async Task<bool> IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)
private async Task<bool> IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(options.AuthenticationProviderKey) && options.AuthenticationProviderKeys.Length == 0)
{
if (string.IsNullOrEmpty(options.AuthenticationProviderKey) && options.AuthenticationProviderKeys.Length == 0)
{
return true;
}

var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
var supportedSchemes = schemes.Select(scheme => scheme.Name);
var primary = options.AuthenticationProviderKey;
return !string.IsNullOrEmpty(primary) && supportedSchemes.Contains(primary)
|| (string.IsNullOrEmpty(primary) && options.AuthenticationProviderKeys.All(supportedSchemes.Contains));
return true;
}

var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
var supportedSchemes = schemes.Select(scheme => scheme.Name);
var primary = options.AuthenticationProviderKey;
return !string.IsNullOrEmpty(primary) && supportedSchemes.Contains(primary)
|| (string.IsNullOrEmpty(primary) && options.AuthenticationProviderKeys.All(supportedSchemes.Contains));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,114 +4,99 @@
using Ocelot.Configuration.File;
using Ocelot.Configuration.Validator;

namespace Ocelot.UnitTests.Configuration.Validation
namespace Ocelot.UnitTests.Configuration.Validation;

public class FileAuthenticationOptionsValidatorTests : UnitTest
{
public class FileAuthenticationOptionsValidatorTests
private readonly FileAuthenticationOptionsValidator _validator;
private readonly Mock<IAuthenticationSchemeProvider> _authProvider;
private FileAuthenticationOptions _authenticationOptions;
private ValidationResult _result;

public FileAuthenticationOptionsValidatorTests()
{
private readonly FileAuthenticationOptionsValidator _validator;
private readonly Mock<IAuthenticationSchemeProvider> _authProvider;
private FileAuthenticationOptions _authenticationOptions;
private ValidationResult _result;
_authProvider = new Mock<IAuthenticationSchemeProvider>();
_validator = new FileAuthenticationOptionsValidator(_authProvider.Object);
}

public FileAuthenticationOptionsValidatorTests()
{
_authProvider = new Mock<IAuthenticationSchemeProvider>();
_validator = new FileAuthenticationOptionsValidator(_authProvider.Object);
}
[Fact]
public async void Should_be_valid_if_specified_authentication_provider_is_registered()
{
// Arrange
const string key = "JwtLads";

[Fact]
public void Should_be_valid_if_specified_authentication_provider_is_registered()
{
const string key = "JwtLads";

var authenticationOptions = new FileAuthenticationOptions
{
AuthenticationProviderKey = key,
};

this.Given(_ => GivenThe(authenticationOptions))
.And(_ => GivenAnAuthProvider(key))
.When(_ => WhenIValidateAsync())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
CreateAuthenticationOptions(key);
GivenAnAuthProvider(key);

[Fact]
public void Should_not_be_valid_if_specified_authentication_provider_is_not_registered()
{
const string key = "JwtLads";

var authenticationOptions = new FileAuthenticationOptions
{
AuthenticationProviderKey = key,
};

this.Given(_ => GivenThe(authenticationOptions))
.When(_ => WhenIValidateAsync())
.Then(_ => ThenTheResultIsNotValid())
.And(_ => ThenTheErrorIs(key))
.BDDfy();
}
// Act
await ValidateAsync();

private void GivenAnAuthProvider(string key)
{
var schemes = new List<AuthenticationScheme>
// Assert
_result.IsValid.ShouldBeTrue();
}

[Fact]
public async void Should_not_be_valid_if_specified_authentication_provider_is_not_registered()
{
// Arrange
const string key = "JwtLads";

CreateAuthenticationOptions(key);

// Act
await ValidateAsync();

// Assert
_result.IsValid.ShouldBeFalse();
_result.Errors[0].ErrorMessage.ShouldBe($"AuthenticationOptions: AuthenticationProviderKey:'{key}',AuthenticationProviderKeys:[]," +
$"AllowedScopes:[] is unsupported authentication provider");
}

private void GivenAnAuthProvider(string key)
{
var schemes = new List<AuthenticationScheme>
{
new AuthenticationScheme(key, key, typeof(FakeAuthHandler)),
new(key, key, typeof(FakeAuthHandler)),
};

_authProvider
.Setup(x => x.GetAllSchemesAsync())
.ReturnsAsync(schemes);
}
_authProvider
.Setup(x => x.GetAllSchemesAsync())
.ReturnsAsync(schemes);
}

private void ThenTheErrorIs(string providerKey)
private void CreateAuthenticationOptions(string key)
{
_authenticationOptions = new FileAuthenticationOptions
{
_result.Errors[0].ErrorMessage.ShouldBe($"AuthenticationOptions: AuthenticationProviderKey:'{providerKey}',AuthenticationProviderKeys:[]," +
$"AllowedScopes:[] is unsupported authentication provider");
}
AuthenticationProviderKey = key,
};
}

private void ThenTheResultIsValid()
{
_result.IsValid.ShouldBeTrue();
}
private async Task ValidateAsync()
{
_result = await _validator.ValidateAsync(_authenticationOptions);
}

private void ThenTheResultIsNotValid()
private class FakeAuthHandler : IAuthenticationHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
_result.IsValid.ShouldBeFalse();
throw new NotImplementedException();
}

private void GivenThe(FileAuthenticationOptions authenticationOptions)
public Task ChallengeAsync(AuthenticationProperties properties)
{
_authenticationOptions = authenticationOptions;
throw new NotImplementedException();
}

private async Task WhenIValidateAsync()
public Task ForbidAsync(AuthenticationProperties properties)
{
_result = await _validator.ValidateAsync(_authenticationOptions);
throw new NotImplementedException();
}

private class FakeAuthHandler : IAuthenticationHandler
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
public Task<AuthenticateResult> AuthenticateAsync()
{
throw new NotImplementedException();
}

public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}

public Task ForbidAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}

public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
throw new NotImplementedException();
}
throw new NotImplementedException();
}
}
}

0 comments on commit 919abc8

Please sign in to comment.