├── 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 | 
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