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

#740 #1580 Support multiple authentication schemes in one route #1870

Merged
merged 66 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
95a9cc1
#1580. Added an opportunity to use several authentication provider keys.
Igor-Polishchuk Jun 5, 2023
2ac543e
Merge branch 'develop' into feature/1580-several-auth-options
raman-m Sep 20, 2023
10343db
Update src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs
MayorSheFF Sep 21, 2023
65ed8d3
#1580. Replaced AuthenticationProviderKeys type from the list to the …
Igor-Polishchuk Sep 21, 2023
e0ed7f0
#1580. Merged from develop.
Igor-Polishchuk Sep 27, 2023
ab3cabb
Merge branch 'ThreeMammals:develop' into feature/1580-several-auth-op…
MayorSheFF Nov 18, 2023
90b9a6e
#1580. Added a doc how to use AuthenticationProviderKeys in a Route.
Nov 18, 2023
0971534
#1580. Amended the description how to use AuthenticationProviderKeys …
Nov 18, 2023
3697a11
Merge branch 'develop' into feature/1580-several-auth-options
Igor-Polishchuk Nov 23, 2023
159c761
#1580. Added an opportunity to use several authentication provider keys.
Igor-Polishchuk Jun 5, 2023
89acbb5
Update src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs
MayorSheFF Sep 21, 2023
500d58f
#1580. Replaced AuthenticationProviderKeys type from the list to the …
Igor-Polishchuk Sep 21, 2023
33c4acc
#1580. Added a doc how to use AuthenticationProviderKeys in a Route.
Nov 18, 2023
81144b6
#1580. Amended the description how to use AuthenticationProviderKeys …
Nov 18, 2023
04c81d6
Quick review
raman-m Dec 2, 2023
d911387
Merge pull request #1 from ThreeMammals/develop
MayorSheFF Dec 16, 2023
bca4858
Merge branch 'develop' into feature/1580-several-auth-options
Igor-Polishchuk Dec 16, 2023
90edc83
#1580. Implemented review points.
Igor-Polishchuk Dec 16, 2023
a6c849c
#1580. Merged from the remote branch.
Igor-Polishchuk Dec 16, 2023
ca7156c
#1580. Initialized result with AuthenticateResult.NoResult().
Igor-Polishchuk Dec 16, 2023
00591a0
#1580. Added @ggnaegi suggestions.
Igor-Polishchuk Dec 16, 2023
75034a6
#1580. Brought back the idea not to allocate AuthenticateResult insta…
Igor-Polishchuk Dec 17, 2023
137afc8
Merge branch 'develop' into feature/1580-several-auth-options
raman-m Dec 18, 2023
681c47b
quick review
raman-m Dec 19, 2023
b44baab
Return Auth result of the last key in the collection
raman-m Dec 19, 2023
15e0f6c
review unit tests
raman-m Dec 27, 2023
481ff76
Enable parallelization of unit tests
raman-m Dec 27, 2023
6d7d45f
Fix messages
raman-m Dec 27, 2023
af0aafd
Disable parallelization for PollyQoSProviderTests
raman-m Dec 27, 2023
970b30b
Switch off unstable test
raman-m Dec 27, 2023
1d5588e
Re-enable parallelization & Isolate unstable test
raman-m Dec 27, 2023
351fb17
Reflection issue in middleware base: remove getting Type object
raman-m Dec 28, 2023
f36d1b3
Switch off unstable test
raman-m Dec 28, 2023
1470b27
Merge branch 'ThreeMammals:develop' into develop
MayorSheFF Dec 28, 2023
4952e39
Merge branch 'develop' into feature/1580-several-auth-options
Dec 28, 2023
76f8e00
Clean code
raman-m Dec 28, 2023
6f50c76
Make MiddlewareName as public property
raman-m Dec 28, 2023
2786962
Code review by @RaynaldM
raman-m Dec 28, 2023
23c8e2f
AuthenticationMiddleware: Line & Branch coverage -> 100%
raman-m Dec 29, 2023
0a5b3d0
AuthenticationOptionsCreator: coverage -> 100%
raman-m Dec 29, 2023
214b66b
Remove private helpers with one reference
raman-m Jan 1, 2024
68ca04e
RouteOptionsCreator: coverage -> 100%
raman-m Jan 1, 2024
2f0f887
FileAuthenticationOptions: Refactor ToString method
raman-m Jan 2, 2024
c3675af
FileConfigurationFluentValidator: coverage -> 100%
raman-m Jan 2, 2024
03973db
RouteFluentValidator: Branch coverage -> 100%
raman-m Jan 3, 2024
1ec8564
TODO and Skip unstable test
raman-m Jan 3, 2024
4815334
Move acceptance tests to the separate folder
raman-m Jan 3, 2024
630f53b
Review and refactor acceptance tests
raman-m Jan 3, 2024
4f5cb09
Add AuthenticationSteps class.
raman-m Jan 8, 2024
1843473
Add 'GivenIHaveATokenWithScope' to 'AuthenticationSteps'
raman-m Jan 11, 2024
5d2eebb
Temporarily disable 'Should_timeout_per_default_after_90_seconds' test
raman-m Jan 11, 2024
2343ac2
Add CreateIdentityServer method
raman-m Jan 11, 2024
6b3335c
Add draft test
raman-m Jan 12, 2024
062c422
Merge branch 'develop' into feature/1580-several-auth-options
raman-m Jan 18, 2024
6fe721f
Update route validator to support multiple auth schemes
raman-m Jan 25, 2024
0f0a856
Acceptance tests
raman-m Feb 4, 2024
248f780
Revert "TODO and Skip unstable test"
raman-m Feb 4, 2024
5ae0393
Revert "Make MiddlewareName as public property"
raman-m Feb 4, 2024
4bfa776
Revert "Reflection issue in middleware base: remove getting Type object"
raman-m Feb 4, 2024
1b953ca
Clean up
raman-m Feb 4, 2024
200a322
Isolate unstable test
raman-m Feb 4, 2024
9773470
Mark old property as `Obsolete`
raman-m Feb 4, 2024
84c3baa
a tiny little bit of cleanup
ggnaegi Feb 5, 2024
1490859
Handling cases when principal or identity are null
ggnaegi Feb 5, 2024
99a1a3b
Update Authentication feature docs
raman-m Feb 5, 2024
9868b03
Convert back to block scoped namespace
raman-m Feb 5, 2024
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
126 changes: 98 additions & 28 deletions docs/features/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,91 @@ Authentication
==============

In order to authenticate Routes and subsequently use any of Ocelot's claims based features such as authorization or modifying the request with values from the token,
users must register authentication services in their **Startup.cs** as usual but they provide a scheme (authentication provider key) with each registration e.g.
users must register authentication services in their **Startup.cs** as usual but they provide `a scheme <https://learn.microsoft.com/en-us/aspnet/core/security/authentication/#authentication-scheme>`_
(authentication provider key) with each registration e.g.

.. code-block:: csharp
public void ConfigureServices(IServiceCollection services)
{
var authenticationProviderKey = "TestKey";
const string AuthenticationProviderKey = "MyKey";
services
.AddAuthentication()
.AddJwtBearer(authenticationProviderKey,
options => { /* custom auth-setup */ });
.AddJwtBearer(AuthenticationProviderKey, options =>
{
// Custom Authentication setup via options initialization
});
}
In this example "**TestKey**" is the scheme that this provider has been registered with. We then map this to a Route in the configuration e.g.
In this example ``MyKey`` is `the scheme <https://learn.microsoft.com/en-us/aspnet/core/security/authentication/#authentication-scheme>`_ that this provider has been registered with.
We then map this to a Route in the configuration using the following `AuthenticationOptions <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20AuthenticationOptions&type=code>`_ properties:

* ``AuthenticationProviderKey`` is a string object, obsolete. [#f1]_ This is legacy definition when you define :ref:`authentication-single` (scheme).
* ``AuthenticationProviderKeys`` is an array of strings, the recommended definition of :ref:`authentication-multiple` feature.

.. authentication-single:
Single Key [#f1]_
-----------------

| Property: ``AuthenticationOptions.AuthenticationProviderKey``
We map authentication provider to a Route in the configuration e.g.

.. code-block:: json
"Routes": [{
"AuthenticationOptions": {
"AuthenticationProviderKey": "TestKey",
"AllowedScopes": []
}
}]
"AuthenticationOptions": {
"AuthenticationProviderKey": "MyKey",
"AllowedScopes": []
}
When Ocelot runs it will look at this Routes ``AuthenticationOptions.AuthenticationProviderKey`` and check that there is an authentication provider registered with the given key.
When Ocelot runs it will look at this Routes ``AuthenticationProviderKey`` and check that there is an authentication provider registered with the given key.
If there isn't then Ocelot will not start up. If there is then the Route will use that provider when it executes.

If a Route is authenticated, Ocelot will invoke whatever scheme is associated with it while executing the authentication middleware.
If the request fails authentication, Ocelot returns a HTTP status code `401 Unauthorized <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401>`_.

.. authentication-multiple:
Multiple Authentication Schemes [#f2]_
--------------------------------------

| Property: ``AuthenticationOptions.AuthenticationProviderKeys``
In real world of ASP.NET, apps may need to support multiple types of authentication by single Ocelot app instance.
To register `multiple authentication schemes <https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme#use-multiple-authentication-schemes>`_
(authentication provider keys) for each appropriate authentication provider, use and develop this abstract configuration of two or more schemes:

.. code-block:: csharp
public void ConfigureServices(IServiceCollection services)
{
const string DefaultScheme = JwtBearerDefaults.AuthenticationScheme; // Bearer
services.AddAuthentication()
.AddJwtBearer(DefaultScheme, options => { /* JWT setup */ })
// AddJwtBearer, AddCookie, AddIdentityServerAuthentication etc.
.AddMyProvider("MyKey", options => { /* Custom auth setup */ });
}
In this example, the schemes ``MyKey`` and ``Bearer`` represent the keys which these providers have been registered with.
We then map these schemes to a Route in the configuration, as shown below

.. code-block:: json
"AuthenticationOptions": {
"AuthenticationProviderKeys": [ "Bearer", "MyKey" ] // The order matters!
"AllowedScopes": []
}
Afterward, Ocelot applies all steps that are specified for ``AuthenticationProviderKey`` as :ref:`authentication-single`.

**Note** that the order of the keys in an array definition does matter! We use a "First One Wins" authentication strategy.

Finally, we would say that registering providers, initializing options, forwarding authentication artifacts can be a "real" coding challenge.
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%20MultipleAuthSchemesFeatureTests&type=code>`_
(currently for `Identity Server 4 <https://identityserver4.readthedocs.io/>`_ only).
We would appreciate any new PRs to add extra acceptance tests for your custom scenarios with `multiple authentication schemes <https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme#use-multiple-authentication-schemes>`__. [#f2]_

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

Expand All @@ -41,7 +96,7 @@ If you want to authenticate using JWT tokens maybe from a provider like `Auth0 <
public void ConfigureServices(IServiceCollection services)
{
var authenticationProviderKey = "TestKey";
var authenticationProviderKey = "MyKey";
services
.AddAuthentication()
.AddJwtBearer(authenticationProviderKey, options =>
Expand All @@ -56,12 +111,16 @@ Then map the authentication provider key to a Route in your configuration e.g.

.. code-block:: json
"Routes": [{
"AuthenticationOptions": {
"AuthenticationProviderKey": "TestKey",
"AllowedScopes": []
}
}]
"AuthenticationOptions": {
"AuthenticationProviderKeys": [ "MyKey" ],
"AllowedScopes": []
}
Docs
^^^^

* Microsoft Learn: `Authentication and authorization in minimal APIs <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security>`_
* Andrew Lock | .NET Escapades: `A look behind the JWT bearer authentication middleware in ASP.NET Core <https://andrewlock.net/a-look-behind-the-jwt-bearer-authentication-middleware-in-asp-net-core/>`_

Identity Server Bearer Tokens
-----------------------------
Expand All @@ -73,7 +132,7 @@ If you don't understand how to do this, please consult the IdentityServer `docum
public void ConfigureServices(IServiceCollection services)
{
var authenticationProviderKey = "TestKey";
var authenticationProviderKey = "MyKey";
Action<JwtBearerOptions> options = (opt) =>
{
opt.Authority = "https://whereyouridentityserverlives.com";
Expand All @@ -89,12 +148,10 @@ Then map the authentication provider key to a Route in your configuration e.g.

.. code-block:: json
"Routes": [{
"AuthenticationOptions": {
"AuthenticationProviderKey": "TestKey",
"AllowedScopes": []
}
}]
"AuthenticationOptions": {
"AuthenticationProviderKeys": [ "MyKey" ],
"AllowedScopes": []
}
Auth0 by Okta
-------------
Expand Down Expand Up @@ -137,8 +194,21 @@ If you add scopes to **AllowedScopes**, Ocelot will get all the user claims (fro

This is a way to restrict access to a Route on a per scope basis.

More identity providers
-----------------------
Links
-----

* Microsoft Learn: `Overview of ASP.NET Core authentication <https://learn.microsoft.com/en-us/aspnet/core/security/authentication/>`_
* Microsoft Learn: `Authorize with a specific scheme in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme>`_
* Microsoft Learn: `Policy schemes in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes>`_
* Microsoft .NET Blog: `ASP.NET Core Authentication with IdentityServer4 <https://devblogs.microsoft.com/dotnet/asp-net-core-authentication-with-identityserver4/>`_

Future
------

We invite you to add more examples, if you have integrated with other identity providers and the integration solution is working.
Please, open `Show and tell <https://github.com/ThreeMammals/Ocelot/discussions/categories/show-and-tell>`_ discussion in the repository.

""""

.. [#f1] Use the ``AuthenticationProviderKeys`` property instead of ``AuthenticationProviderKey`` one. We supports this obsolete property because of backward compatibility and allowing migrations. In future releases the property can be removed as a breaking change.
.. [#f2] `Multiple authentication schemes <https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme#use-multiple-authentication-schemes>`__ feature was requested in issues `740 <https://github.com/ThreeMammals/Ocelot/issues/740>`_, `1580 <https://github.com/ThreeMammals/Ocelot/issues/1580>`_ and delivered as a part of `23.0 <https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0>`_ release.
88 changes: 63 additions & 25 deletions src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,93 @@

namespace Ocelot.Authentication.Middleware
{
public class AuthenticationMiddleware : OcelotMiddleware
public sealed class AuthenticationMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;

public AuthenticationMiddleware(RequestDelegate next,
IOcelotLoggerFactory loggerFactory)
public AuthenticationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory)
: base(loggerFactory.CreateLogger<AuthenticationMiddleware>())
{
_next = next;
}

public async Task Invoke(HttpContext httpContext)
{
var request = httpContext.Request;
var path = httpContext.Request.Path;
var downstreamRoute = httpContext.Items.DownstreamRoute();

if (httpContext.Request.Method.ToUpper() != "OPTIONS" && IsAuthenticatedRoute(downstreamRoute))
// reducing nesting, returning early when no authentication is needed.
if (request.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase) || !downstreamRoute.IsAuthenticated)
{
Logger.LogInformation(() => $"{httpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated");
Logger.LogInformation($"No authentication needed for path '{path}'.");
await _next(httpContext);
return;
}

var result = await httpContext.AuthenticateAsync(downstreamRoute.AuthenticationOptions.AuthenticationProviderKey);
Logger.LogInformation(() => $"The path '{path}' is an authenticated route! {MiddlewareName} checking if client is authenticated...");

httpContext.User = result.Principal;
var result = await AuthenticateAsync(httpContext, downstreamRoute);

if (httpContext.User.Identity.IsAuthenticated)
{
Logger.LogInformation(() => $"Client has been authenticated for {httpContext.Request.Path}");
await _next.Invoke(httpContext);
}
else
{
var error = new UnauthenticatedError(
$"Request for authenticated route {httpContext.Request.Path} by {httpContext.User.Identity.Name} was unauthenticated");
if (result.Principal?.Identity == null)
{
SetUnauthenticatedError(httpContext, path, null);
return;
}

Logger.LogWarning(() =>$"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}");
httpContext.User = result.Principal;

httpContext.Items.SetError(error);
}
}
else
if (httpContext.User.Identity.IsAuthenticated)
{
Logger.LogInformation(() => $"No authentication needed for {httpContext.Request.Path}");

Logger.LogInformation(() => $"Client has been authenticated for path '{path}' by '{httpContext.User.Identity.AuthenticationType}' scheme.");
await _next.Invoke(httpContext);
return;
}

SetUnauthenticatedError(httpContext, path, httpContext.User.Identity.Name);
}

private void SetUnauthenticatedError(HttpContext httpContext, string path, string userName)
{
var error = new UnauthenticatedError($"Request for authenticated route '{path}' {(string.IsNullOrEmpty(userName) ? "was unauthenticated" : $"by '{userName}' was unauthenticated!")}");
Logger.LogWarning(() => $"Client has NOT been authenticated for path '{path}' and pipeline error set. {error};");
httpContext.Items.SetError(error);
}

private static bool IsAuthenticatedRoute(DownstreamRoute route)
private async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, DownstreamRoute route)
{
return route.IsAuthenticated;
var options = route.AuthenticationOptions;
if (!string.IsNullOrWhiteSpace(options.AuthenticationProviderKey))
{
return await context.AuthenticateAsync(options.AuthenticationProviderKey);
}

var providerKeys = options.AuthenticationProviderKeys;
if (providerKeys.Length == 0 || providerKeys.All(string.IsNullOrWhiteSpace))
{
Logger.LogWarning(() => $"Impossible to authenticate client for path '{route.DownstreamPathTemplate}': both {nameof(options.AuthenticationProviderKey)} and {nameof(options.AuthenticationProviderKeys)} are empty but the {nameof(Configuration.AuthenticationOptions)} have defined.");
return AuthenticateResult.NoResult();
}

AuthenticateResult result = null;
foreach (var scheme in providerKeys.Where(apk => !string.IsNullOrWhiteSpace(apk)))
{
try
{
result = await context.AuthenticateAsync(scheme);
if (result?.Succeeded == true)
{
return result;
}
}
catch (Exception e)
{
Logger.LogWarning(() =>
$"Impossible to authenticate client for path '{route.DownstreamPathTemplate}' and {nameof(options.AuthenticationProviderKey)}:{scheme}. Error: {e.Message}.");
}
}

return result ?? AuthenticateResult.NoResult();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Builder;

namespace Ocelot.Authentication.Middleware;

public static class AuthenticationMiddlewareExtensions
{
public static IApplicationBuilder UseAuthenticationMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<AuthenticationMiddleware>();
}
}

This file was deleted.

63 changes: 50 additions & 13 deletions src/Ocelot/Configuration/AuthenticationOptions.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
namespace Ocelot.Configuration
{
public class AuthenticationOptions
{
public AuthenticationOptions(List<string> allowedScopes, string authenticationProviderKey)
{
AllowedScopes = allowedScopes;
AuthenticationProviderKey = authenticationProviderKey;
}

public List<string> AllowedScopes { get; }
public string AuthenticationProviderKey { get; }
}
using Ocelot.Configuration.File;

namespace Ocelot.Configuration
{
public sealed class AuthenticationOptions
{
public AuthenticationOptions(List<string> allowedScopes, string authenticationProviderKey)
{
AllowedScopes = allowedScopes;
AuthenticationProviderKey = authenticationProviderKey;
AuthenticationProviderKeys = [];
}

public AuthenticationOptions(FileAuthenticationOptions from)
{
AllowedScopes = from.AllowedScopes ?? [];
AuthenticationProviderKey = from.AuthenticationProviderKey ?? string.Empty;
AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? [];
}

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

public List<string> AllowedScopes { get; }

/// <summary>
/// Authentication scheme registered in DI services with appropriate authentication provider.
/// </summary>
/// <value>
/// A <see langword="string"/> value of the scheme name.
/// </value>
[Obsolete("Use the " + nameof(AuthenticationProviderKeys) + " property!")]
public string AuthenticationProviderKey { get; }

/// <summary>
/// Multiple authentication schemes registered in DI services with appropriate authentication providers.
/// </summary>
/// <remarks>
/// The order in the collection matters: first successful authentication result wins.
/// </remarks>
/// <value>
/// An array of <see langword="string"/> values of the scheme names.
/// </value>
public string[] AuthenticationProviderKeys { get; }
}
}
Loading