├── src ├── Fancy.ResourceLinker.Models.ITest │ ├── Usings.cs │ ├── Fancy.ResourceLinker.Models.ITest.csproj │ └── DynamicResourceSerializerTests.cs ├── Fancy.ResourceLinker.Models.UTest │ ├── Usings.cs │ ├── ResourceAction.cs │ └── Fancy.ResourceLinker.Models.UTest.csproj ├── Fancy.ResourceLinker.Models │ ├── DynamicResource.cs │ ├── README.md │ ├── ResourceLink.cs │ ├── Json │ │ ├── JsonSerializerOptionsExtensions.cs │ │ ├── ResourceJsonConverterFactory.cs │ │ └── ResourceJsonConverter.cs │ ├── Fancy.ResourceLinker.Models.csproj │ ├── ResourceAction.cs │ ├── ResourceSocket.cs │ ├── DynamicResourceEnumerator.cs │ ├── IResource.cs │ └── ResourceBase.cs ├── Fancy.ResourceLinker.Gateway │ ├── README.md │ ├── Common │ │ ├── GatewayCommon.cs │ │ └── DiscoveryDocumentService.cs │ ├── GatewaySettings.cs │ ├── Authentication │ │ ├── DiscoveryDocument.cs │ │ ├── TokenServiceExceptions.cs │ │ ├── ITokenStore.cs │ │ ├── TokenCleanupBackgroundService.cs │ │ ├── TokenRecord.cs │ │ ├── GatewayAuthenticationEndpoints.cs │ │ ├── InMemoryTokenStore.cs │ │ ├── GatewayAuthenticationSettings.cs │ │ ├── TokenClient.cs │ │ └── TokenService.cs │ ├── Routing │ │ ├── Util │ │ │ └── HttpRequestHeadersExtensions.cs │ │ ├── IResourceCache.cs │ │ ├── GatewayPipeline.cs │ │ ├── Auth │ │ │ ├── IRouteAuthenticationStrategy.cs │ │ │ ├── NoAuthenticationAuthStrategy.cs │ │ │ ├── Auth0ClientCredentialsAuthStrategy.cs │ │ │ ├── TokenPassThroughAuthStrategy.cs │ │ │ ├── EnsureAuthenticatedAuthStrategy.cs │ │ │ ├── RouteAuthenticationManager.cs │ │ │ └── AzureOnBehalfOfAuthStrategy.cs │ │ ├── GatewayForwarderHttpTransformer.cs │ │ ├── InMemoryResourceCache.cs │ │ ├── GatewayRoutingSettings.cs │ │ └── GatewayRouting.cs │ ├── Fancy.ResourceLinker.Gateway.csproj │ ├── WebApplicationExtensions.cs │ ├── AntiForgery │ │ └── GatewayAntiForgery.cs │ └── ServiceCollectionExtensions.cs ├── Fancy.ResourceLinker.Hateoas │ ├── README.md │ ├── ILinkStrategy.cs │ ├── IResourceLinker.cs │ ├── Fancy.ResourceLinker.Hateoas.csproj │ ├── HypermediaController.cs │ ├── LinkStrategyBase.cs │ ├── ControllerExtensions.cs │ ├── IMvcBuilderExtensions.cs │ ├── ResourceLinker.cs │ └── UrlHelperExtensions.cs └── Fancy.ResourceLinker.Gateway.EntityFrameworkCore │ ├── README.md │ ├── GatewayDbContext.cs │ ├── Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj │ ├── TokenSet.cs │ ├── ServiceCollectionExtensions.cs │ └── DbTokenStore.cs ├── architecture.drawio.png ├── copyToLocalFeed.bat ├── .github └── workflows │ ├── Fancy.ResourceLinker.Gateway.yaml │ ├── Fancy.ResourceLinker.Hateoas.yaml │ ├── Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml │ └── Fancy.ResourceLinker.Models.yaml ├── .gitattributes ├── .gitignore ├── Fancy.ResourceLinker.sln ├── README.md └── doc └── features └── authentication.md /src/Fancy.ResourceLinker.Models.ITest/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models.UTest/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /architecture.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielmurrmann/Fancy.ResourceLinker/HEAD/architecture.drawio.png -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/DynamicResource.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Models; 2 | 3 | /// 4 | /// A resource type for a complete dynamic resource without any static properties. 5 | /// 6 | /// 7 | /// Can be used to create ad hoc resources dynamically at run time. 8 | /// 9 | public sealed class DynamicResource : DynamicResourceBase 10 | { } -------------------------------------------------------------------------------- /copyToLocalFeed.bat: -------------------------------------------------------------------------------- 1 | dotnet pack .\src\Fancy.ResourceLinker.Models\ -c debug 2 | xcopy .\src\Fancy.ResourceLinker.Models\bin\Debug\*.nupkg ..\..\Packages /Y 3 | 4 | dotnet pack .\src\Fancy.ResourceLinker.Gateway\ -c debug 5 | xcopy .\src\Fancy.ResourceLinker.Gateway\bin\Debug\*.nupkg ..\..\Packages /Y 6 | 7 | dotnet pack .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\ -c debug 8 | xcopy .\src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\bin\Debug\*.nupkg ..\..\Packages /Y -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/README.md: -------------------------------------------------------------------------------- 1 | # Fancy.ResourceLinker.Models 2 | 3 | This library is part of the **Fancy.ResourceLinker** project. 4 | 5 | A set of libraries to easily create API Gateways and Backend for Frontends (BFF) based on ASP.NET Core with truly RESTful web apis. 6 | 7 | Please find more information at the official GitHub repository: [https://github.com/fancyDevelopment/Fancy.ResourceLinker](https://github.com/fancyDevelopment/Fancy.ResourceLinker) 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/README.md: -------------------------------------------------------------------------------- 1 | # Fancy.ResourceLinker.Gateway 2 | 3 | This library is part of the **Fancy.ResourceLinker** project. 4 | 5 | A set of libraries to easily create API Gateways and Backend for Frontends (BFF) based on ASP.NET Core with truly RESTful web apis. 6 | 7 | Please find more information at the official GitHub repository: [https://github.com/fancyDevelopment/Fancy.ResourceLinker](https://github.com/fancyDevelopment/Fancy.ResourceLinker) 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/README.md: -------------------------------------------------------------------------------- 1 | # Fancy.ResourceLinker.Hateoas 2 | 3 | This library is part of the **Fancy.ResourceLinker** project. 4 | 5 | A set of libraries to easily create API Gateways and Backend for Frontends (BFF) based on ASP.NET Core with truly RESTful web apis. 6 | 7 | Please find more information at the official GitHub repository: [https://github.com/fancyDevelopment/Fancy.ResourceLinker](https://github.com/fancyDevelopment/Fancy.ResourceLinker) 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/README.md: -------------------------------------------------------------------------------- 1 | # Fancy.ResourceLinker.Gateway.EntityFrameworkCore 2 | 3 | This library is part of the **Fancy.ResourceLinker** project. 4 | 5 | A set of libraries to easily create API Gateways and Backend for Frontends (BFF) based on ASP.NET Core with truly RESTful web apis. 6 | 7 | Please find more information at the official GitHub repository: [https://github.com/fancyDevelopment/Fancy.ResourceLinker](https://github.com/fancyDevelopment/Fancy.ResourceLinker) 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Common/GatewayCommon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Common; 4 | 5 | /// 6 | /// Class with helper methods to set up the common services. 7 | /// 8 | internal class GatewayCommon 9 | { 10 | /// 11 | /// Adds the commmon gateway services. 12 | /// 13 | /// The services. 14 | internal static void AddGatewayCommonServices(IServiceCollection services) 15 | { 16 | services.AddSingleton(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/ResourceLink.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Models; 2 | 3 | /// 4 | /// Contains information regarding a link which can be performed onto a resource. 5 | /// 6 | public class ResourceLink 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The href. 12 | public ResourceLink(string href) 13 | { 14 | Href = href; 15 | } 16 | 17 | /// 18 | /// Gets or sets the destination URL of the link. 19 | /// 20 | /// 21 | /// The href. 22 | /// 23 | public string Href { get; set; } 24 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models.UTest/ResourceAction.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Models.UTest; 2 | 3 | [TestClass] 4 | public class ResourceActionTests 5 | { 6 | [TestMethod] 7 | public void Constructor_WithValidArguments_CreatesInstance() 8 | { 9 | var target = new ResourceAction("PUT", "http://foo.bar/baz"); 10 | Assert.IsNotNull(target); 11 | target = new ResourceAction("POST", "http://foo.bar/baz"); 12 | Assert.IsNotNull(target); 13 | target = new ResourceAction("DELETE", "http://foo.bar/baz"); 14 | Assert.IsNotNull(target); 15 | } 16 | 17 | [TestMethod] 18 | [ExpectedException(typeof(ArgumentException))] 19 | public void Constructor_WithGETVerb_ThrowsException() 20 | { 21 | var target = new ResourceAction("GET", "http://foo.bar/baz"); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/GatewaySettings.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Fancy.ResourceLinker.Gateway.Routing; 3 | 4 | namespace Fancy.ResourceLinker.Gateway; 5 | 6 | /// 7 | /// A class to describe the required settings for setting up a gateway. 8 | /// 9 | public class GatewaySettings 10 | { 11 | /// 12 | /// Gets or sets the authentication settings. 13 | /// 14 | /// 15 | /// The authentication settings. 16 | /// 17 | public GatewayAuthenticationSettings? Authentication { get; set; } 18 | 19 | /// 20 | /// Gets or sets the routing settings. 21 | /// 22 | /// 23 | /// The routing settings. 24 | /// 25 | public GatewayRoutingSettings? Routing { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/DiscoveryDocument.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Authentication; 4 | 5 | /// 6 | /// A class to describe a discovery document 7 | /// 8 | public class DiscoveryDocument 9 | { 10 | /// 11 | /// Gets or sets the token endpoint. 12 | /// 13 | /// 14 | /// The token endpoint. 15 | /// 16 | [JsonPropertyName("token_endpoint")] 17 | public string TokenEndpoint { get; set; } = ""; 18 | 19 | /// 20 | /// Gets or sets the userinfo endpoint. 21 | /// 22 | /// 23 | /// The userinfo endpoint. 24 | /// 25 | [JsonPropertyName("userinfo_endpoint")] 26 | public string UserinfoEndpoint { get; set; } = ""; 27 | } 28 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Util/HttpRequestHeadersExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Routing.Util; 4 | 5 | internal static class HttpRequestHeadersExtensions 6 | { 7 | /// 8 | /// Sets the X-Forwarded headers. 9 | /// 10 | /// The headers. 11 | /// The origin to set the headers for. 12 | public static void SetForwardedHeaders(this HttpRequestHeaders headers, string? origin) 13 | { 14 | if (origin != null) 15 | { 16 | string[] proxyParts = origin.Split("://"); 17 | string proto = proxyParts[0]; 18 | string host = proxyParts[1]; 19 | headers.Add("X-Forwarded-Proto", proto); 20 | headers.Add("X-Forwarded-Host", host); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/Json/JsonSerializerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Fancy.ResourceLinker.Models.Json; 4 | 5 | /// 6 | /// Extensions for JsonSerializerOptions 7 | /// 8 | public static class JsonSerializerOptionsExtensions 9 | { 10 | /// 11 | /// Adds the resource converter an instance of JsonSerializerOptions. 12 | /// 13 | /// The options. 14 | /// if set to true ignores empty metadata fields. 15 | /// Specifies is fields which starts with '_' shall be read an written. 16 | public static void AddResourceConverter(this JsonSerializerOptions options, bool ignoreEmptyMetadata = true, bool writePrivates = true) 17 | { 18 | options.Converters.Add(new ResourceJsonConverterFactory(writePrivates, ignoreEmptyMetadata)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/TokenServiceExceptions.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Gateway.Authentication; 2 | 3 | /// 4 | /// A base class for custom token service exceptions. 5 | /// 6 | internal abstract class TokenServiceException : Exception {} 7 | 8 | /// 9 | /// Custom exception type to indicate that a session id is required but is not available. 10 | /// 11 | internal class NoSessionIdException : TokenServiceException 12 | { 13 | } 14 | 15 | /// 16 | /// Custom exception type to indicate that there is a valid session but no token is available for this session. 17 | /// 18 | /// 19 | internal class NoTokenForCurrentSessionIdException : TokenServiceException 20 | { 21 | } 22 | 23 | /// 24 | /// Custom exception type to indicate that something with the token refresh went wrong. 25 | /// 26 | internal class TokenRefreshException : TokenServiceException 27 | { 28 | } 29 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models.ITest/Fancy.ResourceLinker.Models.ITest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models.UTest/Fancy.ResourceLinker.Models.UTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/Fancy.ResourceLinker.Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.1.0 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | Fancy Resource Linker Gateway 13 | A library to work with dynamic object wich can be serialized and deserialized to and from json. 14 | fancy Development - Daniel Murrmann 15 | Copyright 2015-2025 fancyDevelopment - Daniel Murrmann 16 | Apache-2.0 17 | https://github.com/fancyDevelopment/Fancy.ResourceLinker 18 | README.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/ILinkStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Fancy.ResourceLinker.Hateoas; 5 | 6 | /// 7 | /// Interface for a link strategy. 8 | /// 9 | /// 10 | /// A link strategy is responsible to link a specific type of a data transfer object or view model. 11 | /// 12 | public interface ILinkStrategy 13 | { 14 | /// 15 | /// Determines whether this instance can link the specified type. 16 | /// 17 | /// The type. 18 | /// True, if this instance can link the specified type; otherwise, false. 19 | bool CanLinkType(Type type); 20 | 21 | /// 22 | /// Links the resource to endpoints. 23 | /// 24 | /// The resource to link. 25 | /// The URL helper. 26 | /// Resource as wrong type;resource 27 | void LinkResource(IResource resource, IUrlHelper urlHelper); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/Fancy.ResourceLinker.Gateway.yaml: -------------------------------------------------------------------------------- 1 | name: Fancy.ResourceLinker.Gateway 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/Fancy.ResourceLinker.Gateway/**' 7 | - 'src/Fancy.ResourceLinker.Gateway.UTest/**' 8 | - 'src/Fancy.ResourceLinker.Gateway.ITest/**' 9 | - '.github/workflows/Fancy.ResourceLinker.Gateway.yaml' 10 | pull_request: 11 | paths: 12 | - 'src/Fancy.ResourceLinker.Gateway/**' 13 | - 'src/Fancy.ResourceLinker.Gateway.UTest/**' 14 | - 'src/Fancy.ResourceLinker.Gateway.ITest/**' 15 | - '.github/workflows/Fancy.ResourceLinker.Gateway.yaml' 16 | 17 | jobs: 18 | buid: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Checkout repository 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | # Install the .NET Core workload 26 | - name: Install .NET Core 27 | uses: actions/setup-dotnet@v3 28 | with: 29 | dotnet-version: 8.0.x 30 | 31 | # Build library 32 | - name: Build library 33 | run: | 34 | cd ./src/Fancy.ResourceLinker.Gateway/ 35 | dotnet build 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/Fancy.ResourceLinker.Hateoas.yaml: -------------------------------------------------------------------------------- 1 | name: Fancy.ResourceLinker.Hateoas 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/Fancy.ResourceLinker.Hateoas/**' 7 | - 'src/Fancy.ResourceLinker.Hateoas.UTest/**' 8 | - 'src/Fancy.ResourceLinker.Hateoas.ITest/**' 9 | - '.github/workflows/Fancy.ResourceLinker.Hateoas.yaml' 10 | pull_request: 11 | paths: 12 | - 'src/Fancy.ResourceLinker.Hateoas/**' 13 | - 'src/Fancy.ResourceLinker.Hateoas.UTest/**' 14 | - 'src/Fancy.ResourceLinker.Hateoas.ITest/**' 15 | - '.github/workflows/Fancy.ResourceLinker.Hateoas.yaml' 16 | 17 | jobs: 18 | buid: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Checkout repository 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | # Install the .NET Core workload 26 | - name: Install .NET Core 27 | uses: actions/setup-dotnet@v3 28 | with: 29 | dotnet-version: 8.0.x 30 | 31 | # Build library 32 | - name: Build library 33 | run: | 34 | cd ./src/Fancy.ResourceLinker.Hateoas/ 35 | dotnet build 36 | 37 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/GatewayDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; 5 | 6 | /// 7 | /// A database context to hold all information needed to be persisted in a gateway. 8 | /// 9 | public abstract class GatewayDbContext : DbContext, IDataProtectionKeyContext 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The options. 15 | public GatewayDbContext(DbContextOptions options) : base(options) { } 16 | 17 | /// 18 | /// Gets or sets the data protection keys. 19 | /// 20 | /// 21 | /// The data protection keys. 22 | /// 23 | public DbSet DataProtectionKeys { get; set; } = null!; 24 | 25 | /// 26 | /// Gets or sets the token sets. 27 | /// 28 | /// 29 | /// The token sets. 30 | /// 31 | public DbSet TokenSets { get; set; } = null!; 32 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/IResourceLinker.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Fancy.ResourceLinker.Hateoas; 5 | 6 | /// 7 | /// Interface for a service which can be used to link resources. 8 | /// 9 | public interface IResourceLinker 10 | { 11 | /// 12 | /// Adds links to a resource using a link strategy. 13 | /// 14 | /// The type of resource to add links to. 15 | /// The resource to add links to. 16 | /// The URL helper. 17 | void AddLinks(TResource resource, IUrlHelper urlHelper) where TResource : IResource; 18 | 19 | /// 20 | /// Adds links to a collection of resources using a link strategy. 21 | /// 22 | /// The type of resource to add links to. 23 | /// The resources to add links to. 24 | /// The URL helper. 25 | void AddLinks(IEnumerable resources, IUrlHelper urlHelper) where TResource : IResource; 26 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/ResourceAction.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Models; 2 | 3 | /// 4 | /// Contains information regarding an action which can be performed onto a resource. 5 | /// 6 | public class ResourceAction 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The method. 12 | /// The href. 13 | public ResourceAction(string method, string href) 14 | { 15 | if(method.Trim().ToLower() == "get") 16 | { 17 | throw new ArgumentException("An action may not have the HTTP Verb GET", "method"); 18 | } 19 | 20 | Method = method; 21 | Href = href; 22 | } 23 | 24 | /// 25 | /// Gets or sets the HTTP method to use for this action. 26 | /// 27 | /// 28 | /// The method. 29 | /// 30 | public string Method { get; set; } 31 | 32 | /// 33 | /// Gets or sets the destination URL of the action. 34 | /// 35 | /// 36 | /// The href. 37 | /// 38 | public string Href { get; set; } 39 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/Fancy.ResourceLinker.Hateoas.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.1.0 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | Fancy Resource Linker Gateway 13 | A library to create HATEOAS like links to resources published by ASP.NET Core.. 14 | fancy Development - Daniel Murrmann 15 | Copyright 2015-2025 fancyDevelopment - Daniel Murrmann 16 | Apache-2.0 17 | https://github.com/fancyDevelopment/Fancy.ResourceLinker 18 | README.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml: -------------------------------------------------------------------------------- 1 | name: Fancy.ResourceLinker.Gateway.EntityFrameworkCore 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/**' 7 | - 'src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.UTest/**' 8 | - 'src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.ITest/**' 9 | - '.github/workflows/Fancy.ResourceLinker.Gateway.yaml' 10 | pull_request: 11 | paths: 12 | - 'src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/**' 13 | - 'src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.UTest/**' 14 | - 'src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.ITest/**' 15 | - '.github/workflows/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml' 16 | 17 | jobs: 18 | buid: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Checkout repository 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | # Install the .NET Core workload 26 | - name: Install .NET Core 27 | uses: actions/setup-dotnet@v3 28 | with: 29 | dotnet-version: 8.0.x 30 | 31 | # Build library 32 | - name: Build library 33 | run: | 34 | cd ./src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/ 35 | dotnet build 36 | 37 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/IResourceCache.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Routing; 4 | 5 | /// 6 | /// Interface for a service which can be used to cache resources. 7 | /// 8 | public interface IResourceCache 9 | { 10 | /// 11 | /// Writes a resource with the specified key to the cache. 12 | /// 13 | /// The type of the resource. 14 | /// The key to save the resource under. 15 | /// The resource instance to save. 16 | void Write(string key, TResource resource) where TResource : class; 17 | 18 | /// 19 | /// Tries to read a resource from the cache. 20 | /// 21 | /// The type of the resource. 22 | /// The key of the resource to get. 23 | /// The maximum age of the resource. 24 | /// The resource. 25 | /// True if the cache was able to read and provide a valid resource instance; otherwise, false. 26 | bool TryRead(string key, TimeSpan maxResourceAge, out TResource? resource) where TResource : class; 27 | } 28 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/HypermediaController.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Fancy.ResourceLinker.Hateoas; 5 | 6 | /// 7 | /// Controller base class for HATEOAS controllers with helper methods. 8 | /// 9 | /// 10 | public class HypermediaController : ControllerBase 11 | { 12 | /// 13 | /// Helper method to return a hypermedia result for the specified content. 14 | /// 15 | /// The type of the resource. 16 | /// The content. 17 | /// A linked object result. 18 | public virtual IActionResult Hypermedia(TResource content) where TResource : IResource 19 | { 20 | this.LinkResource(content); 21 | return new ObjectResult(content); 22 | } 23 | 24 | /// 25 | /// Helper method to return a hypermedia result for the specified list of content elements. 26 | /// 27 | /// The type of the resource. 28 | /// The content. 29 | /// A linked object result. 30 | public virtual IActionResult Hypermedia(IEnumerable content) where TResource : IResource 31 | { 32 | this.LinkResources(content); 33 | return new ObjectResult(content); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/Fancy.ResourceLinker.Models.yaml: -------------------------------------------------------------------------------- 1 | name: Fancy.ResourceLinker.Models 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/Fancy.ResourceLinker.Models/**' 7 | - 'src/Fancy.ResourceLinker.Models.UTest/**' 8 | - 'src/Fancy.ResourceLinker.Models.ITest/**' 9 | - '.github/workflows/Fancy.ResourceLinker.Models.yaml' 10 | pull_request: 11 | paths: 12 | - 'src/Fancy.ResourceLinker.Models/**' 13 | - 'src/Fancy.ResourceLinker.Models.UTest/**' 14 | - 'src/Fancy.ResourceLinker.Models.ITest/**' 15 | - '.github/workflows/Fancy.ResourceLinker.Models.yaml' 16 | 17 | jobs: 18 | buid: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Checkout repository 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | # Install the .NET Core workload 26 | - name: Install .NET Core 27 | uses: actions/setup-dotnet@v3 28 | with: 29 | dotnet-version: 8.0.x 30 | 31 | # Build library 32 | - name: Build library 33 | run: | 34 | cd ./src/Fancy.ResourceLinker.Models/ 35 | dotnet build 36 | 37 | # Execute all unit tests 38 | - name: Execute unit tests 39 | run: | 40 | cd ./src/Fancy.ResourceLinker.Models.UTest/ 41 | dotnet test 42 | 43 | # Execute all integration tests 44 | - name: Execute integration tests 45 | run: | 46 | cd ./src/Fancy.ResourceLinker.Models.ITest/ 47 | dotnet test 48 | 49 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Fancy.ResourceLinker.Gateway.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.1.0 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | Fancy Resource Linker Gateway 13 | A library to create API Gateways with dynamic data structures on top of ASP.NET Core. 14 | fancy Development - Daniel Murrmann 15 | Copyright 2015-2025 fancyDevelopment - Daniel Murrmann 16 | Apache-2.0 17 | https://github.com/fancyDevelopment/Fancy.ResourceLinker 18 | README.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/GatewayPipeline.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Routing.Auth; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Fancy.ResourceLinker.Gateway.Routing; 7 | 8 | /// 9 | /// Helper class to set up the pipeline for the yarp proxy. 10 | /// 11 | internal static class GatewayPipeline 12 | { 13 | /// 14 | /// Adds required middleware to the yarp proxy pipeline. 15 | /// 16 | /// The pipeline. 17 | internal static void UseGatewayPipeline(this IReverseProxyApplicationBuilder pipeline) 18 | { 19 | pipeline.Use(async (context, next) => 20 | { 21 | // Check if token shall be added 22 | var proxyFeature = context.GetReverseProxyFeature(); 23 | if (proxyFeature.Route.Config.Metadata != null 24 | && proxyFeature.Route.Config.Metadata.ContainsKey("RouteName")) 25 | { 26 | string routeName = proxyFeature.Route.Config.Metadata["RouteName"]; 27 | RouteAuthenticationManager authStrategyFactory = context.RequestServices.GetRequiredService(); 28 | IRouteAuthenticationStrategy authStrategy = await authStrategyFactory.GetAuthStrategyAsync(routeName); 29 | await authStrategy.SetAuthenticationAsync(context); 30 | } 31 | await next().ConfigureAwait(false); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/ResourceSocket.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Models; 2 | 3 | /// 4 | /// Contains information regarding a socket which can be used to implement server to client messaging onto a resource. 5 | /// 6 | public class ResourceSocket 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The href. 12 | /// The method. 13 | public ResourceSocket(string href, string method) 14 | { 15 | Href = href; 16 | Method = method; 17 | Token = null; 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The href. 24 | /// The method. 25 | /// The token. 26 | public ResourceSocket(string href, string method, string token) 27 | { 28 | Href = href; 29 | Method = method; 30 | Token = token; 31 | } 32 | 33 | /// 34 | /// Gets or sets the hub URL. 35 | /// 36 | /// 37 | /// The hub URL. 38 | /// 39 | public string Href { get; set; } 40 | 41 | /// 42 | /// Gets the method. 43 | /// 44 | /// 45 | /// The method. 46 | /// 47 | public string Method { get; } 48 | 49 | /// 50 | /// Gets or sets the token. 51 | /// 52 | /// 53 | /// The token. 54 | /// 55 | public string? Token { get; set; } 56 | } 57 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Common/DiscoveryDocumentService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using Fancy.ResourceLinker.Gateway.Authentication; 3 | 4 | namespace Fancy.ResourceLinker.Gateway.Common; 5 | 6 | /// 7 | /// A service to retrieve the discovery document from an authorization server. 8 | /// 9 | public class DiscoveryDocumentService 10 | { 11 | /// 12 | /// The well known discovery URL 13 | /// 14 | private const string DISCOVERY_URL = "/.well-known/openid-configuration"; 15 | 16 | /// 17 | /// Loads a discovery document asynchronous. 18 | /// 19 | /// The authority URL. 20 | /// An instance of a class representing the discovery document. 21 | public async Task LoadDiscoveryDocumentAsync(string authorityUrl) 22 | { 23 | HttpClient httpClient = new HttpClient(); 24 | 25 | string url = CombineUrls(authorityUrl, DISCOVERY_URL); 26 | 27 | DiscoveryDocument? result = await httpClient.GetFromJsonAsync(url); 28 | 29 | if (result == null) 30 | { 31 | throw new Exception("Error loading discovery document from " + url); 32 | } 33 | 34 | return result; 35 | } 36 | 37 | /// 38 | /// Combines the urls. 39 | /// 40 | /// The uri1. 41 | /// The uri2. 42 | /// The combinde URL. 43 | private string CombineUrls(string uri1, string uri2) 44 | { 45 | uri1 = uri1.TrimEnd('/'); 46 | uri2 = uri2.TrimStart('/'); 47 | return string.Format("{0}/{1}", uri1, uri2); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/LinkStrategyBase.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Fancy.ResourceLinker.Hateoas; 5 | 6 | /// 7 | /// Base implementatio of a link strategy. 8 | /// 9 | /// The type of the resource this strategy links. 10 | /// 11 | public abstract class LinkStrategyBase : ILinkStrategy where T : class 12 | { 13 | /// 14 | /// Determines whether this instance can link the specified type. 15 | /// 16 | /// The type. 17 | /// 18 | /// True, if this instance can link the specified type; otherwise, false. 19 | /// 20 | public bool CanLinkType(Type type) 21 | { 22 | return typeof (T) == type; 23 | } 24 | 25 | /// 26 | /// Links the resource to endpoints. 27 | /// 28 | /// The resource to link. 29 | /// The URL helper. 30 | public void LinkResource(IResource resource, IUrlHelper urlHelper) 31 | { 32 | T? typedResource = resource as T; 33 | 34 | if (typedResource == null) 35 | { 36 | throw new InvalidOperationException("Could not cast resource of type " + resource.GetType().Name + " to type " + typeof(T).Name); 37 | } 38 | 39 | LinkResourceInternal(typedResource, urlHelper); 40 | } 41 | 42 | /// 43 | /// Links the resource internal. 44 | /// 45 | /// The resource. 46 | /// The URL helper. 47 | protected abstract void LinkResourceInternal(T resource, IUrlHelper urlHelper); 48 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/ITokenStore.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Gateway.Authentication; 2 | 3 | /// 4 | /// Interface to a token store. 5 | /// 6 | public interface ITokenStore 7 | { 8 | /// 9 | /// Saves the or update tokens asynchronous. 10 | /// 11 | /// The session identifier. 12 | /// The identifier token. 13 | /// The access token. 14 | /// The refresh token. 15 | /// The expires at. 16 | /// A task indicating the completion of the asynchronous operation. 17 | Task SaveOrUpdateTokensAsync(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiresAt); 18 | 19 | /// 20 | /// Saves the or update userinfo claims asynchronous. 21 | /// 22 | /// The session identifier. 23 | /// The userinfo object as json string. 24 | /// 25 | /// A task indicating the completion of the asynchronous operation. 26 | /// 27 | Task SaveOrUpdateUserinfoClaimsAsync(string sessionId, string userinfoClaims); 28 | 29 | /// 30 | /// Gets the token record for a provided session asynchronous. 31 | /// 32 | /// The session identifier. 33 | /// 34 | /// A token record if available. 35 | /// 36 | Task GetTokenRecordAsync(string sessionId); 37 | 38 | /// 39 | /// Cleans up the expired token records asynchronous. 40 | /// 41 | /// A task indicating the completion of the asynchronous operation. 42 | Task CleanupExpiredTokenRecordsAsync(); 43 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/IRouteAuthenticationStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 5 | 6 | /// 7 | /// Interface for an authorization strategy. 8 | /// 9 | public interface IRouteAuthenticationStrategy 10 | { 11 | /// 12 | /// Gets the name of the strategy. 13 | /// 14 | /// 15 | /// The name. 16 | /// 17 | string Name { get; } 18 | 19 | /// 20 | /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. 21 | /// 22 | /// The gateway authentication settigns. 23 | /// The route authentication settigns. 24 | /// 25 | /// A task indicating the completion of the asynchronous operation. 26 | /// 27 | Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings); 28 | 29 | /// 30 | /// Sets the authentication to an http context asynchronous. 31 | /// 32 | /// The http context. 33 | /// A task indicating the completion of the asynchronous operation 34 | Task SetAuthenticationAsync(HttpContext context); 35 | 36 | /// 37 | /// Sets the authentication to an http request message asynchronous. 38 | /// 39 | /// The current service provider. 40 | /// The http request message. 41 | /// 42 | /// A task indicating the completion of the asynchronous operation 43 | /// 44 | Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request); 45 | } 46 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/NoAuthenticationAuthStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 5 | 6 | /// 7 | /// An authentication strategy which does not set any authentication. 8 | /// 9 | public class NoAuthenticationAuthStrategy : IRouteAuthenticationStrategy 10 | { 11 | /// 12 | /// The name of the auth strategy 13 | /// 14 | public const string NAME = "NoAuthentication"; 15 | 16 | /// 17 | /// Gets the name of the strategy. 18 | /// 19 | /// 20 | /// The name. 21 | /// 22 | public string Name => NAME; 23 | 24 | public Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) 25 | { 26 | return Task.CompletedTask; 27 | } 28 | 29 | 30 | /// 31 | /// Sets the authentication to an http context asynchronous. 32 | /// 33 | /// The http context. 34 | /// 35 | /// A task indicating the completion of the asynchronous operation 36 | /// 37 | public Task SetAuthenticationAsync(HttpContext context) 38 | { 39 | // Nothing to do here! 40 | return Task.CompletedTask; 41 | } 42 | 43 | /// 44 | /// Sets the authentication to an http request message asynchronous. 45 | /// 46 | /// The current service provider. 47 | /// The http request message. 48 | /// 49 | /// A task indicating the completion of the asynchronous operation 50 | /// 51 | public Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) 52 | { 53 | // Nothing to do here! 54 | return Task.CompletedTask; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.1.0 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | Fancy Resource Linker Gateway 13 | Extends the Fancy.ResourceLinker.Gateway package with features to save shared resources for all instances to database. 14 | fancy Development - Daniel Murrmann 15 | Copyright 2015-2025 fancyDevelopment - Daniel Murrmann 16 | Apache-2.0 17 | https://github.com/fancyDevelopment/Fancy.ResourceLinker 18 | README.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/GatewayForwarderHttpTransformer.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Routing.Auth; 2 | using Fancy.ResourceLinker.Gateway.Routing.Util; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Yarp.ReverseProxy.Forwarder; 6 | 7 | namespace Fancy.ResourceLinker.Gateway.Routing; 8 | 9 | /// 10 | /// A http transformer used in combination with the http forwarder to add token if necessary. 11 | /// 12 | /// 13 | internal class GatewayForwarderHttpTransformer : HttpTransformer 14 | { 15 | /// 16 | /// The send access token item key. 17 | /// 18 | internal static readonly string RouteNameItemKey = "RouteNameItemKey"; 19 | 20 | /// 21 | /// The resource proxy item key. 22 | /// 23 | internal static readonly string ResourceProxyItemKey = "ResourceProxyItemKey"; 24 | 25 | public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) 26 | { 27 | await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); 28 | 29 | string? routeName = httpContext.Items[RouteNameItemKey]?.ToString(); 30 | string? resourceProxy = httpContext.Items[ResourceProxyItemKey]?.ToString(); 31 | 32 | if (!string.IsNullOrEmpty(routeName)) 33 | { 34 | // Add authentication 35 | RouteAuthenticationManager routeAuthenticationManager = httpContext.RequestServices.GetRequiredService(); 36 | IRouteAuthenticationStrategy authStrategy = await routeAuthenticationManager.GetAuthStrategyAsync(routeName); 37 | await authStrategy.SetAuthenticationAsync(httpContext.RequestServices, proxyRequest); 38 | } 39 | 40 | proxyRequest.RequestUri = new Uri(destinationPrefix); 41 | proxyRequest.Headers.SetForwardedHeaders(resourceProxy); 42 | 43 | // Suppress the original request header, use the one from the destination Uri. 44 | proxyRequest.Headers.Host = null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/ControllerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Fancy.ResourceLinker.Hateoas; 5 | 6 | /// 7 | /// Class with extension methods for a controller to link its resources to related resources. 8 | /// 9 | public static class ControllerExtensions 10 | { 11 | /// 12 | /// Links a resource to other resources by using a link strategy. 13 | /// 14 | /// The type of resource to link. 15 | /// The controller responding to a http call. 16 | /// The resource to link. 17 | public static void LinkResource(this ControllerBase controller, TResource resource) where TResource : IResource 18 | { 19 | IResourceLinker? resourceLinker = controller.HttpContext.RequestServices.GetService(typeof (IResourceLinker)) as IResourceLinker; 20 | 21 | if (resourceLinker == null) 22 | { 23 | throw new InvalidOperationException("No resource linker was found in the ioc container. Register a class implementing the IResourceLinker interface into the ioc container."); 24 | } 25 | 26 | resourceLinker.AddLinks(resource, controller.Url); 27 | } 28 | 29 | /// 30 | /// Links resources to other resources by using a link strategy. 31 | /// 32 | /// The type of resource to link. 33 | /// The controller responding to a http call. 34 | /// The resources to link. 35 | public static void LinkResources(this ControllerBase controller, IEnumerable resources) where TResource : IResource 36 | { 37 | IResourceLinker? resourceLinker = controller.HttpContext.RequestServices.GetService(typeof(IResourceLinker)) as IResourceLinker; 38 | 39 | if (resourceLinker == null) 40 | { 41 | throw new InvalidOperationException("No resource linker was found in the ioc container. Register a class implementing the IResourceLinker interface into the ioc container."); 42 | } 43 | 44 | resourceLinker.AddLinks(resources, controller.Url); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/TokenSet.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; 4 | 5 | /// 6 | /// A entity object to save token sets to database. 7 | /// 8 | public class TokenSet 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The session identifier. 14 | /// The identifier token. 15 | /// The access token. 16 | /// The refresh token. 17 | /// The expires at. 18 | public TokenSet(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiresAt) 19 | { 20 | SessionId = sessionId; 21 | IdToken = idToken; 22 | AccessToken = accessToken; 23 | RefreshToken = refreshToken; 24 | ExpiresAt = expiresAt; 25 | } 26 | 27 | /// 28 | /// Gets or sets the session identifier. 29 | /// 30 | /// 31 | /// The session identifier. 32 | /// 33 | [Key] 34 | [MaxLength(40)] 35 | public string SessionId { get; set; } 36 | 37 | /// 38 | /// Gets or sets the identifier token. 39 | /// 40 | /// 41 | /// The identifier token. 42 | /// 43 | public string IdToken { get; set; } 44 | 45 | /// 46 | /// Gets or sets the access token. 47 | /// 48 | /// 49 | /// The access token. 50 | /// 51 | public string AccessToken { get; set; } 52 | 53 | /// 54 | /// Gets or sets the refresh token. 55 | /// 56 | /// 57 | /// The refresh token. 58 | /// 59 | public string RefreshToken { get; set; } 60 | 61 | /// 62 | /// Gets or sets the userinfo claims. 63 | /// 64 | /// 65 | /// The userinfo claims. 66 | /// 67 | public string? UserinfoClaims { get; set; } 68 | 69 | /// 70 | /// Gets or sets the expires at. 71 | /// 72 | /// 73 | /// The expires at. 74 | /// 75 | public DateTime ExpiresAt { get; set; } 76 | } 77 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/InMemoryResourceCache.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Routing; 4 | 5 | /// 6 | /// Implements the interface with an im memory cache which caches the resources 7 | /// directly in the working memory. 8 | /// 9 | /// 10 | public class InMemoryResourceCache : IResourceCache 11 | { 12 | /// 13 | /// The cache dictonary which holds all keys and accoarding cache entries. 14 | /// 15 | private Dictionary> _cache = new Dictionary>(); 16 | 17 | /// 18 | /// Writes a resource with the specified key to the cache. 19 | /// 20 | /// The type of the resource. 21 | /// The key to save the resource under. 22 | /// The resource instance to save. 23 | public void Write(string key, TResource resource) where TResource : class 24 | { 25 | _cache[key] = new Tuple(DateTime.Now, resource); 26 | } 27 | 28 | /// 29 | /// Tries to read a resource from the cache. 30 | /// 31 | /// The type of the resource. 32 | /// The key of the resource to get. 33 | /// The maximum age of the resource. 34 | /// The resource. 35 | /// 36 | /// True if the cache was able to read and provide a valid resource instance; otherwise, false. 37 | /// 38 | public bool TryRead(string key, TimeSpan maxResourceAge, out TResource? resource) where TResource : class 39 | { 40 | resource = default; 41 | 42 | // Check if the key exists within the cache 43 | if (_cache.ContainsKey(key)) 44 | { 45 | var cacheEntry = _cache[key]; 46 | 47 | // Check if the item within the cahe is not too old 48 | if (DateTime.Now.Subtract(cacheEntry.Item1) < maxResourceAge) 49 | { 50 | resource = (TResource)cacheEntry.Item2; 51 | return true; 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/TokenCleanupBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Fancy.ResourceLinker.Gateway.Authentication 6 | { 7 | /// 8 | /// A background service to regulary clean up expired tokens from the database. 9 | /// 10 | internal class TokenCleanupBackgroundService : BackgroundService 11 | { 12 | /// 13 | /// The logger. 14 | /// 15 | private readonly ILogger _logger; 16 | 17 | /// 18 | /// The service provider. 19 | /// 20 | private readonly IServiceProvider _serviceProvider; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The service provider. 26 | public TokenCleanupBackgroundService(ILogger logger, IServiceProvider serviceProvider) 27 | { 28 | _logger = logger; 29 | _serviceProvider = serviceProvider; 30 | } 31 | 32 | /// 33 | /// This method is called when the starts. The implementation should return a task that represents 34 | /// the lifetime of the long running operation(s) being performed. 35 | /// 36 | /// Triggered when is called. 37 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 38 | { 39 | using PeriodicTimer timer = new(TimeSpan.FromMinutes(15)); 40 | using IServiceScope scope = _serviceProvider.CreateScope(); 41 | 42 | ITokenStore tokenStore = scope.ServiceProvider.GetRequiredService(); 43 | 44 | // Clean up once at startup 45 | await tokenStore.CleanupExpiredTokenRecordsAsync(); 46 | 47 | try 48 | { 49 | while (await timer.WaitForNextTickAsync(stoppingToken)) 50 | { 51 | await tokenStore.CleanupExpiredTokenRecordsAsync(); 52 | _logger.LogTrace("Cleaned up expired tokens"); 53 | } 54 | } 55 | catch (OperationCanceledException) 56 | { 57 | _logger.LogInformation("Timed Hosted Service is stopping."); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/DynamicResourceEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Fancy.ResourceLinker.Models; 4 | 5 | /// 6 | /// An enumerator to enumerator through all keys of a . 7 | /// 8 | public class DynamicResourceEnumerator : IEnumerator> 9 | { 10 | /// 11 | /// The resource to enumerate. 12 | /// 13 | private DynamicResourceBase _resource; 14 | 15 | /// 16 | /// The current element. 17 | /// 18 | private KeyValuePair _current; 19 | 20 | /// 21 | /// The current index. 22 | /// 23 | private int _currentIndex = -1; 24 | 25 | /// 26 | /// Gets the element in the collection at the current position of the enumerator. 27 | /// 28 | public KeyValuePair Current => _current; 29 | 30 | /// 31 | /// Gets the element in the collection at the current position of the enumerator. 32 | /// 33 | object IEnumerator.Current => _current; 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// The resorce. 39 | public DynamicResourceEnumerator(DynamicResourceBase resorce) 40 | { 41 | _resource = resorce; 42 | } 43 | 44 | /// 45 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 46 | /// 47 | public void Dispose() 48 | { 49 | } 50 | 51 | /// 52 | /// Advances the enumerator to the next element of the collection. 53 | /// 54 | /// 55 | /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. 56 | /// 57 | public bool MoveNext() 58 | { 59 | _currentIndex++; 60 | 61 | if(_currentIndex < _resource.Count) 62 | { 63 | SetCurrentIndex(); 64 | return true; 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /// 71 | /// Sets the enumerator to its initial position, which is before the first element in the collection. 72 | /// 73 | public void Reset() 74 | { 75 | _currentIndex = -1; 76 | SetCurrentIndex(); 77 | } 78 | 79 | /// 80 | /// Sets key value pair of the current index to the current element. 81 | /// 82 | private void SetCurrentIndex() 83 | { 84 | string key = _resource.Keys.ToList()[_currentIndex]; 85 | object? value = _resource[key]; 86 | 87 | _current = new KeyValuePair(key, value); 88 | } 89 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/Json/ResourceJsonConverterFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Fancy.ResourceLinker.Models.Json; 6 | 7 | /// 8 | /// A factory for the generic to create concrete typed convertes. 9 | /// 10 | /// 11 | public class ResourceJsonConverterFactory : JsonConverterFactory 12 | { 13 | /// 14 | /// The write privates. 15 | /// 16 | private readonly bool _writePrivates; 17 | 18 | /// 19 | /// The ignore empty metadata 20 | /// 21 | private readonly bool _ignoreEmptyMetadata; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// if set to true the convertes write private json fields. 27 | /// if set to true ignores empty metadata fields. 28 | public ResourceJsonConverterFactory(bool writePrivates, bool ignoreEmptyMetadata) 29 | { 30 | _writePrivates = writePrivates; 31 | _ignoreEmptyMetadata = ignoreEmptyMetadata; 32 | } 33 | 34 | /// 35 | /// Determines whether the converter instance can convert the specified object type. 36 | /// 37 | /// The type of the object to check whether it can be converted by this converter instance. 38 | /// 39 | /// if the instance can convert the specified object type; otherwise, . 40 | /// 41 | public override bool CanConvert(Type typeToConvert) 42 | { 43 | return typeToConvert.IsAssignableTo(typeof(IResource)); 44 | } 45 | 46 | /// 47 | /// Creates a converter for a specified type. 48 | /// 49 | /// The type handled by the converter. 50 | /// The serialization options to use. 51 | /// 52 | /// A converter for which is compatible with . 53 | /// 54 | public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 55 | { 56 | JsonConverter converter = (JsonConverter)Activator.CreateInstance( 57 | typeof(ResourceJsonConverter<>).MakeGenericType(new Type[] { typeToConvert }), 58 | BindingFlags.Instance | BindingFlags.Public, 59 | binder: null, 60 | args: new object[] { _writePrivates, _ignoreEmptyMetadata }, 61 | culture: null)!; 62 | 63 | return converter; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.AspNetCore.DataProtection; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; 6 | 7 | /// 8 | /// Extension class with helpers to easily register the gateway ef core to ioc container. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// A database context added flag. 14 | /// 15 | private static bool _dbContextAdded = false; 16 | 17 | /// 18 | /// Uses the provided database context in the gateway. 19 | /// 20 | /// The type of the database context. 21 | /// The builder. 22 | /// 23 | /// A type of a gateway builder. 24 | /// 25 | /// DbContext can be added only once 26 | public static GatewayBuilder UseDbContext(this GatewayBuilder builder) where TDbContext : GatewayDbContext 27 | { 28 | if(_dbContextAdded) 29 | { 30 | throw new InvalidOperationException("DbContext can be added only once"); 31 | } 32 | 33 | builder.Services.AddScoped(services => services.GetRequiredService()); 34 | 35 | _dbContextAdded = true; 36 | 37 | return builder; 38 | } 39 | 40 | /// 41 | /// Adds the database token store for the authentication feature. 42 | /// 43 | /// The gateway authentication builder. 44 | /// The gateway authentication builder. 45 | public static GatewayAuthenticationBuilder UseDbTokenStore(this GatewayAuthenticationBuilder builder) 46 | { 47 | if (!_dbContextAdded) 48 | { 49 | throw new InvalidOperationException("Call 'AddDbContext' before configuring options using the db context"); 50 | } 51 | 52 | builder.Services.AddScoped(); 53 | 54 | return builder; 55 | } 56 | 57 | /// 58 | /// Adds the database anti forgery key store for the anti forgery feature. 59 | /// 60 | /// The gateway anti forgery builder. 61 | /// The gateway anti forgery builder. 62 | public static GatewayAntiForgeryBuilder UseDbKeyStore(this GatewayAntiForgeryBuilder builder) 63 | { 64 | if (!_dbContextAdded) 65 | { 66 | throw new InvalidOperationException("Call 'AddDbContext' before configuring options using the db context"); 67 | } 68 | 69 | builder.Services.AddDataProtection().PersistKeysToDbContext(); 70 | 71 | return builder; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/GatewayRoutingSettings.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Routing.Auth; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Routing; 4 | 5 | /// 6 | /// A class to hold all settings required to configure the gateway routing feature. 7 | /// 8 | public class GatewayRoutingSettings 9 | { 10 | /// 11 | /// Gets or sets the resource proxy. 12 | /// 13 | /// 14 | /// The resource proxy. 15 | /// 16 | public string? ResourceProxy { get; set; } 17 | 18 | /// 19 | /// Gets or sets the routes. 20 | /// 21 | /// 22 | /// The routes. 23 | /// 24 | public IDictionary Routes { get; set; } = new Dictionary(); 25 | 26 | /// 27 | /// Validates the settings. 28 | /// 29 | /// Each route needs to have at least a 'BaseUrl' 30 | public void Validate() 31 | { 32 | // Check required fields of each route 33 | foreach(RouteSettings route in Routes.Values) 34 | { 35 | if(route.BaseUrl == null || string.IsNullOrEmpty(route.BaseUrl.AbsoluteUri)) 36 | { 37 | throw new InvalidOperationException("Each route needs to have at least a 'BaseUrl'"); 38 | } 39 | } 40 | } 41 | } 42 | 43 | /// 44 | /// A class to hold all settings required for each route. 45 | /// 46 | public class RouteSettings 47 | { 48 | /// 49 | /// Gets or sets the base URL. 50 | /// 51 | /// 52 | /// The base URL. 53 | /// 54 | public Uri? BaseUrl { get; set; } 55 | 56 | /// 57 | /// Gets or sets the path match. 58 | /// 59 | /// 60 | /// The path match. 61 | /// 62 | public string? PathMatch { get; set; } 63 | 64 | /// 65 | /// Gets or sets the authentication settings. 66 | /// 67 | /// 68 | /// The authentication settings. 69 | /// 70 | public RouteAuthenticationSettings Authentication { get; set; } = new RouteAuthenticationSettings(); 71 | } 72 | 73 | /// 74 | /// A class to hold all setting required for authenticating to a backend. 75 | /// 76 | public class RouteAuthenticationSettings 77 | { 78 | /// 79 | /// Gets or sets the strategy. 80 | /// 81 | /// 82 | /// The strategy. 83 | /// 84 | public string Strategy { get; set; } = "NoAuthentication"; 85 | 86 | /// 87 | /// Gets or sets the options. 88 | /// 89 | /// 90 | /// The options. 91 | /// 92 | public IDictionary Options { get; set; } = new Dictionary(); 93 | } 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/WebApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.AntiForgery; 2 | using Fancy.ResourceLinker.Gateway.Authentication; 3 | using Fancy.ResourceLinker.Gateway.Routing; 4 | using Microsoft.AspNetCore.Builder; 5 | 6 | namespace Fancy.ResourceLinker.Gateway; 7 | 8 | /// 9 | /// A builder to set up an anti forgery policy. 10 | /// 11 | public class AntiForgeryBuilder 12 | { 13 | /// 14 | /// Gets or sets the exclusions. 15 | /// 16 | /// 17 | /// The exclusions. 18 | /// 19 | internal List Exclusions { get; set; } = new List(); 20 | 21 | /// 22 | /// Excludes the specified path start. 23 | /// 24 | /// The path start. 25 | /// An instance to the same instance. 26 | public AntiForgeryBuilder Exclude(string pathStart) 27 | { 28 | Exclusions.Add(pathStart); 29 | return this; 30 | } 31 | } 32 | 33 | /// 34 | /// Extension class with helpers to easily add the gateway features to your middleware pipeline. 35 | /// 36 | public static class WebApplicationExtensions 37 | { 38 | /// 39 | /// Adds the gateway anti forgery feature to the middleware pipeline. 40 | /// 41 | /// The web application. 42 | public static void UseGatewayAntiForgery(this WebApplication webApp) => GatewayAntiForgery.UseGatewayAntiForgery(webApp); 43 | 44 | /// 45 | /// Adds the gateway anti forgery feature to the middleware pipeline. 46 | /// 47 | /// The web application. 48 | /// An action to configure the anti forgery policy. 49 | public static void UseGatewayAntiForgery(this WebApplication webApp, Action configurePolicy) => GatewayAntiForgery.UseGatewayAntiForgery(webApp, configurePolicy); 50 | 51 | /// 52 | /// Adds the gateway authentication feature to the middleware pipeline. 53 | /// 54 | /// The web application. 55 | public static void UseGatewayAuthentication(this WebApplication webApp) => GatewayAuthentication.UseGatewayAuthentication(webApp); 56 | 57 | /// 58 | /// Adds the gateway authentication endpoints to the middleware pipeline. 59 | /// 60 | /// The web application. 61 | public static void UseGatewayAuthenticationEndpoints(this WebApplication webApp) => GatewayAuthenticationEndpoints.UseGatewayAuthenticationEndpoints(webApp); 62 | 63 | /// 64 | /// Adds the gateway routing feature to the middleware pipeline. 65 | /// 66 | /// The web application. 67 | public static void UseGatewayRouting(this WebApplication webApp) => GatewayRouting.UseGatewayRouting(webApp); 68 | } 69 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/Auth0ClientCredentialsAuthStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Fancy.ResourceLinker.Gateway.Common; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 6 | 7 | /// 8 | /// A route authentication strategy running the OAuth client credential flow. 9 | /// 10 | /// 11 | /// In some situations, auth0 requires an audience parameter in the request. This once can be added with this auth strategy. 12 | /// 13 | internal class Auth0ClientCredentialsAuthStrategy : ClientCredentialsAuthStrategy 14 | { 15 | /// 16 | /// The name of the auth strategy. 17 | /// 18 | public new const string NAME = "Auth0ClientCredentials"; 19 | 20 | // 21 | /// The auth0 audience. 22 | /// 23 | private string _audience = string.Empty; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The discovery document service. 29 | /// The logger. 30 | public Auth0ClientCredentialsAuthStrategy(DiscoveryDocumentService discoveryDocumentService, ILogger logger) : base(discoveryDocumentService, logger) 31 | { 32 | } 33 | 34 | /// 35 | /// Gets the name of the strategy. 36 | /// 37 | /// 38 | /// The name. 39 | /// 40 | public override string Name => NAME; 41 | 42 | /// 43 | /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. 44 | /// 45 | /// The gateway authentication settigns. 46 | /// The route authentication settigns. 47 | public override Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) 48 | { 49 | if (routeAuthenticationSettings.Options.ContainsKey("Audience")) 50 | { 51 | _audience = routeAuthenticationSettings.Options["Audience"]; 52 | } 53 | 54 | return base.InitializeAsync(gatewayAuthenticationSettings, routeAuthenticationSettings); 55 | } 56 | 57 | /// 58 | /// Sets up the token request. 59 | /// 60 | /// The token request. 61 | protected override Dictionary SetUpTokenRequest() 62 | { 63 | return new Dictionary 64 | { 65 | { "grant_type", "client_credentials" }, 66 | { "client_id", _clientId }, 67 | { "client_secret", _clientSecret }, 68 | { "scope", _scope }, 69 | { "audience", _audience } 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/IMvcBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models.Json; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Reflection; 4 | 5 | namespace Fancy.ResourceLinker.Hateoas; 6 | 7 | /// 8 | /// Extension class with helper to easily register resource linker to ioc controller service. 9 | /// 10 | public static class IMvcBuilderExtensions 11 | { 12 | /// 13 | /// Adds the resource linker hateoas feature to the ioc container. 14 | /// 15 | /// The mvc builder to add the resource linker to. 16 | /// The assemblies to search for implementations to use to link resources. 17 | /// if set to true ignores empty metadata fields in serialized resources. 18 | /// Specifies if fields which starts with '_' shall be read an written from and to resources. 19 | /// 20 | /// The service collection. 21 | /// 22 | public static IMvcBuilder AddHateoas(this IMvcBuilder mvcBuilder, Assembly[] assemblies, bool ignoreEmptyMetadata = true, bool writePrivates = true) 23 | { 24 | // Add required services 25 | mvcBuilder.Services.AddTransient(typeof(IResourceLinker), typeof(ResourceLinker)); 26 | 27 | // Add resource converter 28 | mvcBuilder.AddJsonOptions(options => options.JsonSerializerOptions.AddResourceConverter(ignoreEmptyMetadata, writePrivates)); 29 | 30 | // Find all link strategies in provided assemblies and register them 31 | foreach (Assembly assembly in assemblies) 32 | { 33 | IEnumerable linkStrategies = assembly.GetTypes().Where(x => typeof(ILinkStrategy).IsAssignableFrom(x)); 34 | 35 | foreach (Type linkStrategy in linkStrategies) 36 | { 37 | mvcBuilder.Services.AddTransient(typeof(ILinkStrategy), linkStrategy); 38 | } 39 | } 40 | 41 | return mvcBuilder; 42 | } 43 | 44 | /// 45 | /// Adds the resource linker hateoas feature to the ioc container and automatically searches the calling assembly for 46 | /// implementation of to use to link resources. 47 | /// 48 | /// The mvc builder to add the resource linker to. 49 | /// if set to true ignores empty metadata fields in serialized resources. 50 | /// Specifies if fields which starts with '_' shall be read an written from and to resources. 51 | /// 52 | /// The service collection. 53 | /// 54 | public static IMvcBuilder AddHateoas(this IMvcBuilder mvcBuilder, bool ignoreEmptyMetadata = true, bool writePrivates = true) 55 | { 56 | // Get the calling assembly and add the resource linker 57 | Assembly assembly = Assembly.GetCallingAssembly(); 58 | return AddHateoas(mvcBuilder, [ assembly ], ignoreEmptyMetadata, writePrivates); 59 | } 60 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/TokenRecord.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Gateway.Authentication; 2 | 3 | /// 4 | /// A class to hold all token information needed for a specific user session. 5 | /// 6 | public class TokenRecord 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The session identifier. 12 | /// The identifier token. 13 | /// The access token. 14 | /// The refresh token. 15 | /// The expires at. 16 | public TokenRecord(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiresAt) 17 | { 18 | SessionId = sessionId; 19 | IdToken = idToken; 20 | AccessToken = accessToken; 21 | RefreshToken = refreshToken; 22 | ExpiresAt = expiresAt; 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The session identifier. 29 | /// The identifier token. 30 | /// The access token. 31 | /// The refresh token. 32 | /// The userinfo claims. 33 | /// The expires at. 34 | public TokenRecord(string sessionId, string idToken, string accessToken, string refreshToken, string userinfoClaims, DateTime expiresAt) 35 | { 36 | SessionId = sessionId; 37 | IdToken = idToken; 38 | AccessToken = accessToken; 39 | RefreshToken = refreshToken; 40 | UserinfoClaims = userinfoClaims; 41 | ExpiresAt = expiresAt; 42 | } 43 | 44 | /// 45 | /// Gets or sets the session identifier. 46 | /// 47 | /// 48 | /// The session identifier. 49 | /// 50 | public string SessionId { get; set; } 51 | 52 | /// 53 | /// Gets or sets the identifier token. 54 | /// 55 | /// 56 | /// The identifier token. 57 | /// 58 | public string IdToken { get; set; } 59 | 60 | /// 61 | /// Gets or sets the access token. 62 | /// 63 | /// 64 | /// The access token. 65 | /// 66 | public string AccessToken { get; set; } 67 | 68 | /// 69 | /// Gets or sets the refresh token. 70 | /// 71 | /// 72 | /// The refresh token. 73 | /// 74 | public string RefreshToken { get; set; } 75 | 76 | /// 77 | /// Gets or sets the userinfo claims. 78 | /// 79 | /// 80 | /// The userinfo claims. 81 | /// 82 | public string? UserinfoClaims { get; set; } 83 | 84 | /// 85 | /// Gets or sets the expires at. 86 | /// 87 | /// 88 | /// The expires at. 89 | /// 90 | public DateTime ExpiresAt { get; set; } 91 | } 92 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/IResource.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Fancy.ResourceLinker.Models; 5 | 6 | /// 7 | /// Interface for a resource which container embedded metadata as hypermedia document. 8 | /// 9 | public interface IResource 10 | { 11 | /// 12 | /// Gets or sets the links of this resource. 13 | /// 14 | /// 15 | /// The links. 16 | /// 17 | [JsonPropertyName("_links")] 18 | [NotMapped] 19 | Dictionary Links { get; } 20 | 21 | /// 22 | /// Gets or sets the actions of this resource. 23 | /// 24 | /// 25 | /// The actions. 26 | /// 27 | [JsonPropertyName("_actions")] 28 | [NotMapped] 29 | Dictionary Actions { get; } 30 | 31 | /// 32 | /// Gets the sockets. 33 | /// 34 | /// 35 | /// The sockets. 36 | /// 37 | [JsonPropertyName("_sockets")] 38 | [NotMapped] 39 | Dictionary Sockets { get; } 40 | 41 | /// 42 | /// Adds a link. 43 | /// 44 | /// The relation. 45 | /// The href. 46 | void AddLink(string rel, string href) 47 | { 48 | Links[rel] = new ResourceLink(href); 49 | } 50 | 51 | /// 52 | /// Adds an action. 53 | /// 54 | /// The relation. 55 | /// The method. 56 | /// The URL to the action. 57 | void AddAction(string rel, string method, string href); 58 | 59 | /// 60 | /// Adds a socket. 61 | /// 62 | /// The relation. 63 | /// The href. 64 | /// The method. 65 | /// The token. 66 | void AddSocket(string rel, string href, string method, string token); 67 | 68 | /// 69 | /// Adds a socket. 70 | /// 71 | /// The relation. 72 | /// The href. 73 | /// The method. 74 | void AddSocket(string rel, string href, string method); 75 | 76 | /// 77 | /// Removes the metadata of links, actions and sockets completely from this instance. 78 | /// 79 | void ClearMetadata(); 80 | 81 | /// 82 | /// Gets a collection containing the keys of the . 83 | /// 84 | ICollection Keys { get; } 85 | 86 | /// 87 | /// Gets the static keys. 88 | /// 89 | /// 90 | /// The static keys. 91 | /// 92 | internal ICollection StaticKeys { get; } 93 | 94 | /// 95 | /// Gets or sets the with the specified key. 96 | /// 97 | /// 98 | /// The . 99 | /// 100 | /// The key. 101 | /// The object. 102 | object? this[string key] { get; set; } 103 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/TokenPassThroughAuthStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Primitives; 5 | using System.Net.Http.Headers; 6 | 7 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 8 | 9 | /// 10 | /// An auth strategy which just passes through the current token 11 | /// 12 | internal class TokenPassThroughAuthStrategy : IRouteAuthenticationStrategy 13 | { 14 | /// 15 | /// The name of the auth strategy. 16 | /// 17 | public const string NAME = "TokenPassThrough"; 18 | 19 | /// 20 | /// Gets the name of the strategy. 21 | /// 22 | /// 23 | /// The name. 24 | /// 25 | public string Name => NAME; 26 | 27 | /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. 28 | /// The gateway authentication settigns. 29 | /// The route authentication settigns. 30 | /// 31 | /// A task indicating the completion of the asyncrhonous operation. 32 | /// 33 | public Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) 34 | { 35 | return Task.CompletedTask; 36 | } 37 | 38 | /// 39 | /// Sets the authentication to an http context asynchronous. 40 | /// 41 | /// The http context. 42 | /// 43 | /// A task indicating the completion of the asynchronous operation 44 | /// 45 | public async Task SetAuthenticationAsync(HttpContext context) 46 | { 47 | TokenService tokenService = GetTokenService(context.RequestServices); 48 | string accessToken = await tokenService.GetAccessTokenAsync(); 49 | context.Request.Headers.Authorization = new StringValues("Bearer " + accessToken); 50 | } 51 | 52 | /// 53 | /// Sets the authentication to an http request message asynchronous. 54 | /// 55 | /// The current service provider. 56 | /// The http request message. 57 | /// 58 | /// A task indicating the completion of the asynchronous operation 59 | /// 60 | public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) 61 | { 62 | TokenService? tokenService = GetTokenService(serviceProvider); 63 | string accessToken = await tokenService.GetAccessTokenAsync(); 64 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 65 | } 66 | 67 | /// 68 | /// Gets the token service. 69 | /// 70 | /// The service provider. 71 | /// An instance of the current token service. 72 | private TokenService GetTokenService(IServiceProvider serviceProvider) 73 | { 74 | try 75 | { 76 | return serviceProvider.GetRequiredService(); 77 | } 78 | catch (InvalidOperationException) 79 | { 80 | throw new InvalidOperationException("A token service in a scope is needed to use the token pass through auth strategy"); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/ResourceLinker.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Fancy.ResourceLinker.Models; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Fancy.ResourceLinker.Hateoas; 6 | 7 | /// 8 | /// Implements the interface. 9 | /// 10 | public class ResourceLinker : IResourceLinker 11 | { 12 | /// 13 | /// The link strategies. 14 | /// 15 | private IEnumerable _linkStrategies; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The link strategies to use in this instance. 21 | public ResourceLinker(IEnumerable linkStrategies) 22 | { 23 | _linkStrategies = linkStrategies; 24 | } 25 | 26 | /// 27 | /// Adds links to a resource using a link strategy. 28 | /// 29 | /// The type of resource to add links to. 30 | /// The resource to add links to. 31 | /// The URL helper. 32 | public void AddLinks(TResource resource, IUrlHelper urlHelper) where TResource : IResource 33 | { 34 | if (resource == null) 35 | { 36 | throw new ArgumentNullException(nameof(resource)); 37 | } 38 | 39 | LinkObject(resource, urlHelper); 40 | 41 | // Interate through all keys and link child objects which are also resources 42 | foreach (string key in resource.Keys) 43 | { 44 | object? dynamicPropertyValue = resource[key]; 45 | 46 | if (dynamicPropertyValue == null) 47 | { 48 | continue; 49 | } 50 | 51 | if (dynamicPropertyValue.GetType().GetTypeInfo().IsSubclassOf(typeof(ResourceBase))) 52 | { 53 | // Type is a resource -> cast the object of the property and link it 54 | AddLinks((ResourceBase)dynamicPropertyValue, urlHelper); 55 | } 56 | else if (dynamicPropertyValue is IEnumerable) 57 | { 58 | // Type is a collection of resources -> cast object and link it 59 | IEnumerable? subResources = dynamicPropertyValue as IEnumerable; 60 | if(subResources != null) AddLinks(subResources, urlHelper); 61 | } 62 | } 63 | } 64 | 65 | /// 66 | /// Adds links to a collection of resources using a link strategy. 67 | /// 68 | /// The type of resource to add links to. 69 | /// The resources to add links to. 70 | /// The URL helper. 71 | public void AddLinks(IEnumerable resources, IUrlHelper urlHelper) where TResource : IResource 72 | { 73 | foreach (IResource resource in resources) 74 | { 75 | AddLinks(resource, urlHelper); 76 | } 77 | } 78 | 79 | /// 80 | /// Links the object. 81 | /// 82 | /// The resource to link. 83 | /// The URL helper to use to build the links. 84 | private void LinkObject(IResource resource, IUrlHelper urlHelper) 85 | { 86 | ILinkStrategy? linkStrategy = _linkStrategies.FirstOrDefault(ls => ls.CanLinkType(resource.GetType())); 87 | 88 | if (linkStrategy != null) 89 | { 90 | linkStrategy.LinkResource(resource, urlHelper); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthenticationEndpoints.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Http; 6 | using System.Security.Claims; 7 | 8 | namespace Fancy.ResourceLinker.Gateway.Authentication; 9 | 10 | /// 11 | /// Class to add endpoints to the web application specific for the authentication feature. 12 | /// 13 | internal static class GatewayAuthenticationEndpoints 14 | { 15 | /// 16 | /// Adds the required gateway authentication endpoints to the middleware pipeline for the authentication feature. 17 | /// 18 | /// The web application. 19 | internal static void UseGatewayAuthenticationEndpoints(this WebApplication webApp) 20 | { 21 | webApp.UseUserInfoEndpoint(); 22 | webApp.UseLoginEndpoint(); 23 | webApp.UseLogoutEndpoint(); 24 | } 25 | 26 | /// 27 | /// Adds the login endpoint to the middleware pipeline. 28 | /// 29 | /// The web application. 30 | private static void UseLoginEndpoint(this WebApplication webApp) 31 | { 32 | webApp.MapGet("/login", async (string? redirectUrl, HttpContext context) => 33 | { 34 | 35 | if (string.IsNullOrEmpty(redirectUrl)) 36 | { 37 | redirectUrl = "/"; 38 | } 39 | 40 | AuthenticationProperties authProps = new AuthenticationProperties 41 | { 42 | RedirectUri = redirectUrl 43 | }; 44 | 45 | await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProps); 46 | }); 47 | } 48 | 49 | /// 50 | /// Adds the logout endpoint to the middleware pipeline. 51 | /// 52 | /// The web application. 53 | private static void UseLogoutEndpoint(this WebApplication webApp) 54 | { 55 | webApp.MapGet("/logout", (string? redirectUrl, HttpContext context) => 56 | { 57 | if (string.IsNullOrEmpty(redirectUrl)) 58 | { 59 | redirectUrl = "/"; 60 | } 61 | 62 | var authProps = new AuthenticationProperties 63 | { 64 | RedirectUri = redirectUrl 65 | }; 66 | 67 | var authSchemes = new string[] { 68 | CookieAuthenticationDefaults.AuthenticationScheme, 69 | OpenIdConnectDefaults.AuthenticationScheme 70 | }; 71 | 72 | return Results.SignOut(authProps, authSchemes); 73 | }); 74 | } 75 | 76 | /// 77 | /// Adds the user info endpoint to the middleware pipeline. 78 | /// 79 | /// The web application. 80 | private static void UseUserInfoEndpoint(this WebApplication webApp) 81 | { 82 | webApp.MapGet("/userinfo", async (TokenService tokenService) => 83 | { 84 | IEnumerable? claims = await tokenService.GetIdentityClaimsAsync(); 85 | 86 | if(claims == null) 87 | { 88 | return Results.Content("undefined"); 89 | } 90 | 91 | // Map all claims to a dictionary 92 | Dictionary dictionary = new Dictionary(); 93 | 94 | foreach (var entry in claims) 95 | { 96 | dictionary[entry.Type] = entry.Value; 97 | } 98 | 99 | return Results.Ok(dictionary); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/EnsureAuthenticatedAuthStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 6 | 7 | /// 8 | /// An auth strategy which makes sure that the current call is authenticated but does not set auth infos to requests. 9 | /// 10 | internal class EnsureAuthenticatedAuthStrategy : IRouteAuthenticationStrategy 11 | { 12 | /// 13 | /// The name of the auth strategy. 14 | /// 15 | public const string NAME = "EnsureAuthenticated"; 16 | 17 | /// 18 | /// Gets the name of the strategy. 19 | /// 20 | /// 21 | /// The name. 22 | /// 23 | public string Name => NAME; 24 | 25 | /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. 26 | /// The gateway authentication settigns. 27 | /// The route authentication settigns. 28 | /// 29 | /// A task indicating the completion of the asyncrhonous operation. 30 | /// 31 | public Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) 32 | { 33 | return Task.CompletedTask; 34 | } 35 | 36 | /// 37 | /// Sets the authentication to an http context asynchronous. 38 | /// 39 | /// The http context. 40 | /// 41 | /// A task indicating the completion of the asynchronous operation 42 | /// 43 | public Task SetAuthenticationAsync(HttpContext context) 44 | { 45 | return EnsureAccessTokenExistsAsync(context.RequestServices); 46 | } 47 | 48 | /// 49 | /// Sets the authentication to an http request message asynchronous. 50 | /// 51 | /// The current service provider. 52 | /// The http request message. 53 | /// 54 | /// A task indicating the completion of the asynchronous operation 55 | /// 56 | public Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) 57 | { 58 | return EnsureAccessTokenExistsAsync(serviceProvider); 59 | } 60 | 61 | /// 62 | /// Ensures the access token exists asynchronous. 63 | /// 64 | /// The service provider. 65 | /// No access token is available 66 | private async Task EnsureAccessTokenExistsAsync(IServiceProvider serviceProvider) 67 | { 68 | TokenService tokenService = GetTokenService(serviceProvider); 69 | string accessToken = await tokenService.GetAccessTokenAsync(); 70 | 71 | if (string.IsNullOrEmpty(accessToken)) 72 | { 73 | throw new InvalidOperationException("No access token is available"); 74 | } 75 | } 76 | 77 | /// 78 | /// Gets the token service. 79 | /// 80 | /// The service provider. 81 | /// An instance of the current token service. 82 | private TokenService GetTokenService(IServiceProvider serviceProvider) 83 | { 84 | try 85 | { 86 | return serviceProvider.GetRequiredService(); 87 | } 88 | catch (InvalidOperationException) 89 | { 90 | throw new InvalidOperationException("A token service in a scope is needed to use the ensure authenticated auth strategy"); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/InMemoryTokenStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Fancy.ResourceLinker.Gateway.Authentication; 4 | 5 | /// 6 | /// A simple implementation of to store tokens in memory. 7 | /// 8 | /// 9 | internal class InMemoryTokenStore : ITokenStore 10 | { 11 | /// 12 | /// A dictionary mapping tokens to sessions. 13 | /// 14 | private ConcurrentDictionary _tokenStore = new ConcurrentDictionary(); 15 | 16 | /// 17 | /// Saves the or update tokens asynchronous. 18 | /// 19 | /// The session identifier. 20 | /// The identifier token. 21 | /// The access token. 22 | /// The refresh token. 23 | /// The expiration. 24 | /// 25 | /// A task indicating the completion of the asynchronous operation. 26 | /// 27 | public Task SaveOrUpdateTokensAsync(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiration) 28 | { 29 | if (_tokenStore.ContainsKey(sessionId)) 30 | { 31 | // Update existing token 32 | _tokenStore[sessionId].IdToken = idToken; 33 | _tokenStore[sessionId].IdToken = idToken; 34 | _tokenStore[sessionId].AccessToken = accessToken; 35 | _tokenStore[sessionId].RefreshToken = refreshToken; 36 | _tokenStore[sessionId].ExpiresAt = expiration; 37 | } 38 | else 39 | { 40 | _tokenStore[sessionId] = new TokenRecord(sessionId, idToken, accessToken, refreshToken, expiration); 41 | } 42 | 43 | return Task.CompletedTask; 44 | } 45 | 46 | /// 47 | /// Saves the or update userinfo claims asynchronous. 48 | /// 49 | /// The session identifier. 50 | /// The userinfo object as json string. 51 | /// 52 | /// A task indicating the completion of the asynchronous operation. 53 | /// 54 | public Task SaveOrUpdateUserinfoClaimsAsync(string sessionId, string userinfoClaims) 55 | { 56 | if(!_tokenStore.ContainsKey(sessionId)) 57 | { 58 | throw new InvalidOperationException("The specified session Id is not valid"); 59 | } 60 | 61 | _tokenStore[sessionId].UserinfoClaims = userinfoClaims; 62 | 63 | return Task.CompletedTask; 64 | } 65 | 66 | /// 67 | /// Gets the token record for a provided session asynchronous. 68 | /// 69 | /// The session identifier. 70 | /// 71 | /// A token record if available. 72 | /// 73 | public Task GetTokenRecordAsync(string sessionId) 74 | { 75 | if (_tokenStore.ContainsKey(sessionId)) return Task.FromResult(_tokenStore[sessionId]); 76 | else return Task.FromResult(null); 77 | } 78 | 79 | /// 80 | /// Cleans up the expired token records asynchronous. 81 | /// 82 | /// A task indicating the completion of the asynchronous operation. 83 | public Task CleanupExpiredTokenRecordsAsync() 84 | { 85 | ConcurrentDictionary validRecords = new ConcurrentDictionary (); 86 | 87 | foreach(var record in _tokenStore.Values) 88 | { 89 | if(record.ExpiresAt > DateTime.UtcNow) validRecords[record.SessionId] = record; 90 | } 91 | 92 | _tokenStore = validRecords; 93 | 94 | return Task.CompletedTask; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/AntiForgery/GatewayAntiForgery.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Antiforgery; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Fancy.ResourceLinker.Gateway.AntiForgery; 7 | 8 | /// 9 | /// Class with helper methods to set up anti forgery feature. 10 | /// 11 | internal static class GatewayAntiForgery 12 | { 13 | /// 14 | /// Adds the gateway anti forgery service to the ioc container. 15 | /// 16 | /// The services. 17 | internal static void AddGatewayAntiForgery(IServiceCollection services) 18 | { 19 | services.AddAntiforgery(setup => 20 | { 21 | setup.HeaderName = "X-XSRF-TOKEN"; 22 | }); 23 | } 24 | 25 | /// 26 | /// Adds the gateway anti forgery middleware to the middleware pipeline. 27 | /// 28 | /// The web application. 29 | /// The configure policy. 30 | internal static void UseGatewayAntiForgery(WebApplication webApp, Action? configurePolicy = null) 31 | { 32 | AntiForgeryBuilder antiForgeryBuilder = new AntiForgeryBuilder(); 33 | if(configurePolicy != null) configurePolicy(antiForgeryBuilder); 34 | 35 | webApp.UseXsrfCookieCreator(); 36 | webApp.UseXsrfHeaderChecks(antiForgeryBuilder.Exclusions.ToArray()); 37 | } 38 | 39 | /// 40 | /// Adds a middleware to create XSRF cookies. 41 | /// 42 | /// The web application. 43 | private static void UseXsrfCookieCreator(this WebApplication webApp) 44 | { 45 | webApp.Use((context, next) => 46 | { 47 | IAntiforgery? antiforgery = webApp.Services.GetService(); 48 | 49 | if (antiforgery == null) 50 | { 51 | throw new InvalidOperationException("IAntiforgery service exptected! Call 'AddAntiForgery' on the gateway builder to add required services!"); 52 | } 53 | 54 | AntiforgeryTokenSet tokens = antiforgery!.GetAndStoreTokens(context); 55 | 56 | if (tokens.RequestToken == null) 57 | { 58 | throw new InvalidOperationException("Antiforgery request token exptected!"); 59 | } 60 | 61 | // Add a XSRF Cookie to the response 62 | context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false }); 63 | 64 | return next(context); 65 | }); 66 | } 67 | 68 | /// 69 | /// Adds a middleware to check XSRF headers. 70 | /// 71 | /// The web application. 72 | private static void UseXsrfHeaderChecks(this WebApplication webApp, string[] exclusions) 73 | { 74 | webApp.Use(async (context, next) => 75 | { 76 | IAntiforgery? antiforgery = webApp.Services.GetService(); 77 | 78 | if (antiforgery == null) 79 | { 80 | throw new InvalidOperationException("IAntiforgery service exptected! Call 'AddAntiForgery' on the gateway builder to add required services!"); 81 | } 82 | 83 | if (!exclusions.Any(excludedPath => context.Request.Path.Value?.StartsWith(excludedPath) ?? false)) 84 | { 85 | if (!await antiforgery.IsRequestValidAsync(context)) 86 | { 87 | // Return an error response 88 | context.Response.StatusCode = StatusCodes.Status400BadRequest; 89 | await context.Response.WriteAsJsonAsync(new { Error = "XSRF token validadation failed" }); 90 | return; 91 | } 92 | } 93 | 94 | await next(context); 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/RouteAuthenticationManager.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 5 | 6 | /// 7 | /// A class to create an hold instances of route authentication strategies for specific routes. 8 | /// 9 | public class RouteAuthenticationManager 10 | { 11 | /// 12 | /// The route authentication strategy instances. 13 | /// 14 | private Dictionary _authStrategyInstances = new Dictionary(); 15 | 16 | /// 17 | /// The routing settings. 18 | /// 19 | private readonly GatewayRoutingSettings _routingSettings; 20 | 21 | /// 22 | /// The service provider. 23 | /// 24 | private readonly IServiceProvider _serviceProvider; 25 | 26 | /// 27 | /// The gateway authentication settings if available. 28 | /// 29 | private readonly GatewayAuthenticationSettings? _gatewayAuthenticationSettings; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The routing settings. 35 | /// The service provider to use to get instances of auth strategies. 36 | public RouteAuthenticationManager(GatewayRoutingSettings routingSettings, IServiceProvider serviceProvider) 37 | { 38 | _routingSettings = routingSettings; 39 | _serviceProvider = serviceProvider; 40 | _gatewayAuthenticationSettings = GetGatewayAuthenticationSettings(); 41 | } 42 | 43 | /// 44 | /// Gets an authentication strategy for a specific route asynchronous. 45 | /// 46 | /// The name. 47 | /// An instance of the auth strategy. 48 | public async Task GetAuthStrategyAsync(string route) 49 | { 50 | if(!_authStrategyInstances.ContainsKey(route)) 51 | { 52 | return await CreateAuthStrategyAsync(route); 53 | } 54 | 55 | return _authStrategyInstances[route]; 56 | } 57 | 58 | /// 59 | /// Creates an authentication strategy asynchronous. 60 | /// 61 | /// The route. 62 | /// 63 | /// The authentication strategy. 64 | /// 65 | private async Task CreateAuthStrategyAsync(string route) 66 | { 67 | RouteAuthenticationSettings routeAuthSettings = _routingSettings.Routes[route].Authentication; 68 | 69 | IRouteAuthenticationStrategy authStrategy; 70 | 71 | try 72 | { 73 | authStrategy = _serviceProvider.GetRequiredKeyedService(routeAuthSettings.Strategy); 74 | } 75 | catch(InvalidOperationException e) 76 | { 77 | throw new InvalidOperationException($"Could not retrieve an IRouteAuthenticationStrategy with strategy name '{routeAuthSettings.Strategy}'", e); 78 | } 79 | 80 | await authStrategy.InitializeAsync(_gatewayAuthenticationSettings, routeAuthSettings); 81 | 82 | _authStrategyInstances[route] = authStrategy; 83 | 84 | return authStrategy; 85 | } 86 | 87 | /// 88 | /// Gets the gateway authentication settings. 89 | /// 90 | /// The gateway authentication settings or null if none are set. 91 | private GatewayAuthenticationSettings? GetGatewayAuthenticationSettings() 92 | { 93 | try 94 | { 95 | return _serviceProvider.GetService(); 96 | } 97 | catch 98 | { 99 | return null; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/GatewayAuthenticationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Fancy.ResourceLinker.Gateway.Authentication; 2 | 3 | /// 4 | /// A class to hold all settings required to configure the gateway authentication feature. 5 | /// 6 | public class GatewayAuthenticationSettings 7 | { 8 | /// 9 | /// Gets or sets the session timeout in minutes. 10 | /// 11 | /// 12 | /// The session timeout in minutes. 13 | /// 14 | public int SessionTimeoutInMin { get; set; } = 5; 15 | 16 | /// 17 | /// Gets or sets the URL to the authority. 18 | /// 19 | /// 20 | /// The authority URL. 21 | /// 22 | public string Authority { get; set; } = string.Empty; 23 | 24 | /// 25 | /// Gets or sets the client identifier. 26 | /// 27 | /// 28 | /// The client identifier. 29 | /// 30 | public string ClientId { get; set; } = string.Empty; 31 | 32 | /// 33 | /// Gets or sets the client secret. 34 | /// 35 | /// 36 | /// The client secret. 37 | /// 38 | public string ClientSecret { get; set; } = string.Empty; 39 | 40 | /// 41 | /// Gets or sets the scopes to request during an authorization code flow. 42 | /// 43 | /// 44 | /// The authorization code scopes. 45 | /// 46 | public string Scopes { get; set; } = string.Empty; 47 | 48 | /// 49 | /// Gets or sets the type of the unique identifier claim. 50 | /// 51 | /// 52 | /// The type of the unique identifier claim. 53 | /// 54 | public string UniqueIdentifierClaimType { get; set; } = string.Empty; 55 | 56 | /// 57 | /// Set whether the handler should go to user info endpoint of the authorization server to retrieve additional claims or not 58 | /// after creating an identity from id_token received from token endpoint. 59 | /// 60 | /// 61 | /// true if the handler shall query the user info endpoint of the authorization server; otherwise, false. 62 | /// 63 | public bool QueryUserInfoEndpoint { get; set; } = false; 64 | 65 | /// 66 | /// Gets or sets the issuer address for sign out. 67 | /// 68 | /// 69 | /// The issuer address for sign out. 70 | /// 71 | public string? IssuerAddressForSignOut { get; set; } 72 | 73 | /// 74 | /// Gets or sets the cookiesettings. 75 | /// 76 | /// 77 | /// Set cookiesettings. 78 | /// 79 | public CookieSettings CookieSettings { get; set; } = new CookieSettings(); 80 | 81 | 82 | public void Validate() 83 | { 84 | // Check required fields 85 | if(string.IsNullOrEmpty(Authority)) 86 | { 87 | throw new InvalidOperationException("'Authority' is required to be set within 'AuthenticationSettings'"); 88 | } 89 | 90 | if (string.IsNullOrEmpty(ClientId)) 91 | { 92 | throw new InvalidOperationException("'ClientId' is required to be set within 'AuthenticationSettings'"); 93 | } 94 | 95 | if (string.IsNullOrEmpty(UniqueIdentifierClaimType)) 96 | { 97 | throw new InvalidOperationException("'UniqueIdentifierClaimType' is required to be set within 'AuthenticationSettings'"); 98 | } 99 | } 100 | } 101 | 102 | public class CookieSettings 103 | { 104 | /// 105 | /// Gets or sets the SameSite policy 106 | /// 107 | /// 108 | /// true || Not set == Strict 109 | /// false == Lax 110 | /// 111 | public bool SameSiteStrict { get; set; } = false; 112 | /// 113 | /// 114 | /// Gets or sets the secure flag. 115 | /// 116 | /// 117 | /// true || Not set == Always 118 | /// false == SameAsRequest 119 | /// 120 | public bool Secure { get; set; } = true; 121 | /// 122 | /// 123 | /// Gets or sets the HttpOnly setting. 124 | /// 125 | /// 126 | /// true || Not set == true 127 | /// false == false 128 | /// 129 | public bool HttpOnly { get; set; } = true; 130 | } 131 | -------------------------------------------------------------------------------- /Fancy.ResourceLinker.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fancy.ResourceLinker.Gateway", "src\Fancy.ResourceLinker.Gateway\Fancy.ResourceLinker.Gateway.csproj", "{FAC615EB-E00E-4DB9-AF91-D02C2006B189}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fancy.ResourceLinker.Gateway.EntityFrameworkCore", "src\Fancy.ResourceLinker.Gateway.EntityFrameworkCore\Fancy.ResourceLinker.Gateway.EntityFrameworkCore.csproj", "{D9153090-F629-482E-81CE-24FB1DECEFD2}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fancy.ResourceLinker.Hateoas", "src\Fancy.ResourceLinker.Hateoas\Fancy.ResourceLinker.Hateoas.csproj", "{99D3DC1B-CAA3-4F53-8071-4C8ED60F9623}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fancy.ResourceLinker.Models", "src\Fancy.ResourceLinker.Models\Fancy.ResourceLinker.Models.csproj", "{C24A8606-FBF8-426B-919A-A3209A19DC5E}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fancy.ResourceLinker.Models.ITest", "src\Fancy.ResourceLinker.Models.ITest\Fancy.ResourceLinker.Models.ITest.csproj", "{25A704BA-8F0C-4C51-AB10-4542E3CD33DF}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fancy.ResourceLinker.Models.UTest", "src\Fancy.ResourceLinker.Models.UTest\Fancy.ResourceLinker.Models.UTest.csproj", "{04A89FBF-5902-4A4E-964D-16CE1828F034}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C8679203-B59A-4A3B-B60C-EC92677D5939}" 19 | ProjectSection(SolutionItems) = preProject 20 | .github\workflows\Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml = .github\workflows\Fancy.ResourceLinker.Gateway.EntityFrameworkCore.yaml 21 | .github\workflows\Fancy.ResourceLinker.Gateway.yaml = .github\workflows\Fancy.ResourceLinker.Gateway.yaml 22 | .github\workflows\Fancy.ResourceLinker.Hateoas.yaml = .github\workflows\Fancy.ResourceLinker.Hateoas.yaml 23 | .github\workflows\Fancy.ResourceLinker.Models.yaml = .github\workflows\Fancy.ResourceLinker.Models.yaml 24 | EndProjectSection 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {FAC615EB-E00E-4DB9-AF91-D02C2006B189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {FAC615EB-E00E-4DB9-AF91-D02C2006B189}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {FAC615EB-E00E-4DB9-AF91-D02C2006B189}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {FAC615EB-E00E-4DB9-AF91-D02C2006B189}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {D9153090-F629-482E-81CE-24FB1DECEFD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {D9153090-F629-482E-81CE-24FB1DECEFD2}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {D9153090-F629-482E-81CE-24FB1DECEFD2}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {D9153090-F629-482E-81CE-24FB1DECEFD2}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {99D3DC1B-CAA3-4F53-8071-4C8ED60F9623}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {99D3DC1B-CAA3-4F53-8071-4C8ED60F9623}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {99D3DC1B-CAA3-4F53-8071-4C8ED60F9623}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {99D3DC1B-CAA3-4F53-8071-4C8ED60F9623}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {C24A8606-FBF8-426B-919A-A3209A19DC5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {C24A8606-FBF8-426B-919A-A3209A19DC5E}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {C24A8606-FBF8-426B-919A-A3209A19DC5E}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {C24A8606-FBF8-426B-919A-A3209A19DC5E}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {25A704BA-8F0C-4C51-AB10-4542E3CD33DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {25A704BA-8F0C-4C51-AB10-4542E3CD33DF}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {25A704BA-8F0C-4C51-AB10-4542E3CD33DF}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {25A704BA-8F0C-4C51-AB10-4542E3CD33DF}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {04A89FBF-5902-4A4E-964D-16CE1828F034}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {04A89FBF-5902-4A4E-964D-16CE1828F034}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {04A89FBF-5902-4A4E-964D-16CE1828F034}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {04A89FBF-5902-4A4E-964D-16CE1828F034}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {41D39620-DCF2-41E4-8FD1-336CC7031518} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/GatewayRouting.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Routing.Auth; 2 | using Fancy.ResourceLinker.Gateway.Routing.Util; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Yarp.ReverseProxy.Configuration; 6 | using Yarp.ReverseProxy.Transforms; 7 | 8 | namespace Fancy.ResourceLinker.Gateway.Routing; 9 | 10 | /// 11 | /// Class with helper methods to set up the routing feature. 12 | /// 13 | internal static class GatewayRouting 14 | { 15 | /// 16 | /// Adds the gateway routing services to the ioc container. 17 | /// 18 | /// The services. 19 | internal static void AddGatewayRouting(IServiceCollection services, GatewayRoutingSettings settings) 20 | { 21 | // Register all other needed services 22 | services.AddHttpForwarder(); 23 | services.AddSingleton(settings); 24 | services.AddTransient(); 25 | services.AddReverseProxy().AddGatewayRoutes(settings); 26 | 27 | // Set up routing authentication subsystem 28 | services.AddSingleton(); 29 | services.AddKeyedTransient(NoAuthenticationAuthStrategy.NAME); 30 | services.AddKeyedTransient(EnsureAuthenticatedAuthStrategy.NAME); 31 | services.AddKeyedTransient(TokenPassThroughAuthStrategy.NAME); 32 | services.AddKeyedTransient(AzureOnBehalfOfAuthStrategy.NAME); 33 | services.AddKeyedTransient(ClientCredentialsAuthStrategy.NAME); 34 | services.AddKeyedTransient(Auth0ClientCredentialsAuthStrategy.NAME); 35 | } 36 | 37 | /// 38 | /// Adds the gateway routing middleware to the middleware pipeline by using the yarp proxy. 39 | /// 40 | /// The web application. 41 | internal static void UseGatewayRouting(WebApplication webApp) 42 | { 43 | webApp.MapReverseProxy(pipeline => 44 | { 45 | pipeline.UseGatewayPipeline(); 46 | }); 47 | } 48 | 49 | /// 50 | /// Adds the gateway routes to the yarp proxy as in memory configuration. 51 | /// 52 | /// The reverse proxy builder. 53 | /// The settings. 54 | private static void AddGatewayRoutes(this IReverseProxyBuilder reverseProxyBuilder, GatewayRoutingSettings settings) 55 | { 56 | List routes = new List(); 57 | List clusters = new List(); 58 | 59 | // Add for each microservcie a route and a cluster 60 | foreach (KeyValuePair routeSettings in settings.Routes) 61 | { 62 | // Only for routes with a path match an automatic gateway route is created 63 | if (string.IsNullOrEmpty(routeSettings.Value.PathMatch)) continue; 64 | 65 | if (routeSettings.Value.BaseUrl == null) throw new InvalidOperationException($"A 'BaseUrl' must be set for route '{routeSettings.Key}'"); 66 | routes.Add(new RouteConfig 67 | { 68 | RouteId = routeSettings.Key, 69 | ClusterId = routeSettings.Key, 70 | Match = new RouteMatch { Path = routeSettings.Value.PathMatch }, 71 | Metadata = new Dictionary { { "RouteName", routeSettings.Key } }, 72 | Transforms = new List> 73 | { 74 | // Turn off default forwarded headers to be able to override it with the origin of the 75 | // ResourceProxy from the configuration 76 | new Dictionary{ { "X-Forwarded", "Off" } } 77 | } 78 | , }); 79 | 80 | clusters.Add(new ClusterConfig 81 | { 82 | ClusterId = routeSettings.Key, 83 | Destinations = new Dictionary { { "default", new DestinationConfig { Address = routeSettings.Value.BaseUrl.AbsoluteUri } } }, 84 | }); 85 | } 86 | 87 | reverseProxyBuilder.LoadFromMemory(routes, clusters) 88 | .AddTransforms(builder => 89 | builder.AddRequestTransform(context => { context.ProxyRequest.Headers.SetForwardedHeaders(settings.ResourceProxy); return ValueTask.CompletedTask; })); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway.EntityFrameworkCore/DbTokenStore.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.Authentication; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.Collections.Concurrent; 4 | 5 | namespace Fancy.ResourceLinker.Gateway.EntityFrameworkCore; 6 | 7 | /// 8 | /// Implements the interface to store all tokens into a database. 9 | /// 10 | /// 11 | internal class DbTokenStore : ITokenStore 12 | { 13 | /// 14 | /// The database context 15 | /// 16 | private readonly GatewayDbContext _dbContext; 17 | 18 | /// 19 | /// The cached tokens. 20 | /// 21 | ConcurrentDictionary _cachedTokens = new ConcurrentDictionary(); 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The database context. 27 | public DbTokenStore(GatewayDbContext dbContext) 28 | { 29 | _dbContext = dbContext; 30 | } 31 | 32 | /// 33 | /// Saves or update tokens asynchronous. 34 | /// 35 | /// The session identifier. 36 | /// The identifier token. 37 | /// The access token. 38 | /// The refresh token. 39 | /// The expires at. 40 | public async Task SaveOrUpdateTokensAsync(string sessionId, string idToken, string accessToken, string refreshToken, DateTime expiresAt) 41 | { 42 | TokenSet? tokenSet = await _dbContext.TokenSets.SingleOrDefaultAsync(ts => ts.SessionId == sessionId); 43 | 44 | if (tokenSet == null) 45 | { 46 | tokenSet = new TokenSet(sessionId, idToken, accessToken, refreshToken, expiresAt); 47 | _dbContext.TokenSets.Add(tokenSet); 48 | } 49 | else 50 | { 51 | tokenSet.IdToken = idToken; 52 | tokenSet.AccessToken = accessToken; 53 | tokenSet.RefreshToken = refreshToken; 54 | tokenSet.ExpiresAt = expiresAt; 55 | } 56 | 57 | _cachedTokens[sessionId] = tokenSet; 58 | await _dbContext.SaveChangesAsync(); 59 | } 60 | 61 | /// 62 | /// Saves the or update userinfo claims asynchronous. 63 | /// 64 | /// The session identifier. 65 | /// The userinfo object as json string. 66 | /// 67 | /// A task indicating the completion of the asynchronous operation. 68 | /// 69 | public async Task SaveOrUpdateUserinfoClaimsAsync(string sessionId, string userinfoClaims) 70 | { 71 | TokenSet? tokenSet = await _dbContext.TokenSets.SingleOrDefaultAsync(ts => ts.SessionId == sessionId); 72 | 73 | if (tokenSet == null) 74 | { 75 | throw new InvalidOperationException("The specified session Id is not valid"); 76 | } 77 | else 78 | { 79 | tokenSet.UserinfoClaims = userinfoClaims; 80 | } 81 | 82 | await _dbContext.SaveChangesAsync(); 83 | } 84 | 85 | /// 86 | /// Gets the token record for a provided session asynchronous. 87 | /// 88 | /// The session identifier. 89 | /// 90 | /// A token record if available. 91 | /// 92 | /// 93 | /// This method is thread save to enable paralell calls from the gateway to backends. 94 | /// 95 | public Task GetTokenRecordAsync(string sessionId) 96 | { 97 | TokenSet? tokenSet; 98 | 99 | lock (_dbContext) 100 | { 101 | if (!_cachedTokens.ContainsKey(sessionId)) 102 | { 103 | tokenSet = _dbContext.TokenSets.SingleOrDefault(ts => ts.SessionId == sessionId); 104 | _cachedTokens[sessionId] = tokenSet; 105 | } 106 | else 107 | { 108 | tokenSet = _cachedTokens[sessionId]; 109 | } 110 | } 111 | 112 | if (tokenSet == null) { return Task.FromResult(null); } 113 | 114 | return Task.FromResult(new TokenRecord(sessionId, tokenSet.IdToken, tokenSet.AccessToken, tokenSet.RefreshToken, tokenSet.ExpiresAt)); 115 | } 116 | 117 | /// 118 | /// Cleans up the expired token records asynchronous. 119 | /// 120 | /// A task indicating the completion of the asynchronous operation. 121 | public Task CleanupExpiredTokenRecordsAsync() 122 | { 123 | return _dbContext.TokenSets.Where(ts => ts.ExpiresAt < DateTime.UtcNow).ExecuteDeleteAsync(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/TokenClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Text.Json.Serialization; 3 | using Fancy.ResourceLinker.Gateway.Common; 4 | 5 | namespace Fancy.ResourceLinker.Gateway.Authentication; 6 | 7 | /// 8 | /// Class to hold the result of a token refresh response. 9 | /// 10 | public class TokenRefreshResponse 11 | { 12 | /// 13 | /// Gets or sets the identifier token. 14 | /// 15 | /// 16 | /// The identifier token. 17 | /// 18 | [JsonPropertyName("id_token")] 19 | public string IdToken { get; set; } = ""; 20 | 21 | /// 22 | /// Gets or sets the access token. 23 | /// 24 | /// 25 | /// The access token. 26 | /// 27 | [JsonPropertyName("access_token")] 28 | public string AccessToken { get; set; } = ""; 29 | 30 | /// 31 | /// Gets or sets the refresh token. 32 | /// 33 | /// 34 | /// The refresh token. 35 | /// 36 | [JsonPropertyName("refresh_token")] 37 | public string RefreshToken { get; set; } = ""; 38 | 39 | /// 40 | /// Gets or sets the expires in. 41 | /// 42 | /// 43 | /// The expires in. 44 | /// 45 | [JsonPropertyName("expires_in")] 46 | public long ExpiresIn { get; set; } 47 | } 48 | 49 | /// 50 | /// Class to hold the result of a client credentials token response. 51 | /// 52 | public class ClientCredentialsTokenResponse 53 | { 54 | /// 55 | /// Gets or sets the access token. 56 | /// 57 | /// 58 | /// The access token. 59 | /// 60 | [JsonPropertyName("access_token")] 61 | public string AccessToken { get; set; } = ""; 62 | 63 | /// 64 | /// Gets or sets the expires in. 65 | /// 66 | /// 67 | /// The expires in. 68 | /// 69 | [JsonPropertyName("expires_in")] 70 | public long ExpiresIn { get; set; } 71 | } 72 | 73 | /// 74 | /// A token client with implementation of typical token logic. 75 | /// 76 | public class TokenClient 77 | { 78 | /// 79 | /// The authentication settings. 80 | /// 81 | private readonly GatewayAuthenticationSettings _settings; 82 | 83 | /// 84 | /// The discovery document service. 85 | /// 86 | private readonly DiscoveryDocumentService _discoveryDocumentService; 87 | 88 | /// 89 | /// The discovery document. 90 | /// 91 | private DiscoveryDocument? _discoveryDocument; 92 | 93 | /// 94 | /// The HTTP client. 95 | /// 96 | private HttpClient _httpClient = new HttpClient(); 97 | 98 | /// 99 | /// Initializes a new instance of the class. 100 | /// 101 | /// The authentication settings. 102 | /// The discovery document service. 103 | public TokenClient(GatewayAuthenticationSettings settings, DiscoveryDocumentService discoveryDocumentService) 104 | { 105 | _settings = settings; 106 | _discoveryDocumentService = discoveryDocumentService; 107 | } 108 | 109 | /// 110 | /// Executes a token refresh the asynchronous. 111 | /// 112 | /// The refresh token. 113 | /// The token refresh response. 114 | public async Task RefreshAsync(string refreshToken) 115 | { 116 | DiscoveryDocument discoveryDocument = await GetDiscoveryDocumentAsync(); 117 | 118 | var payload = new Dictionary 119 | { 120 | { "grant_type", "refresh_token" }, 121 | { "refresh_token", refreshToken }, 122 | { "client_id", _settings.ClientId }, 123 | { "client_secret", _settings.ClientSecret } 124 | }; 125 | 126 | HttpRequestMessage request = new HttpRequestMessage 127 | { 128 | RequestUri = new Uri(discoveryDocument.TokenEndpoint), 129 | Method = HttpMethod.Post, 130 | Content = new FormUrlEncodedContent(payload) 131 | }; 132 | 133 | HttpResponseMessage response = await _httpClient.SendAsync(request); 134 | 135 | if (!response.IsSuccessStatusCode) 136 | { 137 | return null; 138 | } 139 | 140 | return await response.Content.ReadFromJsonAsync(); 141 | } 142 | 143 | /// 144 | /// Queries the user information endpoint. 145 | /// 146 | /// The access token. 147 | /// The json object delivered by the userinfo endpoint. 148 | public async Task QueryUserInfoEndpoint(string accessToken) 149 | { 150 | DiscoveryDocument discoveryDocument = await GetDiscoveryDocumentAsync(); 151 | 152 | HttpRequestMessage request = new HttpRequestMessage 153 | { 154 | RequestUri = new Uri(discoveryDocument.UserinfoEndpoint), 155 | Method = HttpMethod.Get, 156 | Headers = { { "Authorization", $"Bearer {accessToken}" } } 157 | }; 158 | 159 | HttpResponseMessage response = await _httpClient.SendAsync(request); 160 | 161 | response.EnsureSuccessStatusCode(); 162 | 163 | return await response.Content.ReadAsStringAsync(); 164 | } 165 | 166 | /// 167 | /// Gets the discovery document asynchronous. 168 | /// 169 | /// The discovery document. 170 | private async Task GetDiscoveryDocumentAsync() 171 | { 172 | if (_discoveryDocument == null) 173 | { 174 | _discoveryDocument = await _discoveryDocumentService.LoadDiscoveryDocumentAsync(_settings.Authority); 175 | } 176 | 177 | return _discoveryDocument; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/ResourceBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Reflection; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Fancy.ResourceLinker.Models; 6 | 7 | /// 8 | /// Base class of a resource which can be linked to other resources. 9 | /// 10 | public abstract class ResourceBase : IResource 11 | { 12 | /// 13 | /// The static keys. 14 | /// 15 | private ICollection _staticKeys; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public ResourceBase() 21 | { 22 | // Initialize metadata dictionaries 23 | Links = new Dictionary(); 24 | Actions = new Dictionary(); 25 | Sockets = new Dictionary(); 26 | 27 | // Get all static (compile time) properties of this type 28 | _staticKeys = GetType() 29 | .GetProperties(BindingFlags.Public | BindingFlags.Instance) 30 | .Where(p => p.GetIndexParameters().Length == 0) 31 | .Select(p => p.Name) 32 | .ToList(); 33 | 34 | // Remove the control keys of the dictionary interface 35 | _staticKeys.Remove("Keys"); 36 | } 37 | 38 | /// 39 | /// Gets or sets the links of this resource. 40 | /// 41 | /// 42 | /// The links. 43 | /// 44 | [JsonPropertyName("_links")] 45 | [NotMapped] 46 | public Dictionary Links { get; internal set; } 47 | 48 | /// 49 | /// Gets or sets the actions of this resource. 50 | /// 51 | /// 52 | /// The actions. 53 | /// 54 | [JsonPropertyName("_actions")] 55 | [NotMapped] 56 | public Dictionary Actions { get; internal set; } 57 | 58 | /// 59 | /// Gets the sockets. 60 | /// 61 | /// 62 | /// The sockets. 63 | /// 64 | [JsonPropertyName("_sockets")] 65 | [NotMapped] 66 | public Dictionary Sockets { get; internal set; } 67 | 68 | /// 69 | /// Adds a link. 70 | /// 71 | /// The relation. 72 | /// The href. 73 | public void AddLink(string rel, string href) 74 | { 75 | Links[rel] = new ResourceLink(href); 76 | } 77 | 78 | /// 79 | /// Adds an action. 80 | /// 81 | /// The relation. 82 | /// The method. 83 | /// The URL to the action. 84 | public void AddAction(string rel, string method, string href) 85 | { 86 | Actions[rel] = new ResourceAction(method, href); 87 | } 88 | 89 | /// 90 | /// Adds an action. 91 | /// 92 | /// The relation. 93 | /// The resource action to add. 94 | public void AddAction(string rel, ResourceAction action) 95 | { 96 | Actions[rel] = action; 97 | } 98 | 99 | /// 100 | /// Adds a socket. 101 | /// 102 | /// The relation. 103 | /// The href. 104 | /// The method. 105 | /// The token. 106 | public void AddSocket(string rel, string href, string method, string token) 107 | { 108 | Sockets[rel] = new ResourceSocket(href, method, token); 109 | } 110 | 111 | /// 112 | /// Adds a socket. 113 | /// 114 | /// The relation. 115 | /// The href. 116 | /// The method. 117 | public void AddSocket(string rel, string href, string method) 118 | { 119 | Sockets.Add(rel, new ResourceSocket(href, method)); 120 | } 121 | 122 | /// 123 | /// Removes the metadata of links, actions and sockets completely from this instance. 124 | /// 125 | public void ClearMetadata() 126 | { 127 | Links.Clear(); 128 | Actions.Clear(); 129 | Sockets.Clear(); 130 | } 131 | 132 | /// 133 | /// Gets a collection containing the keys of the . 134 | /// 135 | public ICollection Keys { get => _staticKeys; } 136 | 137 | /// 138 | /// Gets the static keys. 139 | /// 140 | /// 141 | /// The static keys. 142 | /// 143 | ICollection IResource.StaticKeys { get => _staticKeys; } 144 | 145 | // The couterpart to the explicit interface implementation to make it accessible within the class 146 | private ICollection StaticKeys { get => _staticKeys; } 147 | 148 | /// 149 | /// Gets or sets the with the specified key. 150 | /// 151 | /// 152 | /// The . 153 | /// 154 | /// The key. 155 | /// The object. 156 | public object? this[string key] 157 | { 158 | get 159 | { 160 | if (StaticKeys.Contains(key)) 161 | { 162 | return GetType().GetProperty(key)!.GetValue(this); 163 | } 164 | else 165 | { 166 | throw new KeyNotFoundException($"Key {key} does not exist on this statically typed resource. If you want to work with dynamic data structures use 'DynamicResourceBase'."); 167 | } 168 | } 169 | set 170 | { 171 | if (StaticKeys.Contains(key)) 172 | { 173 | GetType().GetProperty(key)!.SetValue(this, value); 174 | } 175 | else 176 | { 177 | throw new KeyNotFoundException($"Key {key} does not exist on this statically typed resource. If you want to work with dynamic data structures use 'DynamicResourceBase'."); 178 | } 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fancy.ResourceLinker 2 | A library to easily create API Gateways and Backend for Frontends (BFF) based on ASP.NET Core. 3 | 4 | ## Why an api Gateway/Backend for Frontend 5 | If you need to access multiple backend systems from a single frontend, then it is often better the frontend has a 6 | single system it communicates with and which is also located closer to the backend systems instead of letting the frontend 7 | communicate with each single backend directly. The following picture depicts this connstellation. 8 | 9 | ![architecture](architecture.drawio.png) 10 | 11 | The advantages you get if you use the API Gateway or Backend for Frontend pattern are: 12 | 13 | - **Loose Coupling** 14 | - The host urls, routes and api endpoints of the backend system can be changed independently of the client. If there are breaking 15 | changes, they can be compensated within the API Gateway. 16 | - **Enhanced Performance** 17 | - If you need to call multiple backend systems to fill a single view in your frontend, this task can usually be performed faster and 18 | more efficiently on server systems. Furthermore, the API Gateway is closer to the backend and therefore it has a smaller latency. 19 | - If there is data which does not need to be retrieved with each request, you can easily cache data within the API Gateway which 20 | makes responses faster and saves load on backend. 21 | - If your API Gateway knows about each single view of your frontend, then we talk about a Backend for Frontend. A Backend for Frontend 22 | can create optimized responses for your frontend. The goal is that a frontend makes one request per view to initially fill a view. 23 | This saves payload on the route from frontend to gateway which has typically a smaller bandwidth. 24 | - **Improved Security** 25 | - If you publis all api endpoints of all backends in one api gateway, you have them availalbe under the same orign. Your backends systems 26 | are not available directly which and you have only one point left where you have to care about security. 27 | - You can run OAuth authorization flows server side at the API Gateway which means that tokens are also available only on server side. 28 | Handling tokens on the client side is not recommended or even allowed by the security standards. Shifting this to server side makes 29 | your application more secure. 30 | 31 | ## Features 32 | With Fancy Resource Linker you can easily add the following features to your API Gateway: 33 | 34 | - **Data Aggregation** 35 | 36 | Aggregate data from different sources (e.g. different Microservices) into view models which are optimized for your client. Reduce the calls of the client to the backend by filling an entire view in your client with a single request and thus boosting your client side user experience. 37 | 38 | - **Data Caching** 39 | 40 | Cache data you received from backend system within your API Gateway to make following requests faster and save load on the backend 41 | system. 42 | 43 | - **Reverse Proxy** 44 | 45 | Publish separate deployed Apps or Microfrontends or Microservices under one orign within different virtual directories. 46 | 47 | - **Truly RESTful APIs utilizing HATEOAS** 48 | 49 | Create web api's which enrich your json documents with metadata about connected/linked resources and with possible actions which can be executed on your data to create a full self describing api by the use of hypermedia. 50 | 51 | - **Authentication Gateway** 52 | 53 | Let your gateway act as a single authentication facade to all of your resources behind, running OAuth flows server side and keeping tokens also only server side to obtain the maximum security and comply to all current security standards. 54 | 55 | ## Sample Application 56 | There is a sample application [Fancy.ResourceLinker-Sample](https://github.com/fancyDevelopment/Fancy.ResourceLinker-Sample) which demonstrates all the features of this library. Have a look to it to see how the individual fetures can be used. 57 | 58 | ## Getting Started 59 | 60 | First add the [Fancy.ResourceLinker.Gateway](https://www.nuget.org/packages/Fancy.ResourceLinker.Gateway) nuget package to your project. 61 | 62 | To get started building an api gateway with Fancy.ResourceLinker in your ASP.NET Core application add the required services to the service collection and load a configuration by providing a configuration section. 63 | 64 | ```cs 65 | var builder = WebApplication.CreateBuilder(args); 66 | 67 | builder.Services.AddGateway() 68 | .LoadConfiguration(builder.Configuration.GetSection("Gateway")) 69 | .AddRouting(); 70 | ``` 71 | 72 | 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: 73 | 74 | ```json 75 | "Gateway": { 76 | "Routing": { 77 | "Routes": { 78 | "Microservice1": { 79 | "BaseUrl": "http://localhost:5000", 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | 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. 87 | 88 | ```cs 89 | [ApiController] 90 | public class HomeController : HypermediaController 91 | { 92 | private readonly GatewayRouter _router; 93 | 94 | public HomeController(GatewayRouter router) 95 | { 96 | _router = router; 97 | } 98 | 99 | [HttpGet] 100 | [Route("api/views/home")] 101 | public async Task GetHomeViewModel() 102 | { 103 | var dataFromMicroservice = _router.GetAsync("Microservice1", "api/data/123"); 104 | [...] 105 | } 106 | } 107 | ``` 108 | 109 | 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. 110 | 111 | ## Realize Advanced Features in your Gateway 112 | 113 | To learn how each single feature can be realized have a look to the following individual guidelines. 114 | 115 | * Aggregating data from differend sources into a client optimized model - Documentation Comming Soon 116 | * Provide different apis and/or resources under the same origin - Documentation Comming Soon 117 | * Create truly RESTful services - Documentation Comming Soon 118 | * [Let the gateway act as authentication facade](./doc/features/authentication.md) 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models.ITest/DynamicResourceSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Models.Json; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Fancy.ResourceLinker.Models.ITest; 6 | 7 | /// 8 | /// A object used to test the serialization. 9 | /// 10 | /// 11 | class TestObject : DynamicResourceBase 12 | { 13 | public int IntProperty { get; set; } = 5; 14 | public string StringProperty { get; set; } = "foobar"; 15 | 16 | [JsonIgnore] 17 | public string IgnoredStaticProperty { get; set; } = "foo"; 18 | } 19 | 20 | class NonPubCtorTestObject : DynamicResourceBase 21 | { 22 | private NonPubCtorTestObject() { } 23 | public int IntProperty { get; set; } = 5; 24 | public string StringProperty { get; set; } = "foobar"; 25 | 26 | [JsonIgnore] 27 | public string IgnoredStaticProperty { get; set; } = "foo"; 28 | } 29 | 30 | /// 31 | /// Test class to test serialization and deserializsation using the resource converter. 32 | /// 33 | [TestClass] 34 | public class DynamicResourceSerializerTests 35 | { 36 | /// 37 | /// A complex object used within the tests. 38 | /// 39 | const string TEST_DATA = @" 40 | { 41 | ""intProperty"": 5, 42 | ""stringProperty"": ""foobar"", 43 | ""boolProperty"": true, 44 | ""objProperty"": { ""subObjProperty"": ""subObjFoobar"" }, 45 | ""nullProperty"": null, 46 | ""arrayProperty"": [ 5, ""foo"", { ""objInArrProperty"": ""fooInArray"" }, [ ""subarray"", 6, true ] ] 47 | }"; 48 | 49 | 50 | /// 51 | /// Tests the deserialization of the complex object to a dynamic resource. 52 | /// 53 | [TestMethod] 54 | public void DeserializeAndSerializeComplexObject() 55 | { 56 | JsonSerializerOptions serializerOptions = new JsonSerializerOptions(); 57 | serializerOptions.AddResourceConverter(); 58 | 59 | dynamic deserializedObject = JsonSerializer.Deserialize(TEST_DATA, serializerOptions)!; 60 | 61 | Assert.IsNotNull(deserializedObject); 62 | Assert.AreEqual(5, deserializedObject.IntProperty); 63 | Assert.AreEqual("foobar", deserializedObject.StringProperty); 64 | Assert.AreEqual(true, deserializedObject.BoolProperty); 65 | Assert.AreEqual("subObjFoobar", deserializedObject.ObjProperty.SubObjProperty); 66 | Assert.IsNull(deserializedObject.NullProperty); 67 | Assert.AreEqual(5, deserializedObject.ArrayProperty[0]); 68 | Assert.AreEqual("foo", deserializedObject.ArrayProperty[1]); 69 | Assert.AreEqual("fooInArray", deserializedObject.ArrayProperty[2].ObjInArrProperty); 70 | Assert.AreEqual("subarray", deserializedObject.ArrayProperty[3][0]); 71 | Assert.AreEqual(6, deserializedObject.ArrayProperty[3][1]); 72 | Assert.AreEqual(true, deserializedObject.ArrayProperty[3][2]); 73 | 74 | string serializedObject = JsonSerializer.Serialize(deserializedObject, serializerOptions); 75 | 76 | Assert.AreEqual(TEST_DATA.Replace(Environment.NewLine, "").Replace(" ", ""), serializedObject); 77 | } 78 | 79 | /// 80 | /// Tests the deserialization of the complex object to a dynamic resource. 81 | /// 82 | [TestMethod] 83 | public void DeserializeIntoObjectWithNonPubCtor() 84 | { 85 | JsonSerializerOptions serializerOptions = new JsonSerializerOptions(); 86 | serializerOptions.AddResourceConverter(false, false); 87 | 88 | dynamic deserializedObject = JsonSerializer.Deserialize(TEST_DATA, serializerOptions)!; 89 | 90 | Assert.IsNotNull(deserializedObject); 91 | Assert.AreEqual(5, deserializedObject.IntProperty); 92 | Assert.AreEqual("foobar", deserializedObject.StringProperty); 93 | Assert.AreEqual(true, deserializedObject.BoolProperty); 94 | Assert.AreEqual("subObjFoobar", deserializedObject.ObjProperty.SubObjProperty); 95 | Assert.IsNull(deserializedObject.NullProperty); 96 | Assert.AreEqual(5, deserializedObject.ArrayProperty[0]); 97 | Assert.AreEqual("foo", deserializedObject.ArrayProperty[1]); 98 | Assert.AreEqual("fooInArray", deserializedObject.ArrayProperty[2].ObjInArrProperty); 99 | Assert.AreEqual("subarray", deserializedObject.ArrayProperty[3][0]); 100 | Assert.AreEqual(6, deserializedObject.ArrayProperty[3][1]); 101 | Assert.AreEqual(true, deserializedObject.ArrayProperty[3][2]); 102 | } 103 | 104 | [TestMethod] 105 | public void SerializeComplexObjectWithoutEmptyMetadata_InObjectWithNoMetadata() 106 | { 107 | TestObject data = new TestObject(); 108 | 109 | JsonSerializerOptions serializerOptions = new JsonSerializerOptions(); 110 | serializerOptions.AddResourceConverter(true, true); 111 | 112 | string serializedObject = JsonSerializer.Serialize(data, serializerOptions); 113 | 114 | JsonDocument document = JsonDocument.Parse(serializedObject); 115 | 116 | Assert.AreEqual(5, document.RootElement.GetProperty("intProperty").GetInt32()); 117 | Assert.AreEqual("foobar", document.RootElement.GetProperty("stringProperty").GetString()); 118 | Assert.IsFalse(document.RootElement.TryGetProperty("_links", out var _linksPorperty)); 119 | Assert.IsFalse(document.RootElement.TryGetProperty("_actions", out var _actionsProperty)); 120 | Assert.IsFalse(document.RootElement.TryGetProperty("_sockets", out var _socketsPorperty)); 121 | } 122 | 123 | [TestMethod] 124 | public void SerializeComplexObjectWithoutEmptyMetadata_InObjectWithMetadata() 125 | { 126 | TestObject data = new TestObject(); 127 | data.AddLink("self", "http://my.domain/api/my/object"); 128 | 129 | JsonSerializerOptions serializerOptions = new JsonSerializerOptions(); 130 | serializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; 131 | serializerOptions.AddResourceConverter(true, true); 132 | 133 | string serializedObject = JsonSerializer.Serialize(data, serializerOptions); 134 | 135 | JsonDocument document = JsonDocument.Parse(serializedObject); 136 | 137 | Assert.AreEqual(5, document.RootElement.GetProperty("intProperty").GetInt32()); 138 | Assert.AreEqual("foobar", document.RootElement.GetProperty("stringProperty").GetString()); 139 | Assert.IsTrue(document.RootElement.TryGetProperty("_links", out var _linksPorperty)); 140 | Assert.AreEqual(_linksPorperty.GetProperty("self").GetProperty("href").GetString(), "http://my.domain/api/my/object"); 141 | Assert.IsFalse(document.RootElement.TryGetProperty("_actions", out var _actionsProperty)); 142 | Assert.IsFalse(document.RootElement.TryGetProperty("_sockets", out var _socketsPorperty)); 143 | } 144 | 145 | [TestMethod] 146 | public void SerializeComplexObjectWithoutIgnoredProperties() 147 | { 148 | TestObject data = new TestObject(); 149 | 150 | JsonSerializerOptions serializerOptions = new JsonSerializerOptions(); 151 | serializerOptions.AddResourceConverter(true, true); 152 | 153 | string serializedObject = JsonSerializer.Serialize(data, serializerOptions); 154 | 155 | JsonDocument document = JsonDocument.Parse(serializedObject); 156 | 157 | JsonElement result; 158 | Assert.IsFalse(document.RootElement.TryGetProperty("ignoredStaticProperty", out result)); 159 | } 160 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Models/Json/ResourceJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Reflection; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Fancy.ResourceLinker.Models.Json; 7 | 8 | /// 9 | /// A converter to help converting resource objects correctly into json. 10 | /// 11 | /// The concrete type of the resource to convert. 12 | /// 13 | public class ResourceJsonConverter : JsonConverter where T : class, IResource 14 | { 15 | /// 16 | /// The a flag to indicate weather the converter shall write json private fields. 17 | /// 18 | private readonly bool _writePrivates; 19 | 20 | /// 21 | /// A flag to indicate if empty metadata fields shall be ignored. 22 | /// 23 | private readonly bool _ignoreEmptyMetadata; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// if set to true the converter reads and writes private fields. 29 | /// if set to true ignores empty metadata fields. 30 | public ResourceJsonConverter(bool writePrivates, bool ignoreEmptyMetadata) 31 | { 32 | _writePrivates = writePrivates; 33 | _ignoreEmptyMetadata = ignoreEmptyMetadata; 34 | } 35 | 36 | /// 37 | /// Reads and converts the JSON to type . 38 | /// 39 | /// The reader. 40 | /// The type to convert. 41 | /// An object that specifies serialization options to use. 42 | /// 43 | /// The converted value. 44 | /// 45 | /// 46 | /// Dictionary must be JSON object. 47 | /// or 48 | /// Incomplete JSON object 49 | /// or 50 | /// Incomplete JSON object 51 | /// 52 | public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 53 | { 54 | if (reader.TokenType == JsonTokenType.Null) return null; 55 | 56 | if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("A resource must always be a JSON object."); 57 | 58 | // Create the default result. 59 | T? result = Activator.CreateInstance(typeof(T), true) as T; 60 | 61 | if (result == null) 62 | { 63 | throw new JsonException("Unable to create a new instance of " + typeof(T).Name + ". Make sure your class has at least a private parameterless constructor"); 64 | } 65 | 66 | while (true) 67 | { 68 | if (!reader.Read()) throw new JsonException("Incomplete or broken JSON object!"); 69 | 70 | if (reader.TokenType == JsonTokenType.EndObject) break; 71 | 72 | // Read the next key 73 | var key = reader.GetString()!; 74 | 75 | // If key is private field remove underscore 76 | key = key[0] == '_' ? key.Substring(1) : key; 77 | 78 | // Adjust case of first character to .NET standards 79 | key = char.ToUpper(key[0]) + key.Substring(1); 80 | 81 | if (!reader.Read()) throw new JsonException("Incomplete or broken JSON object!"); 82 | 83 | object? value; 84 | 85 | if (result.StaticKeys.Contains(key)) 86 | { 87 | // Read a static key 88 | value = JsonSerializer.Deserialize(ref reader, result.GetType().GetProperty(key)!.PropertyType, options); 89 | } 90 | else 91 | { 92 | // Read a dynamic key 93 | value = ReadDynamicValue(ref reader, options); 94 | } 95 | 96 | result[key] = value; 97 | } 98 | 99 | if(!_writePrivates) 100 | { 101 | result.ClearMetadata(); 102 | } 103 | 104 | return result; 105 | } 106 | 107 | /// 108 | /// Writes a specified value as JSON. 109 | /// 110 | /// The writer to write to. 111 | /// The value to convert to JSON. 112 | /// An object that specifies serialization options to use. 113 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 114 | { 115 | if (value is null) 116 | { 117 | // Nothing to do 118 | writer.WriteNullValue(); 119 | } 120 | else 121 | { 122 | writer.WriteStartObject(); 123 | 124 | // Step through each key and add it to json 125 | foreach (string key in value.Keys) 126 | { 127 | // If the current attribute is a static attribute with json ignore, just continue 128 | PropertyInfo? staticPropertyInfo = value.GetType().GetProperty(key); 129 | if (staticPropertyInfo != null && Attribute.IsDefined(staticPropertyInfo, typeof(JsonIgnoreAttribute))) continue; 130 | 131 | string jsonKey = char.ToLower(key[0]) + key.Substring(1); 132 | if (key == "Links" || key == "Actions" || key == "Sockets") 133 | { 134 | jsonKey = "_" + jsonKey; 135 | 136 | if (_ignoreEmptyMetadata) 137 | { 138 | IDictionary? metadataDictionary = value[key] as IDictionary; 139 | if (metadataDictionary != null && metadataDictionary.Count == 0) 140 | { 141 | continue; 142 | } 143 | } 144 | } 145 | 146 | if (key.StartsWith("_") && !_writePrivates) continue; 147 | 148 | writer.WritePropertyName(jsonKey); 149 | JsonSerializer.Serialize(writer, value[key], options); 150 | } 151 | 152 | writer.WriteEndObject(); 153 | } 154 | } 155 | 156 | /// 157 | /// Reads a dynamic value from the json reader. If it finds an object or an array the 158 | /// methods proceeds reading in a recursive manner. 159 | /// 160 | /// The reader. 161 | /// The options. 162 | /// The value. 163 | private object? ReadDynamicValue(ref Utf8JsonReader reader, JsonSerializerOptions options) 164 | { 165 | object? value = null; 166 | 167 | if (reader.TokenType == JsonTokenType.Number) 168 | { 169 | value = reader.GetDouble(); 170 | } 171 | else if (reader.TokenType == JsonTokenType.String) 172 | { 173 | value = reader.GetString(); 174 | } 175 | else if (reader.TokenType == JsonTokenType.False) 176 | { 177 | value = false; 178 | } 179 | else if (reader.TokenType == JsonTokenType.True) 180 | { 181 | value = true; 182 | } 183 | else if (reader.TokenType == JsonTokenType.StartObject) 184 | { 185 | value = JsonSerializer.Deserialize(ref reader, options); 186 | } 187 | else if(reader.TokenType == JsonTokenType.StartArray) 188 | { 189 | IList arrayValues = new List(); 190 | while(reader.Read()) 191 | { 192 | if (reader.TokenType == JsonTokenType.EndArray) break; 193 | arrayValues.Add(ReadDynamicValue(ref reader, options)); 194 | } 195 | value = arrayValues; 196 | } else if(reader.TokenType == JsonTokenType.Null) 197 | { 198 | value = null; 199 | } 200 | 201 | return value; 202 | } 203 | } -------------------------------------------------------------------------------- /doc/features/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | 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. 4 | 5 | ## Authentication at the Gateway for a Single Page Application 6 | 7 | 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: 8 | 9 | ```cs 10 | builder.Services.AddGateway() 11 | .LoadConfiguration(builder.Configuration.GetSection("Gateway")) 12 | .AddRouting() 13 | .AddAuthentication(); // <-- Add this to activate the authentication feature 14 | ``` 15 | 16 | Extend your `Gateway` configuration section with an `Authentication` section and a route to your frontend. 17 | 18 | ```json 19 | "Gateway": { 20 | "Authentication": { // <-- Add this section and configure it with your values 21 | "Authority": "", 22 | "ClientId": "", 23 | "Scopes": "", 24 | "ClientSecret": "", 25 | "SessionTimeoutInMin": 30, 26 | "UniqueIdentifierClaimType": "" 27 | } 28 | "Routing": { 29 | "Routes": { 30 | "Microservice1": { 31 | "BaseUrl": "http://localhost:5000", 32 | }, 33 | "Frontend": { // <-- Add a route to your frontend with a 'PathMatch' to activate the reverse proxy for this route 34 | "BaseUrl": "http://localhost:4200", 35 | "PathMatch": "{**path}", 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | 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: 43 | 44 | ```js 45 | window.location.href = './login?redirectUri=' + encodeURIComponent(window.origin); 46 | ``` 47 | 48 | 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. 49 | 50 | To trigger the logout flow set the window location to the `./logout` endpoint as shown in the following snippent. 51 | 52 | ```js 53 | window.location.href = './logout'; 54 | ``` 55 | 56 | To get the ID token into the frontend, the frontend can simply call the `./userinfo` enpoint with a standard http GET request. 57 | 58 | ## Authentication at the Resource Servers 59 | 60 | The gateway provides different authentication strategies you can use to make authenticated calls to your resource servers. 61 | 62 | ### Token Pass Through 63 | 64 | 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. 65 | 66 | ```json 67 | "Gateway": { 68 | "Authentication": { 69 | [...] 70 | }, 71 | "Routing": { 72 | "Routes": { 73 | "Microservice1": { 74 | "BaseUrl": "http://localhost:5000", 75 | "Authentication": { 76 | "Strategy": "TokenPassThrough" // <-- Configure the TokenPassThrough auth strategy 77 | } 78 | }, 79 | [...] 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | ### Token Exchange 86 | 87 | 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. 88 | 89 | #### Azure on Behalf of 90 | 91 | 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: 92 | 93 | ```json 94 | "Gateway": { 95 | "Authentication": { // <-- Configure the properties of the AppRegistration for the gateway here 96 | "Authority": "", 97 | "ClientId": "", 98 | "ClientSecret": "", // <-- Important, you need a client secret here to make the token exchange work 99 | "AuthorizationCodeScopes": "", 100 | "SessionTimeoutInMin": 30, 101 | "UniqueIdentifierClaimType": "" 102 | }, 103 | "Routing": { 104 | "Routes": { 105 | "Microservice1": { 106 | "BaseUrl": "http://localhost:5000", 107 | "Authentication": { 108 | "Strategy": "AzureOnBehalfOf", // <-- Set the azure on behalf of auth strategy 109 | "Options": { 110 | "Scope": "api://microservice1/all" // <-- Set the scope of the api you want to request 111 | } 112 | } 113 | }, 114 | [...] 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | ### Client Credentials 121 | 122 | 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. 123 | 124 | #### Standard Client Credentials 125 | 126 | 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: 127 | 128 | ```json 129 | "Gateway": { 130 | "Routing": { 131 | "Routes": { 132 | "Microservice1": { 133 | "BaseUrl": "http://localhost:5000", 134 | "Authentication": { 135 | "Strategy": "ClientCredentials", 136 | "Options": { 137 | "Authority": "", 138 | "ClientId": "", 139 | "ClientSecret": "", 140 | "Scope": "", 141 | } 142 | } 143 | }, 144 | [...] 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | #### Auth0 Client Credentials 151 | 152 | 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: 153 | 154 | ```json 155 | "Gateway": { 156 | "Routing": { 157 | "Routes": { 158 | "Microservice1": { 159 | "BaseUrl": "http://localhost:5000", 160 | "Authentication": { 161 | "Strategy": "Auth0ClientCredentials", 162 | "Options": { 163 | "Authority": "", 164 | "ClientId": "", 165 | "ClientSecret": "", 166 | "Scope": "", 167 | "Audience": "" // <-- This additional parameter is needed by Auth0 168 | } 169 | } 170 | }, 171 | [...] 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | ### Custom Auth Strategy 178 | 179 | ToDo! 180 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Authentication/TokenService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Security.Claims; 7 | using System.Text.Json; 8 | 9 | namespace Fancy.ResourceLinker.Gateway.Authentication; 10 | 11 | /// 12 | /// A token service with handling logic for tokens needed by the gateway authentication feature. 13 | /// 14 | public class TokenService 15 | { 16 | /// 17 | /// The token store. 18 | /// 19 | private readonly ITokenStore _tokenStore; 20 | 21 | /// 22 | /// The token client. 23 | /// 24 | private readonly TokenClient _tokenClient; 25 | 26 | /// 27 | /// The logger. 28 | /// 29 | private readonly ILogger _logger; 30 | 31 | /// 32 | /// The environment. 33 | /// 34 | private readonly IWebHostEnvironment _environment; 35 | 36 | /// 37 | /// The authentication settings. 38 | /// 39 | private readonly GatewayAuthenticationSettings _settings; 40 | 41 | /// 42 | /// Initializes a new instance of the class. 43 | /// 44 | /// The token store. 45 | /// The token client. 46 | public TokenService(ITokenStore tokenStore, 47 | TokenClient tokenClient, 48 | ILogger logger, 49 | IWebHostEnvironment environment, 50 | GatewayAuthenticationSettings settings) 51 | { 52 | _tokenStore = tokenStore; 53 | _tokenClient = tokenClient; 54 | _logger = logger; 55 | _environment = environment; 56 | _settings = settings; 57 | } 58 | 59 | /// 60 | /// Gets or sets the current session identifier. 61 | /// 62 | /// 63 | /// The current session identifier. 64 | /// 65 | internal string? CurrentSessionId { get; set; } 66 | 67 | /// 68 | /// Saves a token for a new session and generates a new unique session id asynchronous. 69 | /// 70 | /// The token response from authorization server. 71 | /// 72 | /// A new unique session id. 73 | /// 74 | internal async Task SaveTokenForNewSessionAsync(OpenIdConnectMessage tokenResponse) 75 | { 76 | // Create a new guid for the new session 77 | string sessionId = Guid.NewGuid().ToString(); 78 | await SaveOrUpdateTokenAsync(sessionId, tokenResponse); 79 | 80 | if(_settings.QueryUserInfoEndpoint) 81 | { 82 | string userinfoClaims = await _tokenClient.QueryUserInfoEndpoint(tokenResponse.AccessToken); 83 | await _tokenStore.SaveOrUpdateUserinfoClaimsAsync(sessionId, userinfoClaims); 84 | } 85 | 86 | return sessionId; 87 | } 88 | 89 | /// 90 | /// Saves or updates a token for an existing session asynchronous. 91 | /// 92 | /// The session identifier. 93 | /// The token response from authorisation server. 94 | internal async Task SaveOrUpdateTokenAsync(string sessionId, OpenIdConnectMessage tokenResponse) 95 | { 96 | string idToken = tokenResponse.IdToken; 97 | string accessToken = tokenResponse.AccessToken; 98 | string refreshToken = tokenResponse.RefreshToken; 99 | DateTime expiresAt = DateTime.UtcNow.AddSeconds(Convert.ToInt32(tokenResponse.ExpiresIn)); 100 | 101 | await _tokenStore.SaveOrUpdateTokensAsync(sessionId, idToken, accessToken, refreshToken, expiresAt); 102 | } 103 | 104 | /// 105 | /// Gets the access token of the current session asynchronous. 106 | /// 107 | /// The access token. 108 | public async Task GetAccessTokenAsync() 109 | { 110 | if(CurrentSessionId == null) 111 | { 112 | throw new NoSessionIdException(); 113 | } 114 | 115 | TokenRecord? tokenRecord = await _tokenStore.GetTokenRecordAsync(CurrentSessionId); 116 | 117 | if(tokenRecord == null) 118 | { 119 | throw new NoTokenForCurrentSessionIdException(); 120 | } 121 | 122 | if(IsExpired(tokenRecord)) 123 | { 124 | _logger.LogInformation("Access Token expired, executing token refresh"); 125 | 126 | // Refresh the token 127 | TokenRefreshResponse? tokenRefresh = await _tokenClient.RefreshAsync(tokenRecord.RefreshToken); 128 | 129 | if(tokenRefresh == null) 130 | { 131 | throw new TokenRefreshException(); 132 | } 133 | 134 | if (_environment.IsDevelopment()) 135 | _logger.LogInformation($"Received new tokens via token refresh \n " + 136 | $"IdToken: {tokenRefresh.IdToken} \n" + 137 | $"AccessToken: {tokenRefresh.AccessToken} \n" + 138 | $"RefreshToken: {tokenRefresh.RefreshToken}"); 139 | else 140 | _logger.LogInformation("Received new tokens via token refresh"); 141 | 142 | DateTime expiresAt = DateTime.UtcNow.AddSeconds(Convert.ToInt32(tokenRefresh.ExpiresIn)); 143 | await _tokenStore.SaveOrUpdateTokensAsync(CurrentSessionId, tokenRefresh.IdToken, tokenRefresh.AccessToken, tokenRefresh.RefreshToken, expiresAt); 144 | return tokenRefresh.AccessToken; 145 | } 146 | else 147 | { 148 | return tokenRecord.AccessToken; 149 | } 150 | } 151 | 152 | /// 153 | /// Gets the access token claims of the current session asynchronous. 154 | /// 155 | /// 156 | public async Task?> GetAccessTokenClaimsAsync() 157 | { 158 | if (CurrentSessionId == null) return null; 159 | 160 | TokenRecord? tokenRecord = await _tokenStore.GetTokenRecordAsync(CurrentSessionId); 161 | 162 | if (tokenRecord == null) 163 | { 164 | throw new NoTokenForCurrentSessionIdException(); 165 | } 166 | 167 | JwtSecurityToken accessToken = new JwtSecurityTokenHandler().ReadJwtToken(tokenRecord.AccessToken); 168 | 169 | return accessToken.Claims; 170 | } 171 | 172 | /// 173 | /// Gets the identity claims of the current session asynchronous. 174 | /// 175 | /// 176 | public async Task?> GetIdentityClaimsAsync() 177 | { 178 | if (CurrentSessionId == null) return null; 179 | 180 | TokenRecord? tokenRecord = await _tokenStore.GetTokenRecordAsync(CurrentSessionId); 181 | 182 | if (tokenRecord == null) 183 | { 184 | throw new NoTokenForCurrentSessionIdException(); 185 | } 186 | 187 | JwtSecurityToken idToken = new JwtSecurityTokenHandler().ReadJwtToken(tokenRecord.IdToken); 188 | 189 | return idToken.Claims; 190 | } 191 | 192 | public async Task?> GetUserinfoClaimsAsync() 193 | { 194 | if (CurrentSessionId == null) return null; 195 | 196 | TokenRecord? tokenRecord = await _tokenStore.GetTokenRecordAsync(CurrentSessionId); 197 | 198 | if (tokenRecord == null) 199 | { 200 | throw new NoTokenForCurrentSessionIdException(); 201 | } 202 | 203 | if(tokenRecord.UserinfoClaims == null) 204 | { 205 | return null; 206 | } 207 | 208 | Dictionary? userinfos = JsonSerializer.Deserialize>(tokenRecord.UserinfoClaims); 209 | return userinfos?.Select(userinfo => new Claim(userinfo.Key, userinfo.Value.ToString())).ToList(); 210 | } 211 | 212 | /// 213 | /// Determines whether the specified tokent record is expired. 214 | /// 215 | /// The tokent record. 216 | /// 217 | /// true if the specified tokent record is expired; otherwise, false. 218 | /// 219 | private bool IsExpired(TokenRecord tokentRecord) 220 | { 221 | return tokentRecord.ExpiresAt.Subtract(DateTime.UtcNow).TotalSeconds < 30; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Hateoas/UrlHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | using Fancy.ResourceLinker.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Fancy.ResourceLinker.Hateoas; 7 | 8 | /// 9 | /// Class with extension methods for the "UrlHelper" to easily create links to controller methods. 10 | /// 11 | public static class UrlHelperExtensions 12 | { 13 | /// 14 | /// Helper method to create links to controller via linq expressions. 15 | /// 16 | /// The type of the controller. 17 | /// The URL helper. 18 | /// The method expression. 19 | /// The constructed link. 20 | public static string LinkTo(this IUrlHelper urlHelper, Expression> methodExpression) where TController : ControllerBase 21 | { 22 | if (urlHelper == null) throw new ArgumentNullException(nameof(urlHelper)); 23 | if (methodExpression == null) throw new ArgumentNullException(nameof(methodExpression)); 24 | 25 | var methodCallExpression = methodExpression.Body as MethodCallExpression; 26 | 27 | if (methodCallExpression == null) throw new ArgumentException("The provided expression needs to be a method call expression", nameof(methodExpression)); 28 | 29 | return LinkTo(urlHelper, methodCallExpression); 30 | } 31 | 32 | /// 33 | /// Helper method to create links to controller via linq expressions for async controller methods. 34 | /// 35 | /// The type of the controller. 36 | /// The URL helper. 37 | /// The method expression. 38 | /// The constructed link. 39 | public static string LinkTo(this IUrlHelper urlHelper, Expression> methodExpression) where TController : ControllerBase 40 | { 41 | if (urlHelper == null) throw new ArgumentNullException(nameof(urlHelper)); 42 | if (methodExpression == null) throw new ArgumentNullException(nameof(methodExpression)); 43 | 44 | var methodCallExpression = methodExpression.Body as MethodCallExpression; 45 | 46 | if (methodCallExpression == null) throw new ArgumentException("The provided expression needs to be a method call expression", nameof(methodExpression)); 47 | 48 | return LinkTo(urlHelper, methodCallExpression); 49 | } 50 | 51 | /// 52 | /// Creates a ResourceAction object from a controller method expression. 53 | /// 54 | /// The type of the controller. 55 | /// The URL helper. 56 | /// The method expression. 57 | public static ResourceAction ActionTo(this IUrlHelper urlHelper, Expression> methodExpression) where TController : ControllerBase 58 | { 59 | if (urlHelper == null) throw new ArgumentNullException(nameof(urlHelper)); 60 | if (methodExpression == null) throw new ArgumentNullException(nameof(methodExpression)); 61 | 62 | var methodCallExpression = methodExpression.Body as MethodCallExpression; 63 | 64 | if (methodCallExpression == null) throw new ArgumentException("The provided expression needs to be a method call expression", nameof(methodExpression)); 65 | 66 | string href = LinkTo(urlHelper, methodCallExpression); 67 | string verb = GetHttpVerb(methodCallExpression.Method); 68 | 69 | return new ResourceAction(verb, href); 70 | } 71 | 72 | /// 73 | /// Helper method to create links to a known relative Url. 74 | /// 75 | /// The URL helper. 76 | /// The relative URL. 77 | /// 78 | /// The constructed link. 79 | /// 80 | public static string LinkTo(this IUrlHelper urlHelper, string relativeUrl) 81 | { 82 | return CreateAbsoluteUrl(urlHelper, relativeUrl); 83 | } 84 | 85 | /// 86 | /// Builds a link to a controller method. 87 | /// 88 | /// The URL helper. 89 | /// The method call expression. 90 | /// The constructed link. 91 | private static string LinkTo(IUrlHelper urlHelper, MethodCallExpression methodCallExpression) 92 | { 93 | // Read the route parameters and convert them into a dictionary containing the value to each param 94 | Dictionary routeValues = methodCallExpression.Method.GetParameters() 95 | .Select(p => GetParameterValue(methodCallExpression, p)) 96 | .ToDictionary(p => p.Item1, p => p.Item2); 97 | 98 | string controller = methodCallExpression.Method.DeclaringType!.Name; 99 | string action = methodCallExpression.Method.Name; 100 | 101 | // Remove controller suffix 102 | if (controller.EndsWith("Controller")) 103 | { 104 | controller = controller.Substring(0, controller.Length - "Controller".Length); 105 | } 106 | 107 | // Remove action suffix 108 | if(action.EndsWith("Async")) 109 | { 110 | action = action.Substring(0, action.Length - "Async".Length); 111 | } 112 | 113 | // Retrieve url to action 114 | string? actionUrl = urlHelper.Action(action, controller, routeValues); 115 | 116 | if(actionUrl == null) 117 | { 118 | throw new ArgumentException($"Could not find action with name '{action}' on controller '{controller}'"); 119 | } 120 | 121 | return CreateAbsoluteUrl(urlHelper, actionUrl); 122 | } 123 | 124 | /// 125 | /// Extracts the parameter value to from one parameter. 126 | /// 127 | /// The method call expression. 128 | /// The parameter information. 129 | /// The parameter values. 130 | private static Tuple GetParameterValue(MethodCallExpression methodCallExpression, ParameterInfo parameterInfo) 131 | { 132 | if(parameterInfo.Name == null) throw new ArgumentException("ParameterInfo needs to have a name", nameof(parameterInfo)); 133 | 134 | Expression arg = methodCallExpression.Arguments[parameterInfo.Position]; 135 | LambdaExpression lambda = Expression.Lambda(arg); 136 | 137 | object? value = lambda.Compile().DynamicInvoke(); 138 | 139 | return new Tuple(parameterInfo.Name, value); 140 | } 141 | 142 | /// 143 | /// Gets the HTTP verb from the first HTTP verb attribute of a method via reflection. 144 | /// 145 | /// The method information. 146 | /// The HTTP verb as string (GET, POST, PUT, DELETE, PATCH) or null if no verb attribute is found. 147 | private static string GetHttpVerb(MethodInfo methodInfo) 148 | { 149 | var attribute = methodInfo.GetCustomAttributes() 150 | .FirstOrDefault(attr => 151 | attr is HttpGetAttribute || 152 | attr is HttpPostAttribute || 153 | attr is HttpPutAttribute || 154 | attr is HttpDeleteAttribute || 155 | attr is HttpPatchAttribute); 156 | 157 | return attribute switch 158 | { 159 | HttpGetAttribute => "GET", 160 | HttpPostAttribute => "POST", 161 | HttpPutAttribute => "PUT", 162 | HttpDeleteAttribute => "DELETE", 163 | HttpPatchAttribute => "PATCH", 164 | _ => throw new ArgumentException($"No HTTP verb attribute found on method {methodInfo.Name}") 165 | }; 166 | } 167 | 168 | /// 169 | /// Creates the absolute URL for a relative URL. 170 | /// 171 | /// The URL helper. 172 | /// The relative URL. 173 | /// The absolute Url. 174 | private static string CreateAbsoluteUrl(IUrlHelper urlHelper, string relativeUrl) 175 | { 176 | string baseUrl; 177 | if (urlHelper.ActionContext.HttpContext.Request.Headers.ContainsKey("X-Forwarded-Proto") 178 | && urlHelper.ActionContext.HttpContext.Request.Headers.ContainsKey("X-Forwarded-Host")) 179 | { 180 | baseUrl = urlHelper.ActionContext.HttpContext.Request.Headers["X-Forwarded-Proto"]!; 181 | baseUrl += "://"; 182 | baseUrl += urlHelper.ActionContext.HttpContext.Request.Headers["X-Forwarded-Host"]!; 183 | } 184 | else 185 | { 186 | string scheme = urlHelper.ActionContext.HttpContext.Request.Scheme; 187 | if (urlHelper.ActionContext.HttpContext.Request.Headers.ContainsKey("X-Forwarded-Proto")) 188 | { 189 | scheme = urlHelper.ActionContext.HttpContext.Request.Headers["X-Forwarded-Proto"]!; 190 | } 191 | 192 | baseUrl = scheme + "://" + urlHelper.ActionContext.HttpContext.Request.Host.Value; 193 | } 194 | 195 | return CombineUri(baseUrl, relativeUrl); 196 | } 197 | 198 | /// 199 | /// Combines a URI from two parts. 200 | /// 201 | /// The uri1. 202 | /// The uri2. 203 | /// The combined uri. 204 | private static string CombineUri(string uri1, string uri2) 205 | { 206 | uri1 = uri1.TrimEnd('/'); 207 | uri2 = uri2.TrimStart('/'); 208 | return string.Format("{0}/{1}", uri1, uri2); 209 | } 210 | } -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fancy.ResourceLinker.Gateway.AntiForgery; 2 | using Fancy.ResourceLinker.Gateway.Authentication; 3 | using Fancy.ResourceLinker.Gateway.Common; 4 | using Fancy.ResourceLinker.Gateway.Routing; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Fancy.ResourceLinker.Gateway; 9 | 10 | /// 11 | /// A class with the required context to build a gateway configuration. 12 | /// 13 | public class GatewayBuilder 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The services. 19 | internal GatewayBuilder(IServiceCollection services) 20 | { 21 | Services = services; 22 | } 23 | 24 | /// 25 | /// Gets the services. 26 | /// 27 | /// 28 | /// The services. 29 | /// 30 | public IServiceCollection Services { get; } 31 | } 32 | 33 | /// 34 | /// A class with the a configured context to build a gateway. 35 | /// 36 | public class ConfiguredGatewayBuilder : GatewayBuilder 37 | { 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// The services. 42 | /// The settings. 43 | internal ConfiguredGatewayBuilder(IServiceCollection services, GatewaySettings settings) : base(services) 44 | { 45 | Settings = settings; 46 | } 47 | 48 | /// 49 | /// Gets the settings. 50 | /// 51 | /// 52 | /// The settings. 53 | /// 54 | public GatewaySettings Settings { get; } 55 | } 56 | 57 | /// 58 | /// A class with the required context to build a antiforgery configuration. 59 | /// 60 | public class GatewayAntiForgeryBuilder : GatewayBuilder 61 | { 62 | /// 63 | /// Initializes a new instance of the class. 64 | /// 65 | /// The services. 66 | public GatewayAntiForgeryBuilder(IServiceCollection services) : base(services) { } 67 | } 68 | 69 | /// 70 | /// A class with the required context to build a authentication configuration. 71 | /// 72 | public class GatewayAuthenticationBuilder : GatewayBuilder 73 | { 74 | /// 75 | /// Initializes a new instance of the class. 76 | /// 77 | /// The services. 78 | public GatewayAuthenticationBuilder(IServiceCollection services) : base(services) { } 79 | } 80 | 81 | /// 82 | /// A class with the required context to build a routing configuration. 83 | /// 84 | public class GatewayRoutingBuilder : GatewayBuilder 85 | { 86 | /// 87 | /// Initializes a new instance of the class. 88 | /// 89 | /// The services. 90 | public GatewayRoutingBuilder(IServiceCollection services) : base(services) { } 91 | } 92 | 93 | /// 94 | /// Extension class with helpers to easily register the gateway to ioc container. 95 | /// 96 | public static class ServiceCollectionExtensions 97 | { 98 | /// 99 | /// Adds the gateway to the ioc container. 100 | /// 101 | /// The services. 102 | /// A gateway builder. 103 | public static GatewayBuilder AddGateway(this IServiceCollection services) 104 | { 105 | GatewayCommon.AddGatewayCommonServices(services); 106 | return new GatewayBuilder(services); 107 | } 108 | 109 | /// 110 | /// Loads the configuration for the gateway. 111 | /// 112 | /// The gateway builder. 113 | /// The configuration. 114 | /// A configured gateway builder. 115 | public static ConfiguredGatewayBuilder LoadConfiguration(this GatewayBuilder gatewayBuilder, IConfiguration config) 116 | { 117 | GatewaySettings? settings = config.Get(); 118 | 119 | if(settings == null) 120 | { 121 | throw new InvalidOperationException("The provided configuration does not contain proper settings"); 122 | } 123 | 124 | if (settings.Authentication != null) gatewayBuilder.Services.AddSingleton(settings.Authentication); 125 | if (settings.Routing != null) gatewayBuilder.Services.AddSingleton(settings.Routing); 126 | ConfiguredGatewayBuilder configuredGatewayBuilder = new ConfiguredGatewayBuilder(gatewayBuilder.Services, settings); 127 | return configuredGatewayBuilder; 128 | } 129 | 130 | /// 131 | /// Adds the anti forgery feature to the gateway. 132 | /// 133 | /// A subclass of the gateway builder. 134 | /// The gateway builder. 135 | /// A subclass of the gateway builder. 136 | public static T AddAntiForgery(this T gatewayBuilder) where T : GatewayBuilder 137 | { 138 | GatewayAntiForgery.AddGatewayAntiForgery(gatewayBuilder.Services); 139 | return gatewayBuilder; 140 | } 141 | 142 | /// 143 | /// Adds the anti forgery. 144 | /// 145 | /// A subclass of the gateway builder. 146 | /// The gateway builder. 147 | /// The build options. 148 | /// A subclass of the gateway builder. 149 | public static T AddAntiForgery(this T gatewayBuilder, Action buildOptions) where T : GatewayBuilder 150 | { 151 | GatewayAntiForgery.AddGatewayAntiForgery(gatewayBuilder.Services); 152 | buildOptions(new GatewayAntiForgeryBuilder(gatewayBuilder.Services)); 153 | return gatewayBuilder; 154 | } 155 | 156 | /// 157 | /// Adds the authentication feature to the gateway with the default in memory token store. 158 | /// 159 | /// The configured gateway builder. 160 | /// A configured gateway builder. 161 | public static ConfiguredGatewayBuilder AddAuthentication(this ConfiguredGatewayBuilder configuredGatewayBuilder) 162 | { 163 | AddAuthentication(configuredGatewayBuilder, options => options.UseInMemoryTokenStore()); 164 | return configuredGatewayBuilder; 165 | } 166 | 167 | /// 168 | /// Adds the authentication feature to the gateway. 169 | /// 170 | /// The configured gateway builder. 171 | /// The build options. 172 | /// A configured gateway builder. 173 | public static ConfiguredGatewayBuilder AddAuthentication(this ConfiguredGatewayBuilder configuredGatewayBuilder, 174 | Action buildOptions) 175 | { 176 | if(configuredGatewayBuilder.Settings.Authentication == null) 177 | { 178 | throw new InvalidOperationException("You can add authentication only if you have provided settings for it"); 179 | } 180 | 181 | configuredGatewayBuilder.Settings.Authentication.Validate(); 182 | 183 | GatewayAuthentication.AddGatewayAuthentication(configuredGatewayBuilder.Services, configuredGatewayBuilder.Settings.Authentication); 184 | buildOptions(new GatewayAuthenticationBuilder(configuredGatewayBuilder.Services)); 185 | return configuredGatewayBuilder; 186 | } 187 | 188 | /// 189 | /// Adds the routing feature to the gateway with the default in memory configuration. 190 | /// 191 | /// The configured gateway builder. 192 | /// A configured gateway builder. 193 | public static ConfiguredGatewayBuilder AddRouting(this ConfiguredGatewayBuilder configuredGatewayBuilder) 194 | { 195 | AddRouting(configuredGatewayBuilder, options => options.UseInMemoryResourceCache()); 196 | return configuredGatewayBuilder; 197 | } 198 | 199 | /// 200 | /// Adds the routing feature to the gateway. 201 | /// 202 | /// The configured gateway builder. 203 | /// The build options. 204 | /// A configured gateway builder. 205 | public static ConfiguredGatewayBuilder AddRouting(this ConfiguredGatewayBuilder configuredGatewayBuilder, 206 | Action buildOptions) 207 | { 208 | if (configuredGatewayBuilder.Settings.Routing == null) 209 | { 210 | throw new InvalidOperationException("You can add routing only if you have provided settings for it"); 211 | } 212 | 213 | configuredGatewayBuilder.Settings.Routing.Validate(); 214 | 215 | GatewayRouting.AddGatewayRouting(configuredGatewayBuilder.Services, configuredGatewayBuilder.Settings.Routing); 216 | buildOptions(new GatewayRoutingBuilder(configuredGatewayBuilder.Services)); 217 | return configuredGatewayBuilder; 218 | } 219 | 220 | /// 221 | /// Uses the in memory token store for the authentication feature. 222 | /// 223 | /// The gateway authentication builder. 224 | public static void UseInMemoryTokenStore(this GatewayAuthenticationBuilder gatewayAuthenticationBuilder) 225 | { 226 | gatewayAuthenticationBuilder.Services.AddSingleton(); 227 | } 228 | 229 | /// 230 | /// Uses the in memory resource cache for the routing builder. 231 | /// 232 | /// The gateway routing builder. 233 | public static void UseInMemoryResourceCache(this GatewayRoutingBuilder gatewayRoutingBuilder) 234 | { 235 | gatewayRoutingBuilder.Services.AddSingleton(); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Fancy.ResourceLinker.Gateway/Routing/Auth/AzureOnBehalfOfAuthStrategy.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Fancy.ResourceLinker.Gateway.Authentication; 3 | using Fancy.ResourceLinker.Gateway.Common; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Primitives; 8 | using System.Net.Http.Headers; 9 | using System.Net.Http.Json; 10 | using System.Text.Json.Serialization; 11 | 12 | namespace Fancy.ResourceLinker.Gateway.Routing.Auth; 13 | 14 | /// 15 | /// A model to deserialize the on behalf of token response. 16 | /// 17 | class OnBehalfOfTokenResponse 18 | { 19 | /// 20 | /// Gets or sets the access token. 21 | /// 22 | /// 23 | /// The access token. 24 | /// 25 | [JsonPropertyName("access_token")] 26 | public string AccessToken { get; set; } = ""; 27 | 28 | /// 29 | /// Gets or sets the refresh token. 30 | /// 31 | /// 32 | /// The refresh token. 33 | /// 34 | [JsonPropertyName("refresh_token")] 35 | public string RefreshToken { get; set; } = ""; 36 | 37 | /// 38 | /// Gets or sets the expires in. 39 | /// 40 | /// 41 | /// The expires in. 42 | /// 43 | [JsonPropertyName("expires_in")] 44 | public long ExpiresIn { get; set; } 45 | 46 | /// 47 | /// Gets or sets the expires at. 48 | /// 49 | /// 50 | /// The expires at. 51 | /// 52 | [JsonIgnore] 53 | public DateTime ExpiresAt { get; set; } 54 | } 55 | 56 | /// 57 | /// An auth strategy which just passes through the current token 58 | /// 59 | internal class AzureOnBehalfOfAuthStrategy : IRouteAuthenticationStrategy 60 | { 61 | /// 62 | /// The name of the auth strategy. 63 | /// 64 | public const string NAME = "AzureOnBehalfOf"; 65 | 66 | /// 67 | /// The HTTP client. 68 | /// 69 | private readonly HttpClient _httpClient = new HttpClient(); 70 | 71 | /// 72 | /// The discovery document service. 73 | /// 74 | private readonly DiscoveryDocumentService _discoveryDocumentService; 75 | 76 | /// 77 | /// The logger. 78 | /// 79 | private readonly ILogger _logger; 80 | 81 | /// 82 | /// The gateway authentication settings. 83 | /// 84 | private GatewayAuthenticationSettings? _gatewayAuthenticationSettings; 85 | 86 | /// 87 | /// The route authentication settings. 88 | /// 89 | private RouteAuthenticationSettings? _routeAuthenticationSettings; 90 | 91 | /// 92 | /// The discovery document. 93 | /// 94 | private DiscoveryDocument? _discoveryDocument; 95 | 96 | /// 97 | /// The currently in-use token responses per SessionId. 98 | /// 99 | // private ConcurrentDictionary _cachedTokenResponses; 100 | private ConcurrentDictionary _cachedTokenResponses = new (); 101 | 102 | /// 103 | /// Initializes a new instance of the class. 104 | /// 105 | /// The discovery document service. 106 | /// The logger. 107 | public AzureOnBehalfOfAuthStrategy(DiscoveryDocumentService discoveryDocumentService, ILogger logger) 108 | { 109 | _discoveryDocumentService = discoveryDocumentService; 110 | _logger = logger; 111 | } 112 | 113 | /// 114 | /// Gets the name of the strategy. 115 | /// 116 | /// 117 | /// The name. 118 | /// 119 | public string Name => NAME; 120 | 121 | /// 122 | /// Initializes the authentication strategy based on the gateway authentication settings and the route authentication settings asynchronous. 123 | /// 124 | /// The gateway authentication settigns. 125 | /// The route authentication settigns. 126 | public async Task InitializeAsync(GatewayAuthenticationSettings? gatewayAuthenticationSettings, RouteAuthenticationSettings routeAuthenticationSettings) 127 | { 128 | _gatewayAuthenticationSettings = gatewayAuthenticationSettings; 129 | 130 | if(_gatewayAuthenticationSettings == null) 131 | { 132 | throw new InvalidOperationException($"The {NAME} route authentication strategy needs to have the gateway authenticaion configured"); 133 | } 134 | 135 | if(string.IsNullOrEmpty(_gatewayAuthenticationSettings.ClientId) || string.IsNullOrEmpty(_gatewayAuthenticationSettings.ClientSecret)) 136 | { 137 | throw new InvalidOperationException($"The {NAME} route authentication strategy needs to have set the 'ClientId' and 'Client Secret' settings at the gateway authentication settings."); 138 | } 139 | 140 | _routeAuthenticationSettings = routeAuthenticationSettings; 141 | _discoveryDocument = await _discoveryDocumentService.LoadDiscoveryDocumentAsync(_gatewayAuthenticationSettings.Authority); 142 | } 143 | 144 | /// 145 | /// Sets the authentication to an http context asynchronous. 146 | /// 147 | /// The http context. 148 | /// 149 | /// A task indicating the completion of the asynchronous operation 150 | /// 151 | public async Task SetAuthenticationAsync(HttpContext context) 152 | { 153 | string accessToken = await GetAccessTokenAsync(context.RequestServices); 154 | context.Request.Headers.Authorization = new StringValues("Bearer " + accessToken); 155 | } 156 | 157 | /// 158 | /// Sets the authentication to an http request message asynchronous. 159 | /// 160 | /// The current service provider. 161 | /// The http request message. 162 | /// 163 | /// A task indicating the completion of the asynchronous operation 164 | /// 165 | public async Task SetAuthenticationAsync(IServiceProvider serviceProvider, HttpRequestMessage request) 166 | { 167 | string accessToken = await GetAccessTokenAsync(serviceProvider); 168 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 169 | } 170 | 171 | /// 172 | /// Gets the access token asynchronous. 173 | /// 174 | /// The access token. 175 | private async Task GetAccessTokenAsync(IServiceProvider serviceProvider) 176 | { 177 | TokenService tokenService = GetTokenService(serviceProvider); 178 | 179 | var tokenResponse = _cachedTokenResponses.ContainsKey(tokenService.CurrentSessionId!) 180 | ? _cachedTokenResponses[tokenService.CurrentSessionId!] 181 | : null; 182 | 183 | if (tokenResponse == null || IsExpired(tokenResponse)) 184 | { 185 | tokenResponse = await GetOnBehalfOfTokenAsync(tokenService); 186 | _cachedTokenResponses.TryAdd(tokenService.CurrentSessionId!, tokenResponse); 187 | } 188 | 189 | return tokenResponse.AccessToken; 190 | } 191 | 192 | /// 193 | /// Gets a token via an on behalf of flow asynchronous. 194 | /// 195 | /// The service provider. 196 | /// The exchanged token. 197 | private async Task GetOnBehalfOfTokenAsync(TokenService tokenService) 198 | { 199 | if(_gatewayAuthenticationSettings == null || _routeAuthenticationSettings == null) 200 | { 201 | throw new InvalidOperationException("Initialize the auth strategy first before using it"); 202 | } 203 | 204 | string accessToken = await tokenService.GetAccessTokenAsync(); 205 | 206 | var payload = new Dictionary 207 | { 208 | { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }, 209 | { "client_id", _gatewayAuthenticationSettings.ClientId }, 210 | { "client_secret", _gatewayAuthenticationSettings.ClientSecret }, 211 | { "assertion", accessToken }, 212 | { "scope", _routeAuthenticationSettings.Options["Scope"] }, 213 | { "requested_token_use", "on_behalf_of" }, 214 | }; 215 | 216 | FormUrlEncodedContent content = new FormUrlEncodedContent(payload); 217 | HttpResponseMessage httpResponse = await _httpClient.PostAsync(_discoveryDocument?.TokenEndpoint, content); 218 | 219 | try 220 | { 221 | httpResponse.EnsureSuccessStatusCode(); 222 | } 223 | catch(HttpRequestException) 224 | { 225 | string errorResponse = new StreamReader(httpResponse.Content.ReadAsStream()).ReadToEnd(); 226 | _logger.LogError("Error on on behalf of token exchange. Response from server " + errorResponse); 227 | throw; 228 | } 229 | 230 | OnBehalfOfTokenResponse? result = await httpResponse.Content.ReadFromJsonAsync(); 231 | 232 | if(result == null) 233 | { 234 | throw new InvalidOperationException("Could not deserialize token response from azure on behalf of flow"); 235 | } 236 | 237 | result.ExpiresAt = DateTime.UtcNow.AddSeconds(Convert.ToInt32(result.ExpiresIn)); 238 | 239 | return result; 240 | } 241 | 242 | /// 243 | /// Get the token service. 244 | /// 245 | /// The service provider. 246 | /// An instance of the current token service. 247 | private TokenService GetTokenService(IServiceProvider serviceProvider) 248 | { 249 | try 250 | { 251 | return serviceProvider.GetRequiredService(); 252 | } 253 | catch (InvalidOperationException e) 254 | { 255 | throw new InvalidOperationException("A token service in a scope is needed to use the azure on behalf of flow", e); 256 | } 257 | } 258 | 259 | /// Determines whether the specified token response is expired. 260 | /// 261 | /// The response. 262 | /// 263 | /// true if the specified response is expired; otherwise, false. 264 | /// 265 | private bool IsExpired(OnBehalfOfTokenResponse response) 266 | { 267 | return response.ExpiresAt.Subtract(DateTime.UtcNow).TotalSeconds < 30; 268 | } 269 | } 270 | --------------------------------------------------------------------------------