From 2b0e2cc2235d51eeee990b14834bca06ce17ace8 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Wed, 13 Mar 2024 11:58:35 +0100 Subject: [PATCH 01/16] First successfull execution of Azure OnBehalfOf flow --- .../Authentication/GatewayAuthentication.cs | 1 - .../GatewayAuthenticationSettings.cs | 2 +- .../Routing/Auth/AuthStrategyFactory.cs | 68 ++++++ .../Auth/AzureOnBehalfOfAuthStrategy.cs | 151 +++++++++++++ .../Routing/Auth/IAuthStrategy.cs | 34 +++ .../Auth/NoAuthenticationAuthStrategy.cs | 58 +++++ .../Auth/TokenPassThroughAuthStrategy.cs | 95 ++++++++ .../GatewayForwarderHttpTransformer.cs | 18 +- .../Routing/GatewayPipeline.cs | 17 +- .../Routing/GatewayRouter.cs | 212 +++++++----------- .../Routing/GatewayRouting.cs | 17 +- .../Routing/GatewayRoutingSettings.cs | 32 ++- 12 files changed, 551 insertions(+), 154 deletions(-) create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs index 88fe2cd..6dfa918 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs @@ -174,7 +174,6 @@ internal static void UseGatewayAuthentication(WebApplication app) // Add the default asp.net core middlewares for authentication and authorization app.UseAuthentication(); app.UseAuthorization(); - //app.UseCookiePolicy(); // Custom Middleware to read current user into token service app.Use(async (context, next) => diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthenticationSettings.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthenticationSettings.cs index 8b72950..9db2e06 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthenticationSettings.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthenticationSettings.cs @@ -68,7 +68,7 @@ public class GatewayAuthenticationSettings /// /// true if the handler shall query the user info endpoint of the authorization server; otherwise, false. /// - public bool QueryUserInfoEndpoint { get; set; } = true; + public bool QueryUserInfoEndpoint { get; set; } = false; /// /// Gets or sets the issuer address for sign out. diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs new file mode 100644 index 0000000..aa9d098 --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs @@ -0,0 +1,68 @@ +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// A class to create instances of auth strategies for specific routes. +/// +public class AuthStrategyFactory +{ + /// + /// The authentication strategies. + /// + private Dictionary _authStrategies = new Dictionary(); + + /// + /// The authentication strategy instances. + /// + private Dictionary _authStrategyInstances = new Dictionary(); + + /// + /// The routing settings. + /// + private readonly GatewayRoutingSettings _routingSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The routing settings. + public AuthStrategyFactory(GatewayRoutingSettings routingSettings) + { + _routingSettings = routingSettings; + } + + /// + /// Gets the authentication strategy. + /// + /// The name. + /// An instance of the auth strategy. + public IAuthStrategy GetAuthStrategy(string route) + { + if(! _authStrategyInstances.ContainsKey(route)) + { + string strategyName = _routingSettings.Routes[route].Authentication.Strategy; + + Type authStrategyType = _authStrategies[strategyName]; + + // Create a new instance of the auth strategy + IAuthStrategy? instance = Activator.CreateInstance(authStrategyType, _routingSettings.Routes[route].Authentication) as IAuthStrategy; + + if (instance == null) throw new InvalidOperationException("Auth strategy could not be instantiated"); + + _authStrategyInstances[route] = instance; + } + + return _authStrategyInstances[route]; + } + + public void AddAuthStrategy(string name, Type authStrategyType) + { + _authStrategies.Add(name, authStrategyType); + } + + /// + /// Clears the authentication strategies. + /// + public void ClearAuthStrategies() + { + _authStrategies.Clear(); + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs new file mode 100644 index 0000000..8b5aa0f --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs @@ -0,0 +1,151 @@ +using Fancy.ResourceLinker.Gateway.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + + +class TokenExchangeResponse +{ + public string access_token { get; set; } = ""; + public string refresh_token { get; set; } = ""; + public long expires_in { get; set; } +} + +/// +/// An auth strategy which just passes through the current token +/// +internal class AzureOnBehalfOfAuthStrategy : IAuthStrategy +{ + /// + /// The name of the auth strategy. + /// + public const string NAME = "AzureOnBehalfOf"; + + /// + /// The HTTP client. + /// + private readonly HttpClient _httpClient = new HttpClient(); + + /// + /// The route authentication settings. + /// + private readonly RouteAuthenticationSettings _routeAuthenticationSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The token service. + public AzureOnBehalfOfAuthStrategy(RouteAuthenticationSettings routeAuthenticationSettings) + { + _routeAuthenticationSettings = routeAuthenticationSettings; + } + + /// + /// Gets the name of the strategy. + /// + /// + /// The name. + /// + public string Name => NAME; + + /// + /// Sets the authentication to an http context asynchronous. + /// + /// The http context. + /// + /// A task indicating the completion of the asynchronous operation + /// + public async Task SetAuthenticationAsync(HttpContext context) + { + TokenService? tokenService = TryGetTokenService(context.RequestServices); + + if (tokenService != null) + { + string? accessToken = await tokenService.GetAccessTokenAsync(); + context.Request.Headers.Add("Authorization", "Bearer " + accessToken); + } + } + + /// + /// Sets the authentication to an http request message asynchronous. + /// + /// The current service provider. + /// The http request message. + /// + /// A task indicating the completion of the asynchronous operation + /// + public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) + { + string? accessToken = await TryGetOnBehalfOfTokenAsync(serviceProvider); + + if (accessToken != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + } + } + + private async Task TryGetOnBehalfOfTokenAsync(IServiceProvider serviceProvider) + { + DiscoveryDocumentService discoveryDocumentService = serviceProvider.GetRequiredService(); + GatewayAuthenticationSettings authSettings = serviceProvider.GetRequiredService(); + var disoveryDocument = await discoveryDocumentService.LoadDiscoveryDocumentAsync(authSettings.Authority); + + TokenService? tokenService = TryGetTokenService(serviceProvider); + string? accessToken = await tokenService?.GetAccessTokenAsync(); + + if (accessToken != null) + { + var payload = new Dictionary + { + { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }, + { "client_id", _routeAuthenticationSettings.Options["ClientId"] }, + { "client_secret", _routeAuthenticationSettings.Options["ClientSecret"] }, + { "assertion", accessToken }, + { "scope", _routeAuthenticationSettings.Options["Scope"] }, + { "requested_token_use", "on_behalf_of" }, + }; + + FormUrlEncodedContent content = new FormUrlEncodedContent(payload); + HttpResponseMessage httpResponse = await _httpClient.PostAsync(disoveryDocument.TokenEndpoint, content); + + try + { + httpResponse.EnsureSuccessStatusCode(); + } + catch(HttpRequestException) + { + ILogger logger = serviceProvider.GetRequiredService>(); + string errorResponse = new StreamReader(httpResponse.Content.ReadAsStream()).ReadToEnd(); + logger.LogError("Error on on behalf of token exchange. Response from server " + errorResponse); + throw; + } + + TokenExchangeResponse? response = await httpResponse.Content.ReadFromJsonAsync(); + return response?.access_token; + } + return null; + } + + /// + /// Tries to get the token service. + /// + /// The service provider. + /// An instance of the current token service or null if no token service is availalbe. + private TokenService? TryGetTokenService(IServiceProvider serviceProvider) + { + try + { + return serviceProvider.GetService(); + } + catch (InvalidOperationException) + { + // No token service available + return null; + } + } +} + diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs new file mode 100644 index 0000000..7aa9ad5 --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// Interface for an authorization strategy. +/// +public interface IAuthStrategy +{ + /// + /// Gets the name of the strategy. + /// + /// + /// The name. + /// + string Name { get; } + + /// + /// Sets the authentication to an http context asynchronous. + /// + /// The http context. + /// A task indicating the completion of the asynchronous operation + Task SetAuthenticationAsync(HttpContext context); + + /// + /// Sets the authentication to an http request message asynchronous. + /// + /// The current service provider. + /// The http request message. + /// + /// A task indicating the completion of the asynchronous operation + /// + Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request); +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs new file mode 100644 index 0000000..8e886f4 --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Http; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// An authentication strategy which does not set any authentication. +/// +public class NoAuthenticationAuthStrategy : IAuthStrategy +{ + /// + /// The name of the auth strategy + /// + public const string NAME = "NoAuthentication"; + + /// + /// Initializes a new instance of the class. + /// + /// The authentication settings. + public NoAuthenticationAuthStrategy(RouteAuthenticationSettings authenticationSettings) + { + } + + + /// + /// Gets the name of the strategy. + /// + /// + /// The name. + /// + public string Name => NAME; + + /// + /// Sets the authentication to an http context asynchronous. + /// + /// The http context. + /// + /// A task indicating the completion of the asynchronous operation + /// + public Task SetAuthenticationAsync(HttpContext context) + { + // Nothing to do here! + return Task.CompletedTask; + } + + /// + /// Sets the authentication to an http request message asynchronous. + /// + /// The current service provider. + /// The http request message. + /// + /// A task indicating the completion of the asynchronous operation + /// + public Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) + { + // Nothing to do here! + return Task.CompletedTask; + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs new file mode 100644 index 0000000..3a42c29 --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs @@ -0,0 +1,95 @@ +using Fancy.ResourceLinker.Gateway.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// An auth strategy which just passes through the current token +/// +internal class TokenPassThroughAuthStrategy : IAuthStrategy +{ + /// + /// The name of the auth strategy. + /// + public const string NAME = "TokenPassThrough"; + + /// + /// The authentication settings. + /// + private readonly RouteAuthenticationSettings _authenticationSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The token service. + public TokenPassThroughAuthStrategy(RouteAuthenticationSettings authenticationSettings) + { + _authenticationSettings = authenticationSettings; + } + + /// + /// Gets the name of the strategy. + /// + /// + /// The name. + /// + public string Name => NAME; + + /// + /// Sets the authentication to an http context asynchronous. + /// + /// The http context. + /// + /// A task indicating the completion of the asynchronous operation + /// + public async Task SetAuthenticationAsync(HttpContext context) + { + TokenService? tokenService = TryGetTokenService(context.RequestServices); + + if (tokenService != null) + { + string? accessToken = await tokenService.GetAccessTokenAsync(); + context.Request.Headers.Add("Authorization", "Bearer " + accessToken); + } + } + + /// + /// Sets the authentication to an http request message asynchronous. + /// + /// The current service provider. + /// The http request message. + /// + /// A task indicating the completion of the asynchronous operation + /// + public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) + { + TokenService? tokenService = TryGetTokenService(serviceProvider); + + if (tokenService != null) + { + string? accessToken = await tokenService.GetAccessTokenAsync(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + } + } + + /// + /// Tries to get the token service. + /// + /// The service provider. + /// An instance of the current token service or null if no token service is availalbe. + private TokenService? TryGetTokenService(IServiceProvider serviceProvider) + { + try + { + return serviceProvider.GetService(); + } + catch (InvalidOperationException) + { + // No token service available + return null; + } + } +} \ No newline at end of file diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs index eb0dea8..a97caac 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs @@ -1,5 +1,7 @@ using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Routing.Auth; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using System.Net.Http.Headers; using Yarp.ReverseProxy.Forwarder; @@ -14,7 +16,7 @@ internal class GatewayForwarderHttpTransformer : HttpTransformer /// /// The send access token item key. /// - internal static readonly string SendAccessTokenItemKey = "SendAccessTokenItemKey"; + internal static readonly string RouteNameItemKey = "RouteNameItemKey"; /// /// The target URL item key. @@ -25,14 +27,14 @@ public override async ValueTask TransformRequestAsync(HttpContext httpContext, H { await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); - if (Convert.ToBoolean(httpContext.Items[SendAccessTokenItemKey])) - { - // Add access token - TokenService? tokenService = httpContext.RequestServices.GetService(typeof(TokenService)) as TokenService; - - if (tokenService == null) throw new InvalidOperationException($"If 'EnforceAuthentication' is 'true', gateway authentication must be configured."); + string? routeName = httpContext.Items[RouteNameItemKey]?.ToString(); - proxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await tokenService.GetAccessTokenAsync()); + if(!string.IsNullOrEmpty(routeName)) + { + // Add authentication + AuthStrategyFactory authStrategyFactory = httpContext.RequestServices.GetRequiredService(); + IAuthStrategy authStrategy = authStrategyFactory.GetAuthStrategy(routeName); + await authStrategy.SetAuthenticationAsync(httpContext.RequestServices, proxyRequest); } if(httpContext.Items.ContainsKey(TargetUrlItemKey)) diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs index 695ee02..cac608d 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs @@ -1,9 +1,7 @@ -using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Routing.Auth; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Fancy.ResourceLinker.Gateway.Routing; @@ -23,15 +21,12 @@ internal static void UseGatewayPipeline(this IReverseProxyApplicationBuilder pip // Check if token shall be added var proxyFeature = context.GetReverseProxyFeature(); if (proxyFeature.Route.Config.Metadata != null - && proxyFeature.Route.Config.Metadata.ContainsKey("EnforceAuthentication") - && proxyFeature.Route.Config.Metadata["EnforceAuthentication"] == "True") + && proxyFeature.Route.Config.Metadata.ContainsKey("RouteName")) { - // Add access token to request - var tokenService = context.RequestServices.GetRequiredService(); - var logger = context.RequestServices.GetRequiredService().CreateLogger("Fancy.ResourceLinker.Gateway.Routing.GatewayPipeline"); - logger.LogDebug("Adding Authorization header and token into request to " + context.Request.GetDisplayUrl()); - var accessToken = await tokenService.GetAccessTokenAsync(); - context.Request.Headers.Add("Authorization", "Bearer " + accessToken); + string routeName = proxyFeature.Route.Config.Metadata["RouteName"]; + AuthStrategyFactory authStrategyFactory = context.RequestServices.GetRequiredService(); + IAuthStrategy authStrategy = authStrategyFactory.GetAuthStrategy(routeName); + await authStrategy.SetAuthenticationAsync(context); } await next().ConfigureAwait(false); }); diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs index f873c2f..96e77e9 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs @@ -1,11 +1,9 @@ -using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Routing.Auth; using Fancy.ResourceLinker.Models.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Net; -using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Yarp.ReverseProxy.Forwarder; @@ -20,7 +18,7 @@ public class GatewayRouter /// /// The HTTP client. /// - private readonly HttpClient _httpClient = new HttpClient(); + private static readonly HttpClient _httpClient = new HttpClient(); /// /// The forwarder message invoker. @@ -58,14 +56,14 @@ public class GatewayRouter private readonly IResourceCache _resourceCache; /// - /// The token service. + /// The authentication strategy factory. /// - private readonly TokenService? _tokenService; + private readonly AuthStrategyFactory _authStrategyFactory; /// - /// The token client. + /// The service provider. /// - private readonly TokenClient? _tokenClient; + private readonly IServiceProvider _serviceProvider; /// /// Initializes a new instance of the class. @@ -74,24 +72,13 @@ public class GatewayRouter /// The forwarder. /// The resource cache. /// The service provider. - public GatewayRouter(GatewayRoutingSettings settings, IHttpForwarder forwarder, IResourceCache resourceCache, IServiceProvider serviceProvider) + public GatewayRouter(GatewayRoutingSettings settings, IHttpForwarder forwarder, IResourceCache resourceCache, AuthStrategyFactory authStrategyFactory, IServiceProvider serviceProvider) { _settings = settings; _forwarder = forwarder; _resourceCache = resourceCache; - - try - { - // Get the optional token service - _tokenService = serviceProvider.GetService(); - _tokenClient = serviceProvider.GetService(); - } - catch(InvalidOperationException) - { - // The services are not available we assume there is no client scope and work with client credentials - _tokenService = null; - _tokenClient = null; - } + _authStrategyFactory = authStrategyFactory; + _serviceProvider = serviceProvider; // Set up serializer options _serializerOptions = new JsonSerializerOptions(); @@ -114,47 +101,23 @@ public GatewayRouter(GatewayRoutingSettings settings, IHttpForwarder forwarder, _forwarderTransformer = new GatewayForwarderHttpTransformer(); } - private async Task SetTokenToRequest(HttpRequestMessage request) - { - string accessToken; - if (_tokenService != null) - { - // A user session exists, get token from token service - accessToken = await _tokenService.GetAccessTokenAsync(); - } - else if (_tokenClient != null) - { - // Fall back to client credentials token directly - ClientCredentialsTokenResponse? tokenResponse = await _tokenClient.GetTokenViaClientCredentialsAsync(); - if (tokenResponse == null) throw new InvalidOperationException("Could not retrieve token via client credentials."); - accessToken = tokenResponse.AccessToken; - } - else - { - throw new InvalidOperationException($"If you want to send access tokens, gateway authentication must be configured."); - } - - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - } - /// /// Sends a request and deserializes the response into a given type. /// /// The type of the resource. /// The request to send. - /// If true, the request will be enriched with an access token. + /// The name of the route to use. /// The result deserialized into the specified resource type. - private async Task SendAsync(HttpRequestMessage request, bool sendAccessToken) where TResource : class + private async Task SendAsync(HttpRequestMessage request, string routeName) where TResource : class { if (_settings.ResourceProxy != null) { request.Headers.Add("X-Forwarded-Host", _settings.ResourceProxy); } - if(sendAccessToken) - { - await SetTokenToRequest(request); - } + // Set authentication to request + IAuthStrategy authStrategy = _authStrategyFactory.GetAuthStrategy(routeName); + await authStrategy.SetAuthenticationAsync(_serviceProvider, request); // Get data from microservice HttpResponseMessage responseMessage = await _httpClient.SendAsync(request); @@ -175,18 +138,17 @@ private async Task SetTokenToRequest(HttpRequestMessage request) /// Sends a request. /// /// The request to send. - /// If true, the request will be enriched with an access token. - private async Task SendAsync(HttpRequestMessage request, bool sendAccessToken) + /// The name of the route to use. + private async Task SendAsync(HttpRequestMessage request, string routeName) { if (_settings.ResourceProxy != null) { request.Headers.Add("X-Forwarded-Host", _settings.ResourceProxy); } - if (sendAccessToken) - { - await SetTokenToRequest(request); - } + // Set authentication to request + IAuthStrategy authStrategy = _authStrategyFactory.GetAuthStrategy(routeName); + await authStrategy.SetAuthenticationAsync(_serviceProvider, request); // Get data from microservice HttpResponseMessage responseMessage = await _httpClient.SendAsync(request); @@ -198,9 +160,9 @@ private async Task SendAsync(HttpRequestMessage request, bool sendAccessToken) /// /// The type of the resource. /// The uri of the data to get. - /// If true, the request will be enriched with an access token. + /// The name of the route to use. /// The result deserialized into the specified resource type. - private async Task GetAsync(Uri requestUri, bool sendAccessToken) where TResource : class + private async Task GetAsync(Uri requestUri, string routeName) where TResource : class { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -209,7 +171,7 @@ private async Task GetAsync(Uri requestUri, bool sendAcces Method = HttpMethod.Get, }; - var result = await SendAsync(request, sendAccessToken); + var result = await SendAsync(request, routeName); if(result == null) throw new ApplicationException("No Content was provided by the server"); @@ -220,13 +182,13 @@ private async Task GetAsync(Uri requestUri, bool sendAcces /// Get data from a microservice specified by its key of a provided route and deserializes it into a given type. /// /// The type of the resource. - /// The key of the route to use. + /// The name of the route to use. /// The relative url to the endpoint. /// The result deserialized into the specified resource type. - public Task GetAsync(string routeKey, string relativeUrl) where TResource : class + public Task GetAsync(string routeName, string relativeUrl) where TResource : class { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return GetAsync(requestUri, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return GetAsync(requestUri, routeName); } /// @@ -234,8 +196,8 @@ public Task GetAsync(string routeKey, string relativeUrl) /// /// The uri to send to. /// The content to send - will be serialized as json. - /// If true, the request will be enriched with an access token. - private Task PutAsync(Uri requestUri, object content, bool sendAccessToken) + /// The name of the route to use. + private Task PutAsync(Uri requestUri, object content, string routeName) { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -245,7 +207,7 @@ private Task PutAsync(Uri requestUri, object content, bool sendAccessToken) Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json") }; - return SendAsync(request, sendAccessToken); + return SendAsync(request, routeName); } /// @@ -257,7 +219,7 @@ private Task PutAsync(Uri requestUri, object content, bool sendAccessToken) public Task PutAsync(string routeKey, string relativeUrl, object content) { Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return PutAsync(requestUri, content, _settings.Routes[routeKey].EnforceAuthentication); + return PutAsync(requestUri, content, routeKey); } /// @@ -266,9 +228,9 @@ public Task PutAsync(string routeKey, string relativeUrl, object content) /// The type of the resource. /// The uri to send to. /// The content to send - will be serialized as json. - /// If true, the request will be enriched with an access token. + /// The name of the route to use. /// The result deserialized into the specified resource type. - private Task PutAsync(Uri requestUri, object content, bool sendAccessToken) where TResource: class + private Task PutAsync(Uri requestUri, object content, string routeName) where TResource: class { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -278,21 +240,21 @@ public Task PutAsync(string routeKey, string relativeUrl, object content) Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json") }; - return SendAsync(request, sendAccessToken); + return SendAsync(request, routeName); } /// /// Puts data to a specific uri. /// /// The type of the resource. - /// The key of the route to use. + /// The name of the route to use. /// The relative url to the endpoint. /// The content to send - will be serialized as json. /// The result deserialized into the specified resource type. - public Task PutAsync(string routeKey, string relativeUrl, object content) where TResource: class + public Task PutAsync(string routeName, string relativeUrl, object content) where TResource: class { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return PutAsync(requestUri, content, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return PutAsync(requestUri, content, routeName); } /// @@ -300,8 +262,8 @@ public Task PutAsync(string routeKey, string relativeUrl, object content) /// /// The uri to send to. /// The content to send - will be serialized as json. - /// If true, the request will be enriched with an access token. - private Task PostAsync(Uri requestUri, object content, bool sendAccessToken) + /// The name of the route to use. + private Task PostAsync(Uri requestUri, object content, string routeName) { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -311,30 +273,30 @@ private Task PostAsync(Uri requestUri, object content, bool sendAccessToken) Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json") }; - return SendAsync(request, sendAccessToken); + return SendAsync(request, routeName); } - + /// /// Post data to a specific uri. /// - /// The key of the route to use. + /// The name of the route to use. /// The relative url to the endpoint. /// The content to send - will be serialized as json. - public Task PostAsync(string routeKey, string relativeUrl, object content) + public Task PostAsync(string routeName, string relativeUrl, object content) { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return PostAsync(requestUri, content, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return PostAsync(requestUri, content, routeName); } /// - /// Post data to a specific uri and return the result deserialized into the specified resource type.. + /// Post data to a specific uri and return the result deserialized into the specified resource type. /// /// The type of the resource. /// The uri to send to. /// The content to send - will be serialized as json. - /// If true, the request will be enriched with an access token. + /// The name of the route to use. /// The result deserialized into the specified resource type. - private Task PostAsync(Uri requestUri, object content, bool sendAccessToken) where TResource : class + private Task PostAsync(Uri requestUri, object content, string routeName) where TResource : class { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -344,29 +306,29 @@ public Task PostAsync(string routeKey, string relativeUrl, object content) Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json") }; - return SendAsync(request, sendAccessToken); + return SendAsync(request, routeName); } /// - /// Post data to a specific uri and return the result deserialized into the specified resource type.. + /// Post data to a specific uri and return the result deserialized into the specified resource type. /// /// The type of the resource. - /// The key of the route to use. + /// The name of the route to use. /// The relative url to the endpoint. /// The content to send - will be serialized as json. /// The result deserialized into the specified resource type. - public Task PostAsync(string routeKey, string relativeUrl, object content) where TResource : class + public Task PostAsync(string routeName, string relativeUrl, object content) where TResource : class { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return PostAsync(requestUri, content, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return PostAsync(requestUri, content, routeName); } /// /// Delete data from a specific URI /// /// The uri to send to. - /// If true, the request will be enriched with an access token. - private Task DeleteAsync(Uri requestUri, bool sendAccessToken) + /// The name of the route to use. + private Task DeleteAsync(Uri requestUri, string routeName) { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -375,18 +337,18 @@ private Task DeleteAsync(Uri requestUri, bool sendAccessToken) Method = HttpMethod.Delete, }; - return SendAsync(request, sendAccessToken); + return SendAsync(request, routeName); } /// /// Delete data from a specific URI /// - /// The key of the route to use. + /// The name of the route to use. /// The relative url to the endpoint. - public Task DeleteAsync(string routeKey, string relativeUrl) + public Task DeleteAsync(string routeName, string relativeUrl) { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return DeleteAsync(requestUri, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return DeleteAsync(requestUri, routeName); } /// @@ -394,9 +356,9 @@ public Task DeleteAsync(string routeKey, string relativeUrl) /// /// The type of the resource. /// The uri to send to. - /// If true, the request will be enriched with an access token. + /// The name of the route to use. /// The result deserialized into the specified resource type. - private Task DeleteAsync(Uri requestUri, bool sendAccessToken) where TResource : class + private Task DeleteAsync(Uri requestUri, string routeName) where TResource : class { // Set up request HttpRequestMessage request = new HttpRequestMessage() @@ -405,20 +367,20 @@ public Task DeleteAsync(string routeKey, string relativeUrl) Method = HttpMethod.Delete, }; - return SendAsync(request, sendAccessToken); + return SendAsync(request, routeName); } /// /// Delete data from a specific URI and return the result deserialized into the specified resource type. /// /// The type of the resource. - /// The key of the route to use. + /// The name of the route to use. /// The relative url to the endpoint. /// The result deserialized into the specified resource type. - public Task DeleteAsync(string routeKey, string relativeUrl) where TResource : class + public Task DeleteAsync(string routeName, string relativeUrl) where TResource : class { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return DeleteAsync(requestUri, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return DeleteAsync(requestUri, routeName); } /// @@ -428,10 +390,11 @@ public Task DeleteAsync(string routeKey, string relativeUrl) /// The type of the resource. /// The uri of the data to get. /// The maximum age of the resource which is acceptable. + /// The name of the route to use. /// /// The result deserialized into the specified resource type. /// - public async Task GetCachedAsync(Uri requestUri, TimeSpan maxResourceAge, bool sendAccessToken) where TResource : class + public async Task GetCachedAsync(Uri requestUri, TimeSpan maxResourceAge, string routeName) where TResource : class { string cacheKey = requestUri.ToString(); @@ -444,7 +407,7 @@ public async Task GetCachedAsync(Uri requestUri, TimeSpan else { // Get resource from origin and write it to the cache - data = await GetAsync(requestUri, sendAccessToken); + data = await GetAsync(requestUri, routeName); _resourceCache.Write(cacheKey, data); return data; } @@ -456,25 +419,25 @@ public async Task GetCachedAsync(Uri requestUri, TimeSpan /// data is retrieved from the origin and written to the cache. /// /// The type of the resource. - /// The key of the route url to use. + /// The name of the route to use. /// The relative url to the endpoint. /// The maximum age of the resource which is acceptable. /// /// The result deserialized into the specified resource type. /// - public Task GetCachedAsync(string routeKey, string relativeUrl, TimeSpan maxResourceAge) where TResource : class + public Task GetCachedAsync(string routeName, string relativeUrl, TimeSpan maxResourceAge) where TResource : class { - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); - return GetCachedAsync(requestUri, maxResourceAge, _settings.Routes[routeKey].EnforceAuthentication); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); + return GetCachedAsync(requestUri, maxResourceAge, routeName); } /// /// Sends the current request to a microservice. /// - /// The key to the uri of the microservcie to send the request to. + /// The name of the route to use. /// The relative url to the endpoint. /// The response of the call to the microservice as IActionResult - public async Task ProxyAsync(HttpContext httpContext, string routeKey, string relativeUrl) + public async Task ProxyAsync(HttpContext httpContext, string routeName, string relativeUrl) { HttpRequestMessage proxyRequest = new HttpRequestMessage(); @@ -489,7 +452,7 @@ public async Task ProxyAsync(HttpContext httpContext, string rout proxyRequest.Method = new HttpMethod(httpContext.Request.Method); - Uri requestUri = CombineUris(GetBaseUrl(routeKey), relativeUrl); + Uri requestUri = CombineUris(GetBaseUrl(routeName), relativeUrl); if (_settings.ResourceProxy != null) { @@ -500,10 +463,9 @@ public async Task ProxyAsync(HttpContext httpContext, string rout proxyRequest.Headers.Host = requestUri.Authority; proxyRequest.RequestUri = requestUri; - if (_settings.Routes[routeKey].EnforceAuthentication) - { - await SetTokenToRequest(proxyRequest); - } + // Set authentication to request + IAuthStrategy authStrategy = _authStrategyFactory.GetAuthStrategy(routeName); + await authStrategy.SetAuthenticationAsync(httpContext.RequestServices, proxyRequest); HttpResponseMessage proxyResponse = await _httpClient.SendAsync(proxyRequest); @@ -552,19 +514,19 @@ public Task ProxyAsync(HttpContext httpContext, string routeKey) /// Forwards an http context asynchronous. /// /// The HTTP context. - /// The route key. + /// The route name to use. /// The relativurl. - public async Task ForwardAsync(HttpContext httpContext, string routeKey, string relativurl) + public async Task ForwardAsync(HttpContext httpContext, string routeName, string relativurl) { - string baseUrl = GetBaseUrl(routeKey); + string baseUrl = GetBaseUrl(routeName); Uri targetUrl = CombineUris(baseUrl, relativurl); - httpContext.Items[GatewayForwarderHttpTransformer.SendAccessTokenItemKey] = _settings.Routes[routeKey].EnforceAuthentication; + httpContext.Items[GatewayForwarderHttpTransformer.RouteNameItemKey] = routeName; httpContext.Items[GatewayForwarderHttpTransformer.TargetUrlItemKey] = targetUrl.AbsoluteUri; // Forward request to microservice - ForwarderError error = await _forwarder.SendAsync(httpContext, - routeKey, + ForwarderError error = await _forwarder.SendAsync(httpContext, + routeName, _forwarderMessageInvoker, _forwarderRequestConfig, _forwarderTransformer); diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs index 028c72d..5660967 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs @@ -1,7 +1,6 @@ -using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Routing.Auth; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using System.ComponentModel.DataAnnotations; using Yarp.ReverseProxy.Configuration; namespace Fancy.ResourceLinker.Gateway.Routing; @@ -17,6 +16,17 @@ internal static class GatewayRouting /// The services. internal static void AddGatewayRouting(IServiceCollection services, GatewayRoutingSettings settings) { + // Set up a factory for auth strategy factory + services.AddSingleton(serviceProvider => + { + var factory = new AuthStrategyFactory(serviceProvider.GetRequiredService()); + factory.AddAuthStrategy(NoAuthenticationAuthStrategy.NAME, typeof(NoAuthenticationAuthStrategy)); + factory.AddAuthStrategy(TokenPassThroughAuthStrategy.NAME, typeof(TokenPassThroughAuthStrategy)); + factory.AddAuthStrategy(AzureOnBehalfOfAuthStrategy.NAME, typeof(AzureOnBehalfOfAuthStrategy)); + return factory; + }); + + // Register all other needed services services.AddHttpForwarder(); services.AddSingleton(settings); services.AddTransient(); @@ -56,9 +66,8 @@ private static void AddGatewayRoutes(this IReverseProxyBuilder reverseProxyBuild { RouteId = routeSettings.Key, ClusterId = routeSettings.Key, - AuthorizationPolicy = routeSettings.Value.EnforceAuthentication ? GatewayAuthentication.AuthenticationPolicyName : null, Match = new RouteMatch { Path = routeSettings.Value.PathMatch }, - Metadata = new Dictionary { { "EnforceAuthentication", routeSettings.Value.EnforceAuthentication ? "True" : "False" } } + Metadata = new Dictionary { { "RouteName", routeSettings.Key } } }); clusters.Add(new ClusterConfig diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRoutingSettings.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRoutingSettings.cs index a799565..85db7d4 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRoutingSettings.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRoutingSettings.cs @@ -1,4 +1,6 @@ -namespace Fancy.ResourceLinker.Gateway.Routing; +using Fancy.ResourceLinker.Gateway.Routing.Auth; + +namespace Fancy.ResourceLinker.Gateway.Routing; /// /// A class to hold all settings required to configure the gateway routing feature. @@ -60,12 +62,34 @@ public class RouteSettings public string? PathMatch { get; set; } /// - /// Gets or sets a value indicating whether the authentication shall be enforced by the gateway. + /// Gets or sets the authentication settings. + /// + /// + /// The authentication settings. + /// + public RouteAuthenticationSettings Authentication { get; set; } = new RouteAuthenticationSettings(); +} + +/// +/// A class to hold all setting required for authenticating to a backend. +/// +public class RouteAuthenticationSettings +{ + /// + /// Gets or sets the strategy. + /// + /// + /// The strategy. + /// + public string Strategy { get; set; } = "NoAuthentication"; + + /// + /// Gets or sets the options. /// /// - /// true if the authentication shall be enforced; otherwise, false. + /// The options. /// - public bool EnforceAuthentication { get; set; } + public IDictionary Options { get; set; } = new Dictionary(); } From bb0f293d4b26bd1792a0748764e81b9802c21610 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Thu, 28 Mar 2024 20:28:40 +0100 Subject: [PATCH 02/16] Refactoring of routing authentication subsystem --- ...eLinker.Gateway.EntityFrameworkCore.csproj | 2 +- .../Fancy.ResourceLinker.Gateway.csproj | 2 +- .../Routing/Auth/AuthStrategyFactory.cs | 68 ----- .../Auth/AzureOnBehalfOfAuthStrategy.cs | 223 ++++++++++---- .../Auth/ClientCredentialOnlyAuthStrategy.cs | 285 ++++++++++++++++++ ...egy.cs => IRouteAuthenticationStrategy.cs} | 15 +- .../Auth/NoAuthenticationAuthStrategy.cs | 11 +- .../Auth/RouteAuthenticationManager.cs | 93 ++++++ .../Auth/TokenPassThroughAuthStrategy.cs | 21 +- .../GatewayForwarderHttpTransformer.cs | 4 +- .../Routing/GatewayPipeline.cs | 4 +- .../Routing/GatewayRouter.cs | 19 +- .../Routing/GatewayRouting.cs | 17 +- .../ServiceCollectionExtensions.cs | 8 +- 14 files changed, 614 insertions(+), 158 deletions(-) delete mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs rename src/Fancy.ResourceLinker.Gateway/Routing/Auth/{IAuthStrategy.cs => IRouteAuthenticationStrategy.cs} (57%) create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/RouteAuthenticationManager.cs diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj index 591adef..a316e4c 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ 0.0.7 - net6.0 + net8.0 enable enable diff --git a/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj b/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj index 9c9216c..f068d91 100644 --- a/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj +++ b/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj @@ -2,7 +2,7 @@ 0.0.7 - net6.0 + net8.0 enable enable diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs deleted file mode 100644 index aa9d098..0000000 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AuthStrategyFactory.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Fancy.ResourceLinker.Gateway.Routing.Auth; - -/// -/// A class to create instances of auth strategies for specific routes. -/// -public class AuthStrategyFactory -{ - /// - /// The authentication strategies. - /// - private Dictionary _authStrategies = new Dictionary(); - - /// - /// The authentication strategy instances. - /// - private Dictionary _authStrategyInstances = new Dictionary(); - - /// - /// The routing settings. - /// - private readonly GatewayRoutingSettings _routingSettings; - - /// - /// Initializes a new instance of the class. - /// - /// The routing settings. - public AuthStrategyFactory(GatewayRoutingSettings routingSettings) - { - _routingSettings = routingSettings; - } - - /// - /// Gets the authentication strategy. - /// - /// The name. - /// An instance of the auth strategy. - public IAuthStrategy GetAuthStrategy(string route) - { - if(! _authStrategyInstances.ContainsKey(route)) - { - string strategyName = _routingSettings.Routes[route].Authentication.Strategy; - - Type authStrategyType = _authStrategies[strategyName]; - - // Create a new instance of the auth strategy - IAuthStrategy? instance = Activator.CreateInstance(authStrategyType, _routingSettings.Routes[route].Authentication) as IAuthStrategy; - - if (instance == null) throw new InvalidOperationException("Auth strategy could not be instantiated"); - - _authStrategyInstances[route] = instance; - } - - return _authStrategyInstances[route]; - } - - public void AddAuthStrategy(string name, Type authStrategyType) - { - _authStrategies.Add(name, authStrategyType); - } - - /// - /// Clears the authentication strategies. - /// - public void ClearAuthStrategies() - { - _authStrategies.Clear(); - } -} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs index 8b5aa0f..c8737b9 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs @@ -2,23 +2,56 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text.Json.Serialization; namespace Fancy.ResourceLinker.Gateway.Routing.Auth; - -class TokenExchangeResponse +class OnBehalfOfTokenResponse { - public string access_token { get; set; } = ""; - public string refresh_token { get; set; } = ""; - public long expires_in { get; set; } + /// + /// Gets or sets the access token. + /// + /// + /// The access token. + /// + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = ""; + + /// + /// Gets or sets the refresh token. + /// + /// + /// The refresh token. + /// + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } = ""; + + /// + /// Gets or sets the expires in. + /// + /// + /// The expires in. + /// + [JsonPropertyName("expires_in")] + public long ExpiresIn { get; set; } + + /// + /// Gets or sets the expires at. + /// + /// + /// The expires at. + /// + [JsonIgnore] + public DateTime ExpiresAt { get; set; } } /// /// An auth strategy which just passes through the current token /// -internal class AzureOnBehalfOfAuthStrategy : IAuthStrategy +internal class AzureOnBehalfOfAuthStrategy : IRouteAuthenticationStrategy { /// /// The name of the auth strategy. @@ -30,18 +63,45 @@ internal class AzureOnBehalfOfAuthStrategy : IAuthStrategy /// private readonly HttpClient _httpClient = new HttpClient(); + /// + /// The discovery document service. + /// + private readonly DiscoveryDocumentService _discoveryDocumentService; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The gateway authentication settings. + /// + private GatewayAuthenticationSettings? _gatewayAuthenticationSettings; + /// /// The route authentication settings. /// - private readonly RouteAuthenticationSettings _routeAuthenticationSettings; + private RouteAuthenticationSettings? _routeAuthenticationSettings; + + /// + /// The discovery document. + /// + private DiscoveryDocument? _discoveryDocument; + + /// + /// The current token response. + /// + private OnBehalfOfTokenResponse? _currentTokenResponse; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The token service. - public AzureOnBehalfOfAuthStrategy(RouteAuthenticationSettings routeAuthenticationSettings) + /// The discovery document service. + /// The logger. + public AzureOnBehalfOfAuthStrategy(DiscoveryDocumentService discoveryDocumentService, ILogger logger) { - _routeAuthenticationSettings = routeAuthenticationSettings; + _discoveryDocumentService = discoveryDocumentService; + _logger = logger; } /// @@ -52,6 +112,29 @@ public AzureOnBehalfOfAuthStrategy(RouteAuthenticationSettings routeAuthenticati /// public string Name => NAME; + /// + /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. + /// + /// The gateway authentication settigns. + /// The route authentication settigns. + public async Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) + { + _gatewayAuthenticationSettings = gatewayAuthenticationSettings; + + if(_gatewayAuthenticationSettings == null) + { + throw new InvalidOperationException($"The {NAME} route authentication strategy needs to have the gateway authenticaion configured"); + } + + if(string.IsNullOrEmpty(_gatewayAuthenticationSettings.ClientId) || string.IsNullOrEmpty(_gatewayAuthenticationSettings.ClientSecret)) + { + throw new InvalidOperationException($"The {NAME} route authentication strategy needs to have set the 'ClientId' and 'Client Secret' settings at the gateway authentication settings."); + } + + _routeAuthenticationSettings = routeAuthenticationSettings; + _discoveryDocument = await _discoveryDocumentService.LoadDiscoveryDocumentAsync(_gatewayAuthenticationSettings.Authority); + } + /// /// Sets the authentication to an http context asynchronous. /// @@ -61,13 +144,8 @@ public AzureOnBehalfOfAuthStrategy(RouteAuthenticationSettings routeAuthenticati /// public async Task SetAuthenticationAsync(HttpContext context) { - TokenService? tokenService = TryGetTokenService(context.RequestServices); - - if (tokenService != null) - { - string? accessToken = await tokenService.GetAccessTokenAsync(); - context.Request.Headers.Add("Authorization", "Bearer " + accessToken); - } + string accessToken = await GetAccessTokenAsync(context.RequestServices); + context.Request.Headers.Authorization = new StringValues("Bearer " + accessToken); } /// @@ -80,54 +158,79 @@ public async Task SetAuthenticationAsync(HttpContext context) /// public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) { - string? accessToken = await TryGetOnBehalfOfTokenAsync(serviceProvider); + string accessToken = await GetAccessTokenAsync(serviceProvider); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + } - if (accessToken != null) + /// + /// Gets the access token asynchronous. + /// + /// The access token. + private async Task GetAccessTokenAsync(IServiceProvider serviceProvider) + { + if (_currentTokenResponse == null || IsExpired(_currentTokenResponse)) { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + TokenService? tokenService = TryGetTokenService(serviceProvider); + + if (tokenService == null) + { + throw new InvalidOperationException("A token service in a scope is needed to use the azure on behalf of flow"); + } + + _currentTokenResponse = await GetOnBehalfOfTokenAsync(tokenService); } + + return _currentTokenResponse.AccessToken; } - private async Task TryGetOnBehalfOfTokenAsync(IServiceProvider serviceProvider) + /// + /// Gets a token via an on behalf of flow asynchronous. + /// + /// The service provider. + /// The exchanged token. + private async Task GetOnBehalfOfTokenAsync(TokenService tokenService) { - DiscoveryDocumentService discoveryDocumentService = serviceProvider.GetRequiredService(); - GatewayAuthenticationSettings authSettings = serviceProvider.GetRequiredService(); - var disoveryDocument = await discoveryDocumentService.LoadDiscoveryDocumentAsync(authSettings.Authority); + if(_gatewayAuthenticationSettings == null || _routeAuthenticationSettings == null) + { + throw new InvalidOperationException("Initialize the auth strategy first before using it"); + } - TokenService? tokenService = TryGetTokenService(serviceProvider); - string? accessToken = await tokenService?.GetAccessTokenAsync(); + string accessToken = await tokenService.GetAccessTokenAsync(); - if (accessToken != null) + var payload = new Dictionary { - var payload = new Dictionary - { - { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }, - { "client_id", _routeAuthenticationSettings.Options["ClientId"] }, - { "client_secret", _routeAuthenticationSettings.Options["ClientSecret"] }, - { "assertion", accessToken }, - { "scope", _routeAuthenticationSettings.Options["Scope"] }, - { "requested_token_use", "on_behalf_of" }, - }; - - FormUrlEncodedContent content = new FormUrlEncodedContent(payload); - HttpResponseMessage httpResponse = await _httpClient.PostAsync(disoveryDocument.TokenEndpoint, content); + { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }, + { "client_id", _gatewayAuthenticationSettings.ClientId }, + { "client_secret", _gatewayAuthenticationSettings.ClientSecret }, + { "assertion", accessToken }, + { "scope", _routeAuthenticationSettings.Options["Scope"] }, + { "requested_token_use", "on_behalf_of" }, + }; + + FormUrlEncodedContent content = new FormUrlEncodedContent(payload); + HttpResponseMessage httpResponse = await _httpClient.PostAsync(_discoveryDocument?.TokenEndpoint, content); - try - { - httpResponse.EnsureSuccessStatusCode(); - } - catch(HttpRequestException) - { - ILogger logger = serviceProvider.GetRequiredService>(); - string errorResponse = new StreamReader(httpResponse.Content.ReadAsStream()).ReadToEnd(); - logger.LogError("Error on on behalf of token exchange. Response from server " + errorResponse); - throw; - } + try + { + httpResponse.EnsureSuccessStatusCode(); + } + catch(HttpRequestException) + { + string errorResponse = new StreamReader(httpResponse.Content.ReadAsStream()).ReadToEnd(); + _logger.LogError("Error on on behalf of token exchange. Response from server " + errorResponse); + throw; + } - TokenExchangeResponse? response = await httpResponse.Content.ReadFromJsonAsync(); - return response?.access_token; + OnBehalfOfTokenResponse? result = await httpResponse.Content.ReadFromJsonAsync(); + + if(result == null) + { + throw new InvalidOperationException("Could not deserialize token response from azure on behalf of flow"); } - return null; + + result.ExpiresAt = DateTime.UtcNow.AddSeconds(Convert.ToInt32(result.ExpiresIn)); + + return result; } /// @@ -147,5 +250,15 @@ public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpR return null; } } -} + /// Determines whether the specified token response is expired. + /// + /// The response. + /// + /// true if the specified response is expired; otherwise, false. + /// + private bool IsExpired(OnBehalfOfTokenResponse response) + { + return response.ExpiresAt.Subtract(DateTime.UtcNow).TotalSeconds < 30; + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs new file mode 100644 index 0000000..ed6192c --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs @@ -0,0 +1,285 @@ +using Fancy.ResourceLinker.Gateway.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json.Serialization; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// Class to hold the result of a client credentials token response. +/// +class ClientCredentialsTokenResponse +{ + /// + /// Gets or sets the access token. + /// + /// + /// The access token. + /// + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = ""; + + /// + /// Gets or sets the expires in. + /// + /// + /// The expires in. + /// + [JsonPropertyName("expires_in")] + public long ExpiresIn { get; set; } + + /// + /// Gets or sets the expires at. + /// + /// + /// The expires at. + /// + [JsonIgnore] + public DateTime ExpiresAt { get; set; } +} + +/// +/// A route authentication strategy running the OAuth client credential flow only. +/// +internal class ClientCredentialOnlyAuthStrategy : IRouteAuthenticationStrategy +{ + /// + /// The name of the auth strategy. + /// + public const string NAME = "AzureOnBehalfOf"; + + /// + /// The discovery document service. + /// + private readonly DiscoveryDocumentService _discoveryDocumentService; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The discovery document. + /// + private DiscoveryDocument? _discoveryDocument; + + /// + /// The client identifier. + /// + private string _clientId = string.Empty; + + /// + /// The client secret. + /// + private string _clientSecret = string.Empty; + + /// + /// The scope. + /// + private string _scope = string.Empty; + + /// + /// The is initialized. + /// + private bool _isInitialized = false; + + /// + /// The current token response. + /// + private ClientCredentialsTokenResponse? _currentTokenResponse; + + /// + /// Initializes a new instance of the class. + /// + /// The discovery document service. + /// The logger. + public ClientCredentialOnlyAuthStrategy(DiscoveryDocumentService discoveryDocumentService, ILogger logger) + { + _discoveryDocumentService = discoveryDocumentService; + _logger = logger; + } + + /// + /// Gets the name of the strategy. + /// + /// + /// The name. + /// + public string Name => NAME; + + /// + /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. + /// + /// The gateway authentication settigns. + /// The route authentication settigns. + public async Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) + { + string authority; + + if(routeAuthenticationSettings.Options.ContainsKey("Authority")) + { + authority = routeAuthenticationSettings.Options["Authority"]; + } + else if(!string.IsNullOrEmpty(gatewayAuthenticationSettings?.Authority)) + { + authority = gatewayAuthenticationSettings.Authority; + } + else + { + throw new InvalidOperationException("Either the route authentication settings or the gateway authentication settings need to have an 'Authority' set"); + } + + if (routeAuthenticationSettings.Options.ContainsKey("ClientId")) + { + _clientId = routeAuthenticationSettings.Options["Client"]; + } + else if (!string.IsNullOrEmpty(gatewayAuthenticationSettings?.ClientId)) + { + _clientId = gatewayAuthenticationSettings.ClientId; + } + else + { + throw new InvalidOperationException("Either the route authentication settings or the gateway authentication settings need to have a 'ClientId' set"); + } + + if (routeAuthenticationSettings.Options.ContainsKey("ClientSecret")) + { + _clientSecret = routeAuthenticationSettings.Options["ClientSecret"]; + } + else if (!string.IsNullOrEmpty(gatewayAuthenticationSettings?.ClientSecret)) + { + _clientSecret = gatewayAuthenticationSettings.ClientSecret; + } + else + { + throw new InvalidOperationException("Either the route authentication settings or the gateway authentication settings need to have a 'ClientId' set"); + } + + if (routeAuthenticationSettings.Options.ContainsKey("Scope")) + { + _scope = routeAuthenticationSettings.Options["Scope"]; + } + else + { + throw new InvalidOperationException("The scope needs to be set at the route authentication settings"); + } + + _discoveryDocument = await _discoveryDocumentService.LoadDiscoveryDocumentAsync(authority); + + _isInitialized = true; + } + + /// + /// Sets the authentication to an http context asynchronous. + /// + /// The http context. + /// + /// A task indicating the completion of the asynchronous operation + /// + public async Task SetAuthenticationAsync(HttpContext context) + { + if(!_isInitialized) + { + throw new InvalidOperationException("Call initialize first before using this instance to set authorization headers"); + } + + string accessToken = await GetAccessTokenAsync(); + context.Request.Headers.Authorization = new StringValues("Bearer " + accessToken); + } + + /// + /// Sets the authentication to an http request message asynchronous. + /// + /// The current service provider. + /// The http request message. + /// + /// A task indicating the completion of the asynchronous operation + /// + public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) + { + if(!_isInitialized) + { + throw new InvalidOperationException("Call initialize first before using this instance to set authorization headers"); + } + + string accessToken = await GetAccessTokenAsync(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + } + + /// + /// Gets the access token asynchronous. + /// + /// The access token. + private async Task GetAccessTokenAsync() + { + if(_currentTokenResponse == null || IsExpired(_currentTokenResponse)) + { + _currentTokenResponse = await GetTokenViaClientCredentialsAsync(); + } + + return _currentTokenResponse.AccessToken; + } + + /// + /// Gets the token via client credentials asynchronous. + /// + /// + private async Task GetTokenViaClientCredentialsAsync() + { + var payload = new Dictionary + { + { "grant_type", "client_credentials" }, + { "client_id", _clientId }, + { "client_secret", _clientSecret }, + { "scope", _scope } + }; + + HttpClient httpClient = new HttpClient(); + + HttpRequestMessage request = new HttpRequestMessage + { + RequestUri = new Uri(_discoveryDocument!.TokenEndpoint), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(payload) + }; + + HttpResponseMessage response = await httpClient.SendAsync(request); + + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException) + { + string errorResponse = new StreamReader(response.Content.ReadAsStream()).ReadToEnd(); + _logger.LogError("Error on client credential flow token request. Response from server " + errorResponse); + throw; + } + + ClientCredentialsTokenResponse? result = await response.Content.ReadFromJsonAsync(); + + if(result == null) + { + throw new InvalidOperationException("Could not deserialize client credential token response"); + } + + result.ExpiresAt = DateTime.UtcNow.AddSeconds(Convert.ToInt32(result.ExpiresIn)); + + return result; + } + + /// + /// Determines whether the specified token response is expired. + /// + /// The response. + /// + /// true if the specified response is expired; otherwise, false. + /// + private bool IsExpired(ClientCredentialsTokenResponse response) + { + return response.ExpiresAt.Subtract(DateTime.UtcNow).TotalSeconds < 30; + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IRouteAuthenticationStrategy.cs similarity index 57% rename from src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs rename to src/Fancy.ResourceLinker.Gateway/Routing/Auth/IRouteAuthenticationStrategy.cs index 7aa9ad5..6510dbf 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/IRouteAuthenticationStrategy.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Http; +using Fancy.ResourceLinker.Gateway.Authentication; +using Microsoft.AspNetCore.Http; namespace Fancy.ResourceLinker.Gateway.Routing.Auth; /// /// Interface for an authorization strategy. /// -public interface IAuthStrategy +public interface IRouteAuthenticationStrategy { /// /// Gets the name of the strategy. @@ -15,6 +16,16 @@ public interface IAuthStrategy /// string Name { get; } + /// + /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. + /// + /// The gateway authentication settigns. + /// The route authentication settigns. + /// + /// A task indicating the completion of the asynchronous operation. + /// + Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings); + /// /// Sets the authentication to an http context asynchronous. /// diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs index 8e886f4..37e1e18 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Http; +using Fancy.ResourceLinker.Gateway.Authentication; +using Microsoft.AspNetCore.Http; namespace Fancy.ResourceLinker.Gateway.Routing.Auth; /// /// An authentication strategy which does not set any authentication. /// -public class NoAuthenticationAuthStrategy : IAuthStrategy +public class NoAuthenticationAuthStrategy : IRouteAuthenticationStrategy { /// /// The name of the auth strategy @@ -29,6 +30,12 @@ public NoAuthenticationAuthStrategy(RouteAuthenticationSettings authenticationSe /// public string Name => NAME; + public Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) + { + return Task.CompletedTask; + } + + /// /// Sets the authentication to an http context asynchronous. /// diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/RouteAuthenticationManager.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/RouteAuthenticationManager.cs new file mode 100644 index 0000000..f6c79c8 --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/RouteAuthenticationManager.cs @@ -0,0 +1,93 @@ +using Fancy.ResourceLinker.Gateway.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// A class to create an hold instances of route authentication strategies for specific routes. +/// +public class RouteAuthenticationManager +{ + /// + /// The route authentication strategy instances. + /// + private Dictionary _authStrategyInstances = new Dictionary(); + + /// + /// The routing settings. + /// + private readonly GatewayRoutingSettings _routingSettings; + + /// + /// The service provider. + /// + private readonly IServiceProvider _serviceProvider; + + /// + /// The gateway authentication settings if available. + /// + private readonly GatewayAuthenticationSettings? _gatewayAuthenticationSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The routing settings. + /// The service provider to use to get instances of auth strategies. + public RouteAuthenticationManager(GatewayRoutingSettings routingSettings, IServiceProvider serviceProvider) + { + _routingSettings = routingSettings; + _serviceProvider = serviceProvider; + _gatewayAuthenticationSettings = GetGatewayAuthenticationSettings(); + } + + /// + /// Gets an authentication strategy for a specific route asynchronous. + /// + /// The name. + /// An instance of the auth strategy. + public async Task GetAuthStrategyAsync(string route) + { + if(!_authStrategyInstances.ContainsKey(route)) + { + return await CreateAuthStrategyAsync(route); + } + + return _authStrategyInstances[route]; + } + + /// + /// Creates an authentication strategy asynchronous. + /// + /// The route. + /// + /// The authentication strategy. + /// + private async Task CreateAuthStrategyAsync(string route) + { + RouteAuthenticationSettings routeAuthSettings = _routingSettings.Routes[route].Authentication; + + IRouteAuthenticationStrategy authStrategy = _serviceProvider.GetRequiredKeyedService(routeAuthSettings.Strategy); + + await authStrategy.InitializeAsync(_gatewayAuthenticationSettings, routeAuthSettings); + + _authStrategyInstances[route] = authStrategy; + + return authStrategy; + } + + /// + /// Gets the gateway authentication settings. + /// + /// The gateway authentication settings or null if none are set. + private GatewayAuthenticationSettings? GetGatewayAuthenticationSettings() + { + try + { + return _serviceProvider.GetService(); + } + catch + { + return null; + } + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs index 3a42c29..5fde61d 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs @@ -1,15 +1,15 @@ using Fancy.ResourceLinker.Gateway.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using System.Net.Http.Headers; -using System.Net.Http.Json; namespace Fancy.ResourceLinker.Gateway.Routing.Auth; /// /// An auth strategy which just passes through the current token /// -internal class TokenPassThroughAuthStrategy : IAuthStrategy +internal class TokenPassThroughAuthStrategy : IRouteAuthenticationStrategy { /// /// The name of the auth strategy. @@ -38,6 +38,17 @@ public TokenPassThroughAuthStrategy(RouteAuthenticationSettings authenticationSe /// public string Name => NAME; + /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. + /// The gateway authentication settigns. + /// The route authentication settigns. + /// + /// A task indicating the completion of the asyncrhonous operation. + /// + public Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) + { + return Task.CompletedTask; + } + /// /// Sets the authentication to an http context asynchronous. /// @@ -51,8 +62,8 @@ public async Task SetAuthenticationAsync(HttpContext context) if (tokenService != null) { - string? accessToken = await tokenService.GetAccessTokenAsync(); - context.Request.Headers.Add("Authorization", "Bearer " + accessToken); + string accessToken = await tokenService.GetAccessTokenAsync(); + context.Request.Headers.Authorization = new StringValues("Bearer " + accessToken); } } @@ -70,7 +81,7 @@ public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpR if (tokenService != null) { - string? accessToken = await tokenService.GetAccessTokenAsync(); + string accessToken = await tokenService.GetAccessTokenAsync(); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } } diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs index a97caac..db94245 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs @@ -32,8 +32,8 @@ public override async ValueTask TransformRequestAsync(HttpContext httpContext, H if(!string.IsNullOrEmpty(routeName)) { // Add authentication - AuthStrategyFactory authStrategyFactory = httpContext.RequestServices.GetRequiredService(); - IAuthStrategy authStrategy = authStrategyFactory.GetAuthStrategy(routeName); + RouteAuthenticationManager routeAuthenticationManager = httpContext.RequestServices.GetRequiredService(); + IRouteAuthenticationStrategy authStrategy = await routeAuthenticationManager.GetAuthStrategyAsync(routeName); await authStrategy.SetAuthenticationAsync(httpContext.RequestServices, proxyRequest); } diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs index cac608d..aedb0d1 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs @@ -24,8 +24,8 @@ internal static void UseGatewayPipeline(this IReverseProxyApplicationBuilder pip && proxyFeature.Route.Config.Metadata.ContainsKey("RouteName")) { string routeName = proxyFeature.Route.Config.Metadata["RouteName"]; - AuthStrategyFactory authStrategyFactory = context.RequestServices.GetRequiredService(); - IAuthStrategy authStrategy = authStrategyFactory.GetAuthStrategy(routeName); + RouteAuthenticationManager authStrategyFactory = context.RequestServices.GetRequiredService(); + IRouteAuthenticationStrategy authStrategy = await authStrategyFactory.GetAuthStrategyAsync(routeName); await authStrategy.SetAuthenticationAsync(context); } await next().ConfigureAwait(false); diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs index 96e77e9..727ec7f 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouter.cs @@ -56,9 +56,9 @@ public class GatewayRouter private readonly IResourceCache _resourceCache; /// - /// The authentication strategy factory. + /// The route authentication manager. /// - private readonly AuthStrategyFactory _authStrategyFactory; + private readonly RouteAuthenticationManager _routeAuthManager; /// /// The service provider. @@ -66,18 +66,19 @@ public class GatewayRouter private readonly IServiceProvider _serviceProvider; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The settings. /// The forwarder. /// The resource cache. + /// The route authentication manager. /// The service provider. - public GatewayRouter(GatewayRoutingSettings settings, IHttpForwarder forwarder, IResourceCache resourceCache, AuthStrategyFactory authStrategyFactory, IServiceProvider serviceProvider) + public GatewayRouter(GatewayRoutingSettings settings, IHttpForwarder forwarder, IResourceCache resourceCache, RouteAuthenticationManager routeAuthManager, IServiceProvider serviceProvider) { _settings = settings; _forwarder = forwarder; _resourceCache = resourceCache; - _authStrategyFactory = authStrategyFactory; + _routeAuthManager = routeAuthManager; _serviceProvider = serviceProvider; // Set up serializer options @@ -116,7 +117,7 @@ public GatewayRouter(GatewayRoutingSettings settings, IHttpForwarder forwarder, } // Set authentication to request - IAuthStrategy authStrategy = _authStrategyFactory.GetAuthStrategy(routeName); + IRouteAuthenticationStrategy authStrategy = await _routeAuthManager.GetAuthStrategyAsync(routeName); await authStrategy.SetAuthenticationAsync(_serviceProvider, request); // Get data from microservice @@ -147,7 +148,7 @@ private async Task SendAsync(HttpRequestMessage request, string routeName) } // Set authentication to request - IAuthStrategy authStrategy = _authStrategyFactory.GetAuthStrategy(routeName); + IRouteAuthenticationStrategy authStrategy = await _routeAuthManager.GetAuthStrategyAsync(routeName); await authStrategy.SetAuthenticationAsync(_serviceProvider, request); // Get data from microservice @@ -446,7 +447,7 @@ public async Task ProxyAsync(HttpContext httpContext, string rout using (StreamReader reader = new StreamReader(httpContext.Request.Body)) { string content = await reader.ReadToEndAsync(); - proxyRequest.Content = new StringContent(content, Encoding.UTF8, httpContext.Request.ContentType); + proxyRequest.Content = new StringContent(content, Encoding.UTF8, httpContext.Request.ContentType ?? string.Empty); } } @@ -464,7 +465,7 @@ public async Task ProxyAsync(HttpContext httpContext, string rout proxyRequest.RequestUri = requestUri; // Set authentication to request - IAuthStrategy authStrategy = _authStrategyFactory.GetAuthStrategy(routeName); + IRouteAuthenticationStrategy authStrategy = await _routeAuthManager.GetAuthStrategyAsync(routeName); await authStrategy.SetAuthenticationAsync(httpContext.RequestServices, proxyRequest); HttpResponseMessage proxyResponse = await _httpClient.SendAsync(proxyRequest); diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs index 5660967..e056c80 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs @@ -16,21 +16,18 @@ internal static class GatewayRouting /// The services. internal static void AddGatewayRouting(IServiceCollection services, GatewayRoutingSettings settings) { - // Set up a factory for auth strategy factory - services.AddSingleton(serviceProvider => - { - var factory = new AuthStrategyFactory(serviceProvider.GetRequiredService()); - factory.AddAuthStrategy(NoAuthenticationAuthStrategy.NAME, typeof(NoAuthenticationAuthStrategy)); - factory.AddAuthStrategy(TokenPassThroughAuthStrategy.NAME, typeof(TokenPassThroughAuthStrategy)); - factory.AddAuthStrategy(AzureOnBehalfOfAuthStrategy.NAME, typeof(AzureOnBehalfOfAuthStrategy)); - return factory; - }); - // Register all other needed services services.AddHttpForwarder(); services.AddSingleton(settings); services.AddTransient(); services.AddReverseProxy().AddGatewayRoutes(settings); + + // Set up routing authentication subsystem + services.AddSingleton(); + services.AddKeyedTransient(NoAuthenticationAuthStrategy.NAME); + services.AddKeyedTransient(TokenPassThroughAuthStrategy.NAME); + services.AddKeyedTransient(AzureOnBehalfOfAuthStrategy.NAME); + services.AddKeyedTransient(ClientCredentialOnlyAuthStrategy.NAME); } /// diff --git a/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs b/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs index acb64bf..43a562e 100644 --- a/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs +++ b/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs @@ -112,7 +112,13 @@ public static GatewayBuilder AddGateway(this IServiceCollection services) /// A configured gateway builder. public static ConfiguredGatewayBuilder LoadConfiguration(this GatewayBuilder gatewayBuilder, IConfiguration config) { - GatewaySettings settings = config.Get(); + GatewaySettings? settings = config.Get(); + + if(settings == null) + { + throw new InvalidOperationException("The provided configuration does not contain proper settings"); + } + if (settings.Authentication != null) gatewayBuilder.Services.AddSingleton(settings.Authentication); if (settings.Routing != null) gatewayBuilder.Services.AddSingleton(settings.Routing); ConfiguredGatewayBuilder configuredGatewayBuilder = new ConfiguredGatewayBuilder(gatewayBuilder.Services, settings); From c05de30ce30212ce13cace42063f854f6d8f7420 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Fri, 29 Mar 2024 09:18:21 +0100 Subject: [PATCH 03/16] Fixes after refactoring --- .../Authentication/GatewayAuthentication.cs | 2 +- .../Auth/ClientCredentialOnlyAuthStrategy.cs | 2 +- .../Routing/Auth/NoAuthenticationAuthStrategy.cs | 9 --------- .../Routing/Auth/TokenPassThroughAuthStrategy.cs | 14 ++++++-------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs index 6dfa918..4d7535b 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs @@ -76,7 +76,7 @@ internal static void AddGatewayAuthentication(IServiceCollection services, Gatew options.Authority = settings.Authority; options.ClientId = settings.ClientId; options.UsePkce = true; - options.ClientSecret = settings.ClientSecret; + //options.ClientSecret = settings.ClientSecret; options.ResponseType = OpenIdConnectResponseType.Code; options.SaveTokens = false; options.GetClaimsFromUserInfoEndpoint = settings.QueryUserInfoEndpoint; diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs index ed6192c..7d5065c 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs @@ -49,7 +49,7 @@ internal class ClientCredentialOnlyAuthStrategy : IRouteAuthenticationStrategy /// /// The name of the auth strategy. /// - public const string NAME = "AzureOnBehalfOf"; + public const string NAME = "ClientCredentialsOnly"; /// /// The discovery document service. diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs index 37e1e18..35c4319 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs @@ -13,15 +13,6 @@ public class NoAuthenticationAuthStrategy : IRouteAuthenticationStrategy /// public const string NAME = "NoAuthentication"; - /// - /// Initializes a new instance of the class. - /// - /// The authentication settings. - public NoAuthenticationAuthStrategy(RouteAuthenticationSettings authenticationSettings) - { - } - - /// /// Gets the name of the strategy. /// diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs index 5fde61d..8bc5c2c 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs @@ -17,18 +17,14 @@ internal class TokenPassThroughAuthStrategy : IRouteAuthenticationStrategy public const string NAME = "TokenPassThrough"; /// - /// The authentication settings. + /// The gateway authentication settings. /// - private readonly RouteAuthenticationSettings _authenticationSettings; + private GatewayAuthenticationSettings? _gatewayAuthenticationSettings; /// - /// Initializes a new instance of the class. + /// The route authentication settings. /// - /// The token service. - public TokenPassThroughAuthStrategy(RouteAuthenticationSettings authenticationSettings) - { - _authenticationSettings = authenticationSettings; - } + private RouteAuthenticationSettings? _routeAuthenticationSettings; /// /// Gets the name of the strategy. @@ -46,6 +42,8 @@ public TokenPassThroughAuthStrategy(RouteAuthenticationSettings authenticationSe /// public Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) { + _gatewayAuthenticationSettings = gatewayAuthenticationSettings; + _routeAuthenticationSettings = routeAuthenticationSettings; return Task.CompletedTask; } From a5509119f13ac33a0bbc0e3734a819998bce281c Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Fri, 29 Mar 2024 09:20:47 +0100 Subject: [PATCH 04/16] Made token service public --- .../Authentication/TokenService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs index 622587c..c346771 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs @@ -10,7 +10,7 @@ namespace Fancy.ResourceLinker.Gateway.Authentication; /// /// A token service with handling logic for tokens needed by the gateway authentication feature. /// -internal class TokenService +public class TokenService { /// /// The token store. @@ -87,7 +87,7 @@ internal async Task SaveOrUpdateTokenAsync(string sessionId, OpenIdConnectMessag /// Gets the access token of the current session asynchronous. /// /// The access token. - internal async Task GetAccessTokenAsync() + public async Task GetAccessTokenAsync() { if(CurrentSessionId == null) { @@ -135,7 +135,7 @@ internal async Task GetAccessTokenAsync() /// Gets the access token claims of the current session asynchronous. /// /// - internal async Task?> GetAccessTokenClaimsAsync() + public async Task?> GetAccessTokenClaimsAsync() { if (CurrentSessionId == null) return null; @@ -155,7 +155,7 @@ internal async Task GetAccessTokenAsync() /// Gets the identity claims of the current session asynchronous. /// /// - internal async Task?> GetIdentityClaimsAsync() + public async Task?> GetIdentityClaimsAsync() { if (CurrentSessionId == null) return null; From 5d83d2a4a5579335ae0d95d7eed4ee6646398465 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Fri, 29 Mar 2024 09:23:16 +0100 Subject: [PATCH 05/16] Updated workflow to .NET 8 --- .../Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml | 2 +- .github/workflows/Fancy.ResourceLinker.Gateway.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml b/.github/workflows/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml index 948959a..ecd0553 100644 --- a/.github/workflows/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml +++ b/.github/workflows/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml @@ -26,7 +26,7 @@ jobs: - name: Install .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x # Build library - name: Build library diff --git a/.github/workflows/Fancy.ResourceLinker.Gateway.yaml b/.github/workflows/Fancy.ResourceLinker.Gateway.yaml index b035584..5cda43f 100644 --- a/.github/workflows/Fancy.ResourceLinker.Gateway.yaml +++ b/.github/workflows/Fancy.ResourceLinker.Gateway.yaml @@ -26,7 +26,7 @@ jobs: - name: Install .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x # Build library - name: Build library From 5adecf1e8485fb020f96a84375b646cf21ff4b0d Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Fri, 29 Mar 2024 09:26:34 +0100 Subject: [PATCH 06/16] accessibiliy fixes --- copyToLocalFeed.bat | 8 +++++++- .../Authentication/TokenClient.cs | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/copyToLocalFeed.bat b/copyToLocalFeed.bat index ddec1fc..c347c5e 100644 --- a/copyToLocalFeed.bat +++ b/copyToLocalFeed.bat @@ -1,2 +1,8 @@ +dotnet pack .\src\Fancy.ResourceLinker.Models\ -c debug +xcopy .\src\Fancy.ResourceLinker.Models\bin\Debug\*.nupkg ..\..\Packages + dotnet pack .\src\Fancy.ResourceLinker.Gateway\ -c debug -xcopy .\src\Fancy.ResourceLinker.Gateway\bin\Debug\*.nupkg ..\..\Packages \ No newline at end of file +xcopy .\src\Fancy.ResourceLinker.Gateway\bin\Debug\*.nupkg ..\..\Packages + +dotnet pack .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\ -c debug +xcopy .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\bin\Debug\*.nupkg ..\..\Packages \ No newline at end of file diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs index f01cf6c..5d87a91 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs @@ -6,7 +6,7 @@ namespace Fancy.ResourceLinker.Gateway.Authentication; /// /// Class to hold the result of a token refresh response. /// -internal class TokenRefreshResponse +public class TokenRefreshResponse { /// /// Gets or sets the identifier token. @@ -48,7 +48,7 @@ internal class TokenRefreshResponse /// /// Class to hold the result of a client credentials token response. /// -internal class ClientCredentialsTokenResponse +public class ClientCredentialsTokenResponse { /// /// Gets or sets the access token. @@ -72,7 +72,7 @@ internal class ClientCredentialsTokenResponse /// /// A token client with implementation of typical token logic. /// -internal class TokenClient +public class TokenClient { /// /// The authentication settings. From 8d1eadcf7901644de3fc05c628701af1dd5d4de3 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Fri, 29 Mar 2024 17:21:51 +0100 Subject: [PATCH 07/16] Created common namespace for common gateway services --- copyToLocalFeed.bat | 6 +++--- .../DbTokenStore.cs | 2 +- ...eLinker.Gateway.EntityFrameworkCore.csproj | 10 +++++---- .../GatewayDbContext.cs | 6 +++--- .../GatewayEntityFrameworkCore.cs | 21 ------------------- .../ServiceCollectionExtensions.cs | 19 +++++++++-------- .../TokenSet.cs | 4 ++-- .../Authentication/GatewayAuthentication.cs | 2 +- .../Authentication/ITokenStore.cs | 6 ++++-- .../Authentication/InMemoryTokenStore.cs | 4 +++- .../Authentication/TokenClient.cs | 1 + .../DiscoveryDocumentService.cs | 3 ++- .../Common/GatewayCommon.cs | 18 ++++++++++++++++ .../Fancy.ResourceLinker.Gateway.csproj | 8 +++---- .../Auth/AzureOnBehalfOfAuthStrategy.cs | 4 ++++ .../Auth/ClientCredentialOnlyAuthStrategy.cs | 3 ++- .../ServiceCollectionExtensions.cs | 2 ++ .../Fancy.ResourceLinker.Hateoas.csproj | 6 +++--- .../Fancy.ResourceLinker.Models.ITest.csproj | 13 +++++++----- .../Fancy.ResourceLinker.Models.UTest.csproj | 13 +++++++----- .../Fancy.ResourceLinker.Models.csproj | 4 ++-- 21 files changed, 87 insertions(+), 68 deletions(-) delete mode 100644 src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayEntityFrameworkCore.cs rename src/Fancy.ResourceLinker.Gateway/{Authentication => Common}/DiscoveryDocumentService.cs (93%) create mode 100644 src/Fancy.ResourceLinker.Gateway/Common/GatewayCommon.cs diff --git a/copyToLocalFeed.bat b/copyToLocalFeed.bat index c347c5e..ff949e6 100644 --- a/copyToLocalFeed.bat +++ b/copyToLocalFeed.bat @@ -1,8 +1,8 @@ dotnet pack .\src\Fancy.ResourceLinker.Models\ -c debug -xcopy .\src\Fancy.ResourceLinker.Models\bin\Debug\*.nupkg ..\..\Packages +xcopy .\src\Fancy.ResourceLinker.Models\bin\Debug\*.nupkg ..\..\Packages /Y dotnet pack .\src\Fancy.ResourceLinker.Gateway\ -c debug -xcopy .\src\Fancy.ResourceLinker.Gateway\bin\Debug\*.nupkg ..\..\Packages +xcopy .\src\Fancy.ResourceLinker.Gateway\bin\Debug\*.nupkg ..\..\Packages /Y dotnet pack .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\ -c debug -xcopy .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\bin\Debug\*.nupkg ..\..\Packages \ No newline at end of file +xcopy .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\bin\Debug\*.nupkg ..\..\Packages /Y \ No newline at end of file diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs index 07f0989..43b0342 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs @@ -40,7 +40,7 @@ public DbTokenStore(GatewayDbContext dbContext) } /// - /// Saves the or update tokens asynchronous. + /// Saves or update tokens asynchronous. /// /// The session identifier. /// The identifier token. diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj index a316e4c..175253c 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - 0.0.7 + 0.0.8 net8.0 enable enable @@ -22,9 +22,11 @@ - - - + + + + + diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayDbContext.cs b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayDbContext.cs index 0a054ff..05ca812 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayDbContext.cs +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayDbContext.cs @@ -6,13 +6,13 @@ namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; /// /// A database context to hold all information needed to be persisted in a gateway. /// -internal class GatewayDbContext : DbContext, IDataProtectionKeyContext +public abstract class GatewayDbContext : DbContext, IDataProtectionKeyContext { /// /// Initializes a new instance of the class. /// /// The options. - public GatewayDbContext(DbContextOptions options) : base(options) { } + public GatewayDbContext(DbContextOptions options) : base(options) { } /// /// Gets or sets the data protection keys. @@ -29,4 +29,4 @@ public GatewayDbContext(DbContextOptions options) : base(optio /// The token sets. /// public DbSet TokenSets { get; set; } = null!; -} +} \ No newline at end of file diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayEntityFrameworkCore.cs b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayEntityFrameworkCore.cs deleted file mode 100644 index a1219b3..0000000 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayEntityFrameworkCore.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; - -/// -/// Class with helper methods to set up the ef core feature. -/// -public static class GatewayEntityFrameworkCore -{ - /// - /// Ensures that the gateway database has been created. - /// - /// The web application. - public static void EnsureGatewayDbCreated(this WebApplication webApp) - { - using IServiceScope scope = webApp.Services.CreateScope(); - GatewayDbContext dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - } -} diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/ServiceCollectionExtensions.cs b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/ServiceCollectionExtensions.cs index bf7d3e3..e73eb67 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/ServiceCollectionExtensions.cs +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Fancy.ResourceLinker.Gateway.Authentication; using Microsoft.AspNetCore.DataProtection; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; @@ -16,20 +15,22 @@ public static class ServiceCollectionExtensions private static bool _dbContextAdded = false; /// - /// Adds the database context to the gateway. + /// Uses the provided database context in the gateway. /// - /// A type of a gateway builder. + /// The type of the database context. /// The builder. - /// The options action. - /// A type of a gateway builder. - public static T AddDbContext(this T builder, Action optionsAction) where T : GatewayBuilder + /// + /// A type of a gateway builder. + /// + /// DbContext can be added only once + public static GatewayBuilder UseDbContext(this GatewayBuilder builder) where TDbContext : GatewayDbContext { if(_dbContextAdded) { throw new InvalidOperationException("DbContext can be added only once"); } - builder.Services.AddDbContext(optionsAction); + builder.Services.AddScoped(services => services.GetRequiredService()); _dbContextAdded = true; @@ -45,7 +46,7 @@ public static GatewayAuthenticationBuilder UseDbTokenStore(this GatewayAuthentic { if (!_dbContextAdded) { - throw new InvalidOperationException("Call 'AddDbContext' before configuring options"); + throw new InvalidOperationException("Call 'AddDbContext' before configuring options using the db context"); } builder.Services.AddScoped(); @@ -62,7 +63,7 @@ public static GatewayAntiForgeryBuilder UseDbKeyStore(this GatewayAntiForgeryBui { if (!_dbContextAdded) { - throw new InvalidOperationException("Call 'AddDbContext' before configuring options"); + throw new InvalidOperationException("Call 'AddDbContext' before configuring options using the db context"); } builder.Services.AddDataProtection().PersistKeysToDbContext(); diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/TokenSet.cs b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/TokenSet.cs index ebc35a4..5332e22 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/TokenSet.cs +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/TokenSet.cs @@ -5,10 +5,10 @@ namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; /// /// A entity object to save token sets to database. /// -internal class TokenSet +public class TokenSet { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The session identifier. /// The identifier token. diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs index 4d7535b..2fe5d72 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs @@ -1,4 +1,5 @@ using Fancy.ResourceLinker.Gateway; +using Fancy.ResourceLinker.Gateway.Common; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; @@ -40,7 +41,6 @@ internal sealed class GatewayAuthentication internal static void AddGatewayAuthentication(IServiceCollection services, GatewayAuthenticationSettings settings) { _settings = settings; - services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddHostedService(); diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/ITokenStore.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/ITokenStore.cs index cbc159c..52098bd 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/ITokenStore.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/ITokenStore.cs @@ -12,8 +12,10 @@ public interface ITokenStore /// The identifier token. /// The access token. /// The refresh token. - /// The expiration. - /// A task indicating the completion of the asynchronous operation. + /// The expires at. + /// + /// A task indicating the completion of the asynchronous operation. + /// Task SaveOrUpdateTokensAsync(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiresAt); /// diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs index 07dec4e..442c5eb 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs @@ -19,7 +19,9 @@ internal class InMemoryTokenStore : ITokenStore /// The access token. /// The refresh token. /// The expiration. - /// A task indicating the completion of the asynchronous operation. + /// + /// A task indicating the completion of the asynchronous operation. + /// public Task SaveOrUpdateTokensAsync(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiration) { _tokens[sessionId] = new TokenRecord(sessionId, idToken, accessToken, refreshToken, expiration ); diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs index 5d87a91..b61719c 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs @@ -1,5 +1,6 @@ using System.Net.Http.Json; using System.Text.Json.Serialization; +using Fancy.ResourceLinker.Gateway.Common; namespace Fancy.ResourceLinker.Gateway.Authentication; diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/DiscoveryDocumentService.cs b/src/Fancy.ResourceLinker.Gateway/Common/DiscoveryDocumentService.cs similarity index 93% rename from src/Fancy.ResourceLinker.Gateway/Authentication/DiscoveryDocumentService.cs rename to src/Fancy.ResourceLinker.Gateway/Common/DiscoveryDocumentService.cs index 99b10b8..4973997 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/DiscoveryDocumentService.cs +++ b/src/Fancy.ResourceLinker.Gateway/Common/DiscoveryDocumentService.cs @@ -1,6 +1,7 @@ using System.Net.Http.Json; +using Fancy.ResourceLinker.Gateway.Authentication; -namespace Fancy.ResourceLinker.Gateway.Authentication; +namespace Fancy.ResourceLinker.Gateway.Common; /// /// A service to retrieve the discovery document from an authorization server. diff --git a/src/Fancy.ResourceLinker.Gateway/Common/GatewayCommon.cs b/src/Fancy.ResourceLinker.Gateway/Common/GatewayCommon.cs new file mode 100644 index 0000000..a77b7ce --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Common/GatewayCommon.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Fancy.ResourceLinker.Gateway.Common; + +/// +/// Class with helper methods to set up the common services. +/// +internal class GatewayCommon +{ + /// + /// Adds the commmon gateway services. + /// + /// The services. + internal static void AddGatewayCommonServices(IServiceCollection services) + { + services.AddSingleton(); + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj b/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj index f068d91..7798132 100644 --- a/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj +++ b/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj @@ -1,7 +1,7 @@  - 0.0.7 + 0.0.8 net8.0 enable enable @@ -10,7 +10,7 @@ Fancy Resource Linker Gateway - A library to create API Gateways with dynamic data structures on top of ASP.NET Core.. + A library to create API Gateways with dynamic data structures on top of ASP.NET Core. fancy Development - Daniel Murrmann Copyright 2015-2023 fancyDevelopment - Daniel Murrmann Apache-2.0 @@ -22,8 +22,8 @@ - - + + diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs index c8737b9..0fe99bf 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs @@ -1,4 +1,5 @@ using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Common; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -9,6 +10,9 @@ namespace Fancy.ResourceLinker.Gateway.Routing.Auth; +/// +/// A model to deserialize the on behalf of token response. +/// class OnBehalfOfTokenResponse { /// diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs index 7d5065c..11f02cb 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs @@ -1,4 +1,5 @@ using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Common; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -134,7 +135,7 @@ public async Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenti if (routeAuthenticationSettings.Options.ContainsKey("ClientId")) { - _clientId = routeAuthenticationSettings.Options["Client"]; + _clientId = routeAuthenticationSettings.Options["ClientId"]; } else if (!string.IsNullOrEmpty(gatewayAuthenticationSettings?.ClientId)) { diff --git a/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs b/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs index 43a562e..875dc69 100644 --- a/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs +++ b/src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Fancy.ResourceLinker.Gateway.AntiForgery; using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Common; using Fancy.ResourceLinker.Gateway.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -101,6 +102,7 @@ public static class ServiceCollectionExtensions /// A gateway builder. public static GatewayBuilder AddGateway(this IServiceCollection services) { + GatewayCommon.AddGatewayCommonServices(services); return new GatewayBuilder(services); } diff --git a/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj b/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj index 396eafe..9068511 100644 --- a/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj +++ b/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj @@ -1,8 +1,8 @@ - + - 0.0.7 - net6.0 + 0.0.8 + net8.0 enable enable diff --git a/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj b/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj index 0268559..b5601ef 100644 --- a/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj +++ b/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -10,10 +10,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj b/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj index b607021..eab4c57 100644 --- a/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj +++ b/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable @@ -10,10 +10,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj b/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj index a042b15..d8bf886 100644 --- a/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj +++ b/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj @@ -1,8 +1,8 @@  - 0.0.7 - net6.0 + 0.0.8 + net8.0 enable enable From e7bd8cdcf729133632e85859f722e9add172330a Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Fri, 29 Mar 2024 18:21:42 +0100 Subject: [PATCH 08/16] Fixed tests and multi threading issues --- .../DbTokenStore.cs | 31 ++++++++++++++++--- .../Authentication/InMemoryTokenStore.cs | 10 +++--- .../TokenCleanupBackgroundService.cs | 1 + .../DynamicResourceSerializerTests.cs | 4 +-- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs index 43b0342..ea653dc 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs @@ -1,5 +1,6 @@ using Fancy.ResourceLinker.Gateway.Authentication; using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; @@ -14,6 +15,11 @@ internal class DbTokenStore : ITokenStore /// private readonly GatewayDbContext _dbContext; + /// + /// The cached tokens. + /// + ConcurrentDictionary _cachedTokens = new ConcurrentDictionary(); + /// /// Initializes a new instance of the class. /// @@ -30,13 +36,29 @@ public DbTokenStore(GatewayDbContext dbContext) /// /// A token record if available. /// - public async Task GetTokenRecordAsync(string sessionId) + /// + /// This method is thread save to enable paralell calls from the gateway to backends. + /// + public Task GetTokenRecordAsync(string sessionId) { - TokenSet? tokenSet = await _dbContext.TokenSets.SingleOrDefaultAsync(ts => ts.SessionId == sessionId); + TokenSet? tokenSet; + + lock (_dbContext) + { + if (!_cachedTokens.ContainsKey(sessionId)) + { + tokenSet = _dbContext.TokenSets.SingleOrDefault(ts => ts.SessionId == sessionId); + _cachedTokens[sessionId] = tokenSet; + } + else + { + tokenSet = _cachedTokens[sessionId]; + } + } - if (tokenSet == null) { return null; } + if (tokenSet == null) { return Task.FromResult(null); } - return new TokenRecord(sessionId, tokenSet.IdToken, tokenSet.AccessToken, tokenSet.RefreshToken, tokenSet.ExpiresAt); + return Task.FromResult(new TokenRecord(sessionId, tokenSet.IdToken, tokenSet.AccessToken, tokenSet.RefreshToken, tokenSet.ExpiresAt)); } /// @@ -64,6 +86,7 @@ public async Task SaveOrUpdateTokensAsync(string sessionId, string idToken, stri tokenSet.ExpiresAt = expiresAt; } + _cachedTokens[sessionId] = tokenSet; await _dbContext.SaveChangesAsync(); } diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs index 442c5eb..325a3dc 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs @@ -1,4 +1,6 @@ -namespace Fancy.ResourceLinker.Gateway.Authentication; +using System.Collections.Concurrent; + +namespace Fancy.ResourceLinker.Gateway.Authentication; /// /// A simple implementation of to store tokens in memory. @@ -9,7 +11,7 @@ internal class InMemoryTokenStore : ITokenStore /// /// A dictionary mapping tokens to sessions. /// - private Dictionary _tokens = new Dictionary(); + private ConcurrentDictionary _tokens = new ConcurrentDictionary(); /// /// Saves the or update tokens asynchronous. @@ -47,11 +49,11 @@ public Task SaveOrUpdateTokensAsync(string sessionId, string idToken, string acc /// A task indicating the completion of the asynchronous operation. public Task CleanupExpiredTokenRecordsAsync() { - Dictionary validRecords = new Dictionary (); + ConcurrentDictionary validRecords = new ConcurrentDictionary (); foreach(var record in _tokens.Values) { - if(record.ExpiresAt > DateTime.UtcNow) validRecords.Add(record.SessionId, record); + if(record.ExpiresAt > DateTime.UtcNow) validRecords[record.SessionId] = record; } _tokens = validRecords; diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenCleanupBackgroundService.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenCleanupBackgroundService.cs index 6e95a90..47c9dcc 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenCleanupBackgroundService.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenCleanupBackgroundService.cs @@ -49,6 +49,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) while (await timer.WaitForNextTickAsync(stoppingToken)) { await tokenStore.CleanupExpiredTokenRecordsAsync(); + _logger.LogTrace("Cleaned up expired tokens"); } } catch (OperationCanceledException) diff --git a/src/Fancy.ResourceLinker.Models.ITest/DynamicResourceSerializerTests.cs b/src/Fancy.ResourceLinker.Models.ITest/DynamicResourceSerializerTests.cs index 07eec92..0a56b92 100644 --- a/src/Fancy.ResourceLinker.Models.ITest/DynamicResourceSerializerTests.cs +++ b/src/Fancy.ResourceLinker.Models.ITest/DynamicResourceSerializerTests.cs @@ -63,7 +63,7 @@ public void DeserializeAndSerializeComplexObject() Assert.AreEqual("foobar", deserializedObject.StringProperty); Assert.AreEqual(true, deserializedObject.BoolProperty); Assert.AreEqual("subObjFoobar", deserializedObject.ObjProperty.SubObjProperty); - Assert.AreEqual(null, deserializedObject.NullProperty); + Assert.IsNull(deserializedObject.NullProperty); Assert.AreEqual(5, deserializedObject.ArrayProperty[0]); Assert.AreEqual("foo", deserializedObject.ArrayProperty[1]); Assert.AreEqual("fooInArray", deserializedObject.ArrayProperty[2].ObjInArrProperty); @@ -92,7 +92,7 @@ public void DeserializeIntoObjectWithNonPubCtor() Assert.AreEqual("foobar", deserializedObject.StringProperty); Assert.AreEqual(true, deserializedObject.BoolProperty); Assert.AreEqual("subObjFoobar", deserializedObject.ObjProperty.SubObjProperty); - Assert.AreEqual(null, deserializedObject.NullProperty); + Assert.IsNull(deserializedObject.NullProperty); Assert.AreEqual(5, deserializedObject.ArrayProperty[0]); Assert.AreEqual("foo", deserializedObject.ArrayProperty[1]); Assert.AreEqual("fooInArray", deserializedObject.ArrayProperty[2].ObjInArrProperty); From 96a6cfec1fec800eac20236440f19728e01e699c Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Mon, 15 Apr 2024 14:47:13 +0200 Subject: [PATCH 09/16] Implemented special auth0 client credential flow --- .../Authentication/TokenService.cs | 2 +- .../Auth0ClientCredentialOnlyAuthStrategy.cs | 71 +++++++++++++++++++ .../Auth/ClientCredentialOnlyAuthStrategy.cs | 41 ++++++----- 3 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs index c346771..17c477d 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs @@ -64,7 +64,7 @@ internal async Task SaveTokenForNewSessionAsync(OpenIdConnectMessage tok { // Create a new guid for the new session string sessionId = Guid.NewGuid().ToString(); - await SaveOrUpdateTokenAsync (sessionId, tokenResponse); + await SaveOrUpdateTokenAsync(sessionId, tokenResponse); return sessionId; } diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs new file mode 100644 index 0000000..d736d77 --- /dev/null +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs @@ -0,0 +1,71 @@ +using Fancy.ResourceLinker.Gateway.Authentication; +using Fancy.ResourceLinker.Gateway.Common; +using Microsoft.Extensions.Logging; + +namespace Fancy.ResourceLinker.Gateway.Routing.Auth; + +/// +/// A route authentication strategy running the OAuth client credential flow only. +/// +/// +/// In some situations, auth0 requires an audience parameter in the request. This once can be added with this auth strategy. +/// +internal class Auth0ClientCredentialOnlyAuthStrategy : ClientCredentialOnlyAuthStrategy +{ + /// + /// The name of the auth strategy. + /// + public new const string NAME = "Auth0ClientCredentialOnly"; + + // + /// The auth0 audience. + /// + private string _audience = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// The discovery document service. + /// The logger. + public Auth0ClientCredentialOnlyAuthStrategy(DiscoveryDocumentService discoveryDocumentService, ILogger logger) : base(discoveryDocumentService, logger) + { + } + + /// + /// Gets the name of the strategy. + /// + /// + /// The name. + /// + public override string Name => NAME; + + /// + /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. + /// + /// The gateway authentication settigns. + /// The route authentication settigns. + public override Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) + { + if (routeAuthenticationSettings.Options.ContainsKey("Audience")) + { + _audience = routeAuthenticationSettings.Options["Audience"]; + } + + return base.InitializeAsync(gatewayAuthenticationSettings, routeAuthenticationSettings); + } + + /// + /// Sets up the token request. + /// + /// The token request. + protected override Dictionary SetUpTokenRequest() + { + return new Dictionary + { + { "grant_type", "client_credentials" }, + { "client_id", _clientId }, + { "client_secret", _clientSecret }, + { "scope", _scope } + }; + } +} diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs index 11f02cb..76f4944 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/ClientCredentialOnlyAuthStrategy.cs @@ -65,32 +65,32 @@ internal class ClientCredentialOnlyAuthStrategy : IRouteAuthenticationStrategy /// /// The discovery document. /// - private DiscoveryDocument? _discoveryDocument; + protected DiscoveryDocument? _discoveryDocument; /// /// The client identifier. /// - private string _clientId = string.Empty; + protected string _clientId = string.Empty; /// /// The client secret. /// - private string _clientSecret = string.Empty; + protected string _clientSecret = string.Empty; /// /// The scope. /// - private string _scope = string.Empty; + protected string _scope = string.Empty; /// /// The is initialized. /// - private bool _isInitialized = false; + protected bool _isInitialized = false; /// /// The current token response. /// - private ClientCredentialsTokenResponse? _currentTokenResponse; + protected ClientCredentialsTokenResponse? _currentTokenResponse; /// /// Initializes a new instance of the class. @@ -109,14 +109,14 @@ public ClientCredentialOnlyAuthStrategy(DiscoveryDocumentService discoveryDocume /// /// The name. /// - public string Name => NAME; + public virtual string Name => NAME; /// /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. /// /// The gateway authentication settigns. /// The route authentication settigns. - public async Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) + public virtual async Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) { string authority; @@ -210,6 +210,21 @@ public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpR request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } + /// + /// Sets up the token request. + /// + /// The token request. + protected virtual Dictionary SetUpTokenRequest() + { + return new Dictionary + { + { "grant_type", "client_credentials" }, + { "client_id", _clientId }, + { "client_secret", _clientSecret }, + { "scope", _scope } + }; + } + /// /// Gets the access token asynchronous. /// @@ -230,13 +245,7 @@ private async Task GetAccessTokenAsync() /// private async Task GetTokenViaClientCredentialsAsync() { - var payload = new Dictionary - { - { "grant_type", "client_credentials" }, - { "client_id", _clientId }, - { "client_secret", _clientSecret }, - { "scope", _scope } - }; + Dictionary payload = SetUpTokenRequest(); HttpClient httpClient = new HttpClient(); @@ -262,7 +271,7 @@ private async Task GetTokenViaClientCredentialsA ClientCredentialsTokenResponse? result = await response.Content.ReadFromJsonAsync(); - if(result == null) + if (result == null) { throw new InvalidOperationException("Could not deserialize client credential token response"); } From ef0d293a484f96cfdf2a302d8b88c00c36a163b7 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Tue, 16 Apr 2024 10:21:25 +0200 Subject: [PATCH 10/16] Implemented auth0 client credential flow and updated version --- ...ourceLinker.Gateway.EntityFrameworkCore.csproj | 15 +++++++++------ .../Fancy.ResourceLinker.Gateway.csproj | 4 ++-- .../Auth/Auth0ClientCredentialOnlyAuthStrategy.cs | 3 ++- .../Routing/GatewayRouting.cs | 1 + .../Fancy.ResourceLinker.Hateoas.csproj | 2 +- .../Fancy.ResourceLinker.Models.ITest.csproj | 4 ++-- .../Fancy.ResourceLinker.Models.UTest.csproj | 4 ++-- .../Fancy.ResourceLinker.Models.csproj | 2 +- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj index 175253c..d296220 100644 --- a/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj +++ b/src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - 0.0.8 + 0.0.9 net8.0 enable enable @@ -22,11 +22,14 @@ - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj b/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj index 7798132..b2339cc 100644 --- a/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj +++ b/src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj @@ -1,7 +1,7 @@  - 0.0.8 + 0.0.9 net8.0 enable enable @@ -22,7 +22,7 @@ - + diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs index d736d77..538437e 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialOnlyAuthStrategy.cs @@ -65,7 +65,8 @@ protected override Dictionary SetUpTokenRequest() { "grant_type", "client_credentials" }, { "client_id", _clientId }, { "client_secret", _clientSecret }, - { "scope", _scope } + { "scope", _scope }, + { "audience", _audience } }; } } diff --git a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs index e056c80..281ed5d 100644 --- a/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs +++ b/src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs @@ -28,6 +28,7 @@ internal static void AddGatewayRouting(IServiceCollection services, GatewayRouti services.AddKeyedTransient(TokenPassThroughAuthStrategy.NAME); services.AddKeyedTransient(AzureOnBehalfOfAuthStrategy.NAME); services.AddKeyedTransient(ClientCredentialOnlyAuthStrategy.NAME); + services.AddKeyedTransient(Auth0ClientCredentialOnlyAuthStrategy.NAME); } /// diff --git a/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj b/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj index 9068511..5625447 100644 --- a/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj +++ b/src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj @@ -1,7 +1,7 @@  - 0.0.8 + 0.0.9 net8.0 enable enable diff --git a/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj b/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj index b5601ef..e120e5f 100644 --- a/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj +++ b/src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj b/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj index eab4c57..4e36df4 100644 --- a/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj +++ b/src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj b/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj index d8bf886..30fc443 100644 --- a/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj +++ b/src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj @@ -1,7 +1,7 @@  - 0.0.8 + 0.0.9 net8.0 enable enable From b1cd48351581d1e7f9adc65f71ae3526fadecfd6 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Thu, 25 Apr 2024 19:09:26 +0200 Subject: [PATCH 11/16] Documentation update --- README.md | 35 ++++++++++++++++--- doc/features/authentication.md | 1 + .../Authentication/GatewayAuthentication.cs | 1 - 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 doc/features/authentication.md diff --git a/README.md b/README.md index 897b218..4d9093c 100644 --- a/README.md +++ b/README.md @@ -68,25 +68,52 @@ builder.Services.AddGateway() .LoadConfiguration(builder.Configuration.GetSection("Gateway")); ``` -In your application add a configuration section with the name `"Gateway"` and create the default structure within it as shown in the following snippet: +In your application add a configuration section with the name `"Gateway"` and create the default structure within it as shown in the following sample snippet: ```json "Gateway": { "Routing": { "Routes": { + "Microservice1": { + "BaseUrl": "http://localhost:5000", + } + } } } ``` -With those three steps the library does not do anything yet but is prepared to realize one or more of the features mentioned at the beginning. +Finally you can ask for a type called `GatewayRouter` via dependency injection and use it to make calls to your microservices/backends using the name of your route in the configuration and a relative path to the data you would like to retrieve. -## Realize Features in your Gateway +```cs +[ApiController] +public class HomeController : HypermediaController +{ + private readonly GatewayRouter _router; + + public HomeController(GatewayRouter router) + { + _router = router; + } + + [HttpGet] + [Route("api/views/home")] + public async Task GetHomeViewModel() + { + var dataFromMicroservice = _router.GetAsync("Microservice1", "api/data/123"); + [...] + } +} +``` + +With this basic set up you can make calls to your microservices easily and change the base address in the configuration. Read through the advanced features docs to extend your api gateway with more capabilities. + +## Realize Advanced Features in your Gateway To learn how each single feature can be realized have a look to the following individual guidelines. * Aggregating data from differend sources into a client optimized model - Comming Soon * Provide different apis and/or resources under the same origin - Comming Soon * Create truly RESTful services - Comming Soon -* Let the gateway act as authentication facade - Comming Soon +* [Let the gateway act as authentication facade](./doc/features/authentication.md) diff --git a/doc/features/authentication.md b/doc/features/authentication.md new file mode 100644 index 0000000..66a783b --- /dev/null +++ b/doc/features/authentication.md @@ -0,0 +1 @@ +# Authentication \ No newline at end of file diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs index 2fe5d72..59cf924 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs @@ -1,5 +1,4 @@ using Fancy.ResourceLinker.Gateway; -using Fancy.ResourceLinker.Gateway.Common; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; From 72174389cc114805368fb8ffff77bcb7a642dcd2 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Thu, 25 Apr 2024 20:15:40 +0200 Subject: [PATCH 12/16] First draft of auth documentation --- README.md | 3 +- doc/features/authentication.md | 175 ++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d9093c..45e32a0 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ To get started building an api gateway with Fancy.ResourceLinker in your ASP.NET var builder = WebApplication.CreateBuilder(args); builder.Services.AddGateway() - .LoadConfiguration(builder.Configuration.GetSection("Gateway")); + .LoadConfiguration(builder.Configuration.GetSection("Gateway")) + .AddRouting(); ``` In your application add a configuration section with the name `"Gateway"` and create the default structure within it as shown in the following sample snippet: diff --git a/doc/features/authentication.md b/doc/features/authentication.md index 66a783b..9c9a32d 100644 --- a/doc/features/authentication.md +++ b/doc/features/authentication.md @@ -1 +1,174 @@ -# Authentication \ No newline at end of file +# Authentication + +The Gateway supports two modes of authentication. One is that you can hide a SPA (Single Page Application) behind the gateway and trigger and run the OAuth Code Flow + PKCE from the server side. The other is that the gateway is able to authenticate itself at the resource servers it calls. + +## Authentication at the Gateway for a Single Page Application + +To enable the authentication based on OAuth Code Flow + PKCE add the Authentication feature when registering the library to the services as shown in the snippet below: + +```cs +builder.Services.AddGateway() + .LoadConfiguration(builder.Configuration.GetSection("Gateway")) + .AddRouting() + .AddAuthentication(); // <-- Add this to activate the authentication feature +``` + +Extend your `Gateway` configuration section with an `Authentication` section and a route to your frontend. + +```json +"Gateway": { + "Authentication": { // <-- Add this section and configure it with your values + "Authority": "", + "ClientId": "", + "AuthorizationCodeScopes": "", + "SessionTimeoutInMin": 30, + "UniqueIdentifierClaimType": "" + } + "Routing": { + "Routes": { + "Microservice1": { + "BaseUrl": "http://localhost:5000", + }, + "Frontend": { // <-- Add a route to your frontend with a 'PathMatch' to activate the reverse proxy for this route + "BaseUrl": "http://localhost:4200", + "PathMatch": "{**path}", + } + } + } +} +``` + +If you have completed the steps before you can trigger the OAuth flow by loading the frontend from the origin of the gateway and setting the window location in your browser with the help of JavaScript to the `./login` endpoint as shown in the following snippet: + +```js +window.location.href = './login?redirectUri=' + encodeURIComponent(window.origin); +``` + +You can provide an optional redirect url so that after successfull login you geet automatically redirected to the url you started the login workflow from. + +To trigger the logout flow set the window location to the `./logout` endpoint as shown in the following snippent. + +```js +window.location.href = './logout'; +``` + +To get the ID token into the frontend, the frontend can simply call the `./userinfo` enpoint with a standard http GET request. + +## Authentication at the Backends/Microservices + +The gateway provides different authentication strategies you can use to make authenticated calls to your resource servers. + +### Token Pass Through + +This is the simplest way to provide a token to a resource server is to just pass through the token you got during authenticating the user at the frontend (e.g. your Single Page Application). In this case the resource server must accept the very same token the gateway gets. To pass through the token configure the authentication at the route with the `TokenPassThrough` auth strategy as shown in the following snippet. + +```json +"Gateway": { + "Authentication": { + [...] + }, + "Routing": { + "Routes": { + "Microservice1": { + "BaseUrl": "http://localhost:5000", + "Authentication": { + "Strategy": "TokenPassThrough" // <-- Configure the TokenPassThrough auth strategy + } + }, + [...] + } + } +} +``` + +### Token Exchange + +Typically a resource server requires a token especially for its own audience. In that case the token we got during authenticating the user at the frontend needs to be exchanged to a token for the audience of the resource server. To achive this configure one of the available token exchange auth strategies. + +#### Azure on Behalf of + +The Azure on Behalf of flow is proprietary flow of Microsoft to exchange a token for a specific "AppRegistration" within Microsofts EntraID. To enable this flow you need at least two AppRegistrations. One for the Gateway/Frontend and one for each resource server. The resoruce server AppRegistration then has to provide an API and the gateways AppRegistration needs be allowed to use this api. Finally can request a token for the gateway and exchange it to a token for the resource server with the help of the following configuration: + +```json +"Gateway": { + "Authentication": { // <-- Configure the properties of the AppRegistration for the gateway here + "Authority": "", + "ClientId": "", + "ClientSecret": "", // <-- Important, you need a client secret here to make the token exchange work + "AuthorizationCodeScopes": "", + "SessionTimeoutInMin": 30, + "UniqueIdentifierClaimType": "" + }, + "Routing": { + "Routes": { + "Microservice1": { + "BaseUrl": "http://localhost:5000", + "Authentication": { + "Strategy": "AzureOnBehalfOf", // <-- Set the azure on behalf of auth strategy + "Options": { + "Scope": "api://microservice1/all" // <-- Set the scope of the api you want to request + } + } + }, + [...] + } + } +} +``` + +### Client Credentials + +If you have an anonymous access to the api of the gateway but the gateway must authenticate to the resource servers you can use one of the supported client credential auth strategies. In that case no `Authentication` configuration is needed and also the `AddAuthentication()` feature dos not need to be added to the service collection. + +#### Standard Client Credentials + +In the standard client credential auth strategy we provide the gateway with the necessary data to be able to get a token with the standard OAuth Client Credential flow. To achive this, set up the authentication configuration of a route as follows: + +```json +"Gateway": { + "Routing": { + "Routes": { + "Microservice1": { + "BaseUrl": "http://localhost:5000", + "Authentication": { + "Strategy": "ClientCredentialOnly", + "Options": { + "Authority": "", + "ClientId": "", + "ClientSecret": "", + "Scope": "", + } + } + }, + [...] + } + } +} +``` + +#### Auth0 Client Credentials + +In case you would like to use the client credential auth strategy with Auth0 as a token server, you typically have to provide an additional audience parameter in the token request. For this a special auth strategy for Auth0 was created. Configure the client credential flow for Auth0 as follows: + +```json +"Gateway": { + "Routing": { + "Routes": { + "Microservice1": { + "BaseUrl": "http://localhost:5000", + "Authentication": { + "Strategy": "ClientCredentialOnly", + "Options": { + "Authority": "", + "ClientId": "", + "ClientSecret": "", + "Scope": "", + "Audience": "" // <-- This additional parameter is needed by Auth0 + } + } + }, + [...] + } + } +} +``` \ No newline at end of file From 2feca3349705d342659b01601354f7263d7cdabe Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Thu, 25 Apr 2024 20:17:58 +0200 Subject: [PATCH 13/16] Updated architecture image --- architecture.drawio.png | Bin 18478 -> 19285 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/architecture.drawio.png b/architecture.drawio.png index 4e94057ade8a66a14ca8a72f5a43dbd357f2927a..ac7027fb849a84be850cb2ef39ae654565f3efd1 100644 GIT binary patch literal 19285 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 Date: Thu, 25 Apr 2024 20:24:27 +0200 Subject: [PATCH 14/16] Typo fix --- doc/features/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/features/authentication.md b/doc/features/authentication.md index 9c9a32d..d5d5c3c 100644 --- a/doc/features/authentication.md +++ b/doc/features/authentication.md @@ -60,7 +60,7 @@ The gateway provides different authentication strategies you can use to make aut ### Token Pass Through -This is the simplest way to provide a token to a resource server is to just pass through the token you got during authenticating the user at the frontend (e.g. your Single Page Application). In this case the resource server must accept the very same token the gateway gets. To pass through the token configure the authentication at the route with the `TokenPassThrough` auth strategy as shown in the following snippet. +The simplest way to provide a token to a resource server is to just pass through the token you got during authenticating the user at the frontend (e.g. your Single Page Application). In this case the resource server must accept the very same token the gateway gets. To pass through the token configure the authentication at the route with the `TokenPassThrough` auth strategy as shown in the following snippet. ```json "Gateway": { From 29e5558a986f8c86b3174d0a4c15ff05a50f7a40 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Thu, 25 Apr 2024 22:31:48 +0200 Subject: [PATCH 15/16] Auth doc fixes --- doc/features/authentication.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/features/authentication.md b/doc/features/authentication.md index d5d5c3c..66a63f0 100644 --- a/doc/features/authentication.md +++ b/doc/features/authentication.md @@ -54,7 +54,7 @@ window.location.href = './logout'; To get the ID token into the frontend, the frontend can simply call the `./userinfo` enpoint with a standard http GET request. -## Authentication at the Backends/Microservices +## Authentication at the Resource Servers The gateway provides different authentication strategies you can use to make authenticated calls to your resource servers. @@ -157,13 +157,13 @@ In case you would like to use the client credential auth strategy with Auth0 as "Microservice1": { "BaseUrl": "http://localhost:5000", "Authentication": { - "Strategy": "ClientCredentialOnly", + "Strategy": "Auth0ClientCredentialOnly", "Options": { "Authority": "", "ClientId": "", "ClientSecret": "", "Scope": "", - "Audience": "" // <-- This additional parameter is needed by Auth0 + "Audience": "" // <-- This additional parameter is needed by Auth0 } } }, @@ -171,4 +171,8 @@ In case you would like to use the client credential auth strategy with Auth0 as } } } -``` \ No newline at end of file +``` + +### Custom Auth Strategy + +ToDo! \ No newline at end of file From 42eaf4f32a4bfc01414644ec0c226cbdd68d93f9 Mon Sep 17 00:00:00 2001 From: Daniel Murrmann Date: Sat, 11 May 2024 11:58:42 +0200 Subject: [PATCH 16/16] Enabled confidential clients again --- .../Authentication/GatewayAuthentication.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs index 59cf924..a164d09 100644 --- a/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs +++ b/src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthentication.cs @@ -15,8 +15,6 @@ namespace Fancy.ResourceLinker.Gateway.Authentication; -// ToDo: think about token exchange - /// /// Class with helper methods to set up authentication feature. /// @@ -75,7 +73,7 @@ internal static void AddGatewayAuthentication(IServiceCollection services, Gatew options.Authority = settings.Authority; options.ClientId = settings.ClientId; options.UsePkce = true; - //options.ClientSecret = settings.ClientSecret; + options.ClientSecret = string.IsNullOrWhiteSpace(settings.ClientSecret) ? null : settings.ClientSecret; options.ResponseType = OpenIdConnectResponseType.Code; options.SaveTokens = false; options.GetClaimsFromUserInfoEndpoint = settings.QueryUserInfoEndpoint;