Skip to content

#842 #1414 AuthenticationOptions in global configuration #2114

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

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -4,7 +4,11 @@ namespace Ocelot.Configuration.Creator
{
public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator
{
public AuthenticationOptions Create(FileRoute route)
=> new(route?.AuthenticationOptions ?? new());
public AuthenticationOptions Create(FileAuthenticationOptions routeAuthOptions, FileAuthenticationOptions globalConfAuthOptions)
{
var routeAuthOptionsEmpty = routeAuthOptions?.HasProviderKey != true;
var resultAuthOptions = routeAuthOptionsEmpty ? globalConfAuthOptions : routeAuthOptions;
return new(resultAuthOptions ?? new());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Ocelot.Configuration.Creator
{
public interface IAuthenticationOptionsCreator
{
AuthenticationOptions Create(FileRoute route);
AuthenticationOptions Create(FileAuthenticationOptions routeAuthOptions, FileAuthenticationOptions globalConfAuthOptions);
}
}
2 changes: 1 addition & 1 deletion src/Ocelot/Configuration/Creator/IRouteOptionsCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Ocelot.Configuration.Creator
{
public interface IRouteOptionsCreator
{
RouteOptions Create(FileRoute fileRoute);
RouteOptions Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration);
}
}
13 changes: 6 additions & 7 deletions src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator
{
public class RouteOptionsCreator : IRouteOptionsCreator
{
public RouteOptions Create(FileRoute fileRoute)
public RouteOptions Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration)
{
if (fileRoute == null)
{
return new RouteOptionsBuilder().Build();
}

var authOpts = fileRoute.AuthenticationOptions;
var isAuthenticated = authOpts != null
&& (!string.IsNullOrEmpty(authOpts.AuthenticationProviderKey)
|| authOpts.AuthenticationProviderKeys?.Any(k => !string.IsNullOrWhiteSpace(k)) == true);

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

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

// TODO: This sounds more like a hack, it might be better to refactor this at some point.
Expand Down
4 changes: 2 additions & 2 deletions src/Ocelot/Configuration/Creator/RoutesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ public List<Route> Create(FileConfiguration fileConfiguration)

private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration)
{
var fileRouteOptions = _fileRouteOptionsCreator.Create(fileRoute);
var fileRouteOptions = _fileRouteOptionsCreator.Create(fileRoute, globalConfiguration);

var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration);

var routeKey = _routeKeyCreator.Create(fileRoute);

var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);

var authOptionsForRoute = _authOptionsCreator.Create(fileRoute);
var authOptionsForRoute = _authOptionsCreator.Create(fileRoute.AuthenticationOptions, globalConfiguration?.AuthenticationOptions);

var claimsToHeaders = _claimsToThingCreator.Create(fileRoute.AddHeadersToRequest);

Expand Down
11 changes: 11 additions & 0 deletions src/Ocelot/Configuration/File/FileAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ public FileAuthenticationOptions(FileAuthenticationOptions from)

public List<string> AllowedScopes { get; set; }

/// <summary>
/// Allows anonymous authentication for route when global AuthenticationOptions are used.
/// </summary>
/// <value>
/// <see langword="true"/> if it is allowed; otherwise, <see langword="false"/>.
/// </value>
public bool AllowAnonymous { get; set; }

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

public string[] AuthenticationProviderKeys { get; set; }

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

public override string ToString() => new StringBuilder()
.Append($"{nameof(AuthenticationProviderKey)}:'{AuthenticationProviderKey}',")
.Append($"{nameof(AuthenticationProviderKeys)}:[{string.Join(',', AuthenticationProviderKeys.Select(x => $"'{x}'"))}],")
Expand Down
3 changes: 3 additions & 0 deletions src/Ocelot/Configuration/File/FileGlobalConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public FileGlobalConfiguration()
HttpHandlerOptions = new FileHttpHandlerOptions();
CacheOptions = new FileCacheOptions();
MetadataOptions = new FileMetadataOptions();
AuthenticationOptions = new FileAuthenticationOptions();
}

public string RequestIdKey { get; set; }
Expand Down Expand Up @@ -48,5 +49,7 @@ public FileGlobalConfiguration()
public FileCacheOptions CacheOptions { get; set; }

public FileMetadataOptions MetadataOptions { get; set; }

public FileAuthenticationOptions AuthenticationOptions { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Validator;

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

public FileAuthenticationOptionsValidator(IAuthenticationSchemeProvider authenticationSchemeProvider)
{
_authenticationSchemeProvider = authenticationSchemeProvider;

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

private async Task<bool> IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)
{
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ namespace Ocelot.Configuration.Validator
{
public class FileGlobalConfigurationFluentValidator : AbstractValidator<FileGlobalConfiguration>
{
public FileGlobalConfigurationFluentValidator(FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
public FileGlobalConfigurationFluentValidator(FileQoSOptionsFluentValidator fileQoSOptValidator, FileAuthenticationOptionsValidator fileAuthOptValidator)
{
RuleFor(configuration => configuration.QoSOptions)
.SetValidator(fileQoSOptionsFluentValidator);
.SetValidator(fileQoSOptValidator);

RuleFor(configuration => configuration.AuthenticationOptions)
.SetValidator(fileAuthOptValidator);
}
}
}
31 changes: 7 additions & 24 deletions src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Validator
{
public class RouteFluentValidator : AbstractValidator<FileRoute>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
public RouteFluentValidator(
HostAndPortValidator hostAndPortValidator,
FileQoSOptionsFluentValidator fileQoSOptsValidator,
FileAuthenticationOptionsValidator fileAuthOptsValidator)
{
_authenticationSchemeProvider = authenticationSchemeProvider;

RuleFor(route => route.QoSOptions)
.SetValidator(fileQoSOptionsFluentValidator);
.SetValidator(fileQoSOptsValidator);

RuleFor(route => route.DownstreamPathTemplate)
.NotEmpty()
Expand Down Expand Up @@ -66,8 +65,7 @@ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemePr
});

RuleFor(route => route.AuthenticationOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider");
.SetValidator(fileAuthOptsValidator);

When(route => string.IsNullOrEmpty(route.ServiceName), () =>
{
Expand All @@ -92,21 +90,6 @@ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemePr
});
}

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

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

private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
{
if (string.IsNullOrEmpty(rateLimitOptions.Period))
Expand Down
1 change: 1 addition & 0 deletions src/Ocelot/DependencyInjection/OcelotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.TryAddSingleton<RouteFluentValidator>();
Services.TryAddSingleton<FileGlobalConfigurationFluentValidator>();
Services.TryAddSingleton<FileQoSOptionsFluentValidator>();
Services.TryAddSingleton<FileAuthenticationOptionsValidator>();
Services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>();
Services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>();
Services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ internal Task<BearerToken> GivenAuthToken(string url, string apiScope, string cl
return GivenIHaveATokenWithForm(url, form);
}

public static FileRoute GivenDefaultAuthRoute(int port, string upstreamHttpMethod = null, string authProviderKey = null) => new()
public static FileRoute GivenDefaultAuthRoute(int port, string upstreamHttpMethod = null, string authProviderKey = null, bool allowAnonymous = false) => new()
{
DownstreamPathTemplate = "/",
DownstreamHostAndPorts = new()
Expand All @@ -151,6 +151,15 @@ internal Task<BearerToken> GivenAuthToken(string url, string apiScope, string cl
AuthenticationOptions = new()
{
AuthenticationProviderKeys = new string[] { authProviderKey ?? "Test" },
AllowAnonymous = allowAnonymous,
},
};

public static FileGlobalConfiguration GivenGlobalConfig() => new()
{
AuthenticationOptions = new()
{
AuthenticationProviderKey = "key",
},
};

Expand Down
34 changes: 34 additions & 0 deletions test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,40 @@ public void Should_return_201_using_identity_server_reference_token()
.BDDfy();
}

[Fact]
public void Should_use_global_authentication_and_return_401_when_no_token()
{
var port = PortFinder.GetRandomPort();
var route = GivenDefaultAuthRoute(port);
route.AuthenticationOptions.AuthenticationProviderKeys = null;
var globalConfig = GivenGlobalConfig();
var configuration = GivenConfiguration(globalConfig, route);
this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, AccessTokenType.Reference))
.And(x => x.GivenThereIsAServiceRunningOn(DownstreamServiceUrl(port), HttpStatusCode.OK, string.Empty))
.And(x => GivenThereIsAConfiguration(configuration))
.And(x => GivenOcelotIsRunning(_options, "key"))
.When(x => WhenIGetUrlOnTheApiGateway("/"))
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized))
.BDDfy();
}

[Fact]
public void Should_allow_anonymous_route_and_return_200_when_global_auth_options_and_no_token()
{
var port = PortFinder.GetRandomPort();
var route = GivenDefaultAuthRoute(port, allowAnonymous: true);
route.AuthenticationOptions.AuthenticationProviderKeys = null;
var globalConfig = GivenGlobalConfig();
var configuration = GivenConfiguration(globalConfig, route);
this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, AccessTokenType.Reference))
.And(x => x.GivenThereIsAServiceRunningOn(DownstreamServiceUrl(port), HttpStatusCode.OK, string.Empty))
.And(x => GivenThereIsAConfiguration(configuration))
.And(x => GivenOcelotIsRunning(_options, "key"))
.When(x => WhenIGetUrlOnTheApiGateway("/"))
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.BDDfy();
}

[IgnorePublicMethod]
public async Task GivenThereIsAnIdentityServerOn(string url, AccessTokenType tokenType)
{
Expand Down
Loading