Compare commits
148 Commits
6553104f8d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f5a33f7ec | ||
|
|
ae28159562 | ||
|
|
2665321c8f | ||
|
|
0460466364 | ||
|
|
3336487bde | ||
|
|
addba9cdfa | ||
|
|
74c229bc2d | ||
|
|
dae633b66d | ||
|
|
c3794f1e65 | ||
|
|
019abaffa6 | ||
|
|
bac1fb6054 | ||
|
|
2c330a9dff | ||
|
|
d3b8f400e5 | ||
|
|
358cfdb707 | ||
|
|
cf375a587e | ||
|
|
a429c65ead | ||
|
|
79aebe4ef7 | ||
|
|
b4366e5bbb | ||
|
|
fab002a20c | ||
|
|
51492110a7 | ||
|
|
421f2657dd | ||
|
|
a77c70f655 | ||
|
|
031f830b8f | ||
|
|
5f9efa3bb0 | ||
|
|
d46dbbb877 | ||
|
|
e194cd8054 | ||
|
|
d21e0c06e7 | ||
|
|
dd62af5ada | ||
|
|
b4068eff8e | ||
|
|
3b0428130a | ||
|
|
4ccf7a20b3 | ||
|
|
29ad0554bc | ||
|
|
583864469c | ||
|
|
85ccc52ca1 | ||
|
|
a69e13c2ab | ||
|
|
8ef879a663 | ||
|
|
ef6d834448 | ||
|
|
1db1b35f3c | ||
|
|
74444d301d | ||
|
|
2378b93579 | ||
|
|
85a047467e | ||
|
|
106d31b068 | ||
|
|
48f5c69c91 | ||
|
|
0235f81003 | ||
|
|
01613f2e46 | ||
|
|
6ac2c86520 | ||
|
|
4e941ed35f | ||
|
|
e925c175a0 | ||
|
|
eaf41adb58 | ||
|
|
a2c74cbdd9 | ||
|
|
63c37551be | ||
|
|
6198008475 | ||
|
|
8682f1f9e0 | ||
|
|
fb486296f2 | ||
|
|
aa2572fd17 | ||
|
|
7153d6ec46 | ||
|
|
4e3448b4d4 | ||
|
|
36891b5abb | ||
|
|
6664a1f342 | ||
|
|
b2a287cab5 | ||
|
|
db52e97d03 | ||
|
|
4c001d4087 | ||
|
|
3c37176d5e | ||
|
|
0935573b93 | ||
|
|
f30f1f127d | ||
|
|
1fe3fb9008 | ||
|
|
d21da5028e | ||
|
|
062942b2d2 | ||
|
|
c47197606b | ||
|
|
137ccaa563 | ||
|
|
4062fe750a | ||
|
|
cb6ec8b5e6 | ||
|
|
7873542aca | ||
|
|
6694e4b626 | ||
|
|
0cce082cb7 | ||
|
|
7f39cbe24a | ||
|
|
484cc86a29 | ||
|
|
5ab1f24ce5 | ||
|
|
33ead6ebf4 | ||
|
|
64717fbba5 | ||
|
|
b6d86d3d0d | ||
|
|
5f9926e911 | ||
|
|
319763040c | ||
|
|
e474cf38d4 | ||
|
|
5092890f14 | ||
|
|
27c2c0b4cb | ||
|
|
9d609dd5ac | ||
|
|
360d91353b | ||
|
|
7c5a545926 | ||
|
|
18d7c475ff | ||
|
|
5886e076f4 | ||
|
|
8e979fa14d | ||
|
|
5aab46a221 | ||
|
|
9fee7ea381 | ||
|
|
cfe5df4b1d | ||
|
|
31ccd93b0d | ||
|
|
48970a1e13 | ||
|
|
0614b205bd | ||
|
|
b533634e14 | ||
|
|
766e4e6d27 | ||
|
|
878e927be9 | ||
|
|
24a0efb979 | ||
|
|
8e450f7934 | ||
|
|
77bbcfe4f1 | ||
|
|
c8eacc1d54 | ||
|
|
bea08ce06c | ||
|
|
bf12c889f3 | ||
|
|
a2f4fcfbe0 | ||
|
|
6245a94f43 | ||
|
|
f562690b19 | ||
|
|
54ecf1f4da | ||
|
|
3a79bb7984 | ||
|
|
98a4e2ba5c | ||
|
|
efae188d5c | ||
|
|
f77a68be8d | ||
|
|
b25c9538a4 | ||
|
|
17c00240a6 | ||
|
|
79e3dbd5d8 | ||
|
|
8b5c477b2b | ||
|
|
0a61586e39 | ||
|
|
47aeb49a40 | ||
|
|
a1f996b328 | ||
|
|
110b102926 | ||
|
|
ddc55e0fd9 | ||
|
|
c4f1a9498b | ||
|
|
ffad37a517 | ||
|
|
ccd716badb | ||
|
|
077635e94b | ||
|
|
c6c4d0bd04 | ||
|
|
a73885286f | ||
|
|
d6315ce8a5 | ||
|
|
7bab2657d4 | ||
|
|
79eaa06ed4 | ||
|
|
69efe28310 | ||
|
|
cd4428b8f0 | ||
|
|
a66570bebb | ||
|
|
0a3e1566eb | ||
|
|
de296d34f3 | ||
|
|
314bb08125 | ||
|
|
9c5c0dc61b | ||
|
|
3231aa3299 | ||
|
|
9d35881327 | ||
|
|
f898d8c4a4 | ||
|
|
82f23d447b | ||
|
|
b1bfc46a60 | ||
|
|
8e5180188e | ||
|
|
404ab74ce1 | ||
|
|
34b939f75a |
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Description>DigitalData.Auth.Abstractions defines lightweight interfaces for sending and receiving authentication keys in .NET applications. It provides a unified IAuthClient for managing connections and errors, enabling seamless integration with authentication systems.</Description>
|
||||||
|
<PackageId>DigitalData.Auth.Abstractions</PackageId>
|
||||||
|
<Version>1.2.0</Version>
|
||||||
|
<Company>Digital Data GmbH</Company>
|
||||||
|
<Product>Digital Data GmbH</Product>
|
||||||
|
<Copyright>Copyright 2025</Copyright>
|
||||||
|
<PackageProjectUrl>http://git.dd:3000/AppStd/DigitalData.Auth</PackageProjectUrl>
|
||||||
|
<PackageIcon>auth_icon.png</PackageIcon>
|
||||||
|
<RepositoryUrl>http://git.dd:3000/AppStd/DigitalData.Auth</RepositoryUrl>
|
||||||
|
<PackageTags>Digital Data Auth Authorization Authentication Abstractions</PackageTags>
|
||||||
|
<AssemblyVersion>1.2.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.2.0</FileVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="PublicKey.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\..\nuget-package-icons\auth_icon.png">
|
||||||
|
<Pack>True</Pack>
|
||||||
|
<PackagePath>\</PackagePath>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
DigitalData.Auth.Abstractions/IAuthClient.cs
Normal file
8
DigitalData.Auth.Abstractions/IAuthClient.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace DigitalData.Auth.Abstractions;
|
||||||
|
|
||||||
|
public interface IAuthClient : IAuthListenHandler, IAuthSenderHandler
|
||||||
|
{
|
||||||
|
bool IsConnected { get; }
|
||||||
|
|
||||||
|
Task StartAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
6
DigitalData.Auth.Abstractions/IAuthListenHandler.cs
Normal file
6
DigitalData.Auth.Abstractions/IAuthListenHandler.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DigitalData.Auth.Abstractions;
|
||||||
|
|
||||||
|
public interface IAuthListenHandler
|
||||||
|
{
|
||||||
|
Task ReceivePublicKeyAsync(string issuer, string audience, string value);
|
||||||
|
}
|
||||||
8
DigitalData.Auth.Abstractions/IAuthSenderHandler.cs
Normal file
8
DigitalData.Auth.Abstractions/IAuthSenderHandler.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace DigitalData.Auth.Abstractions;
|
||||||
|
|
||||||
|
public interface IAuthSenderHandler
|
||||||
|
{
|
||||||
|
Task SendPublicKeyAsync(string issuer, string audience, string key);
|
||||||
|
|
||||||
|
Task GetPublicKeyAsync(string issuer, string audience);
|
||||||
|
}
|
||||||
108
DigitalData.Auth.Client/AuthClient.cs
Normal file
108
DigitalData.Auth.Client/AuthClient.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using DigitalData.Auth.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Client;
|
||||||
|
|
||||||
|
public class AuthClient : IAuthClient, IHostedService
|
||||||
|
{
|
||||||
|
private readonly HubConnection _connection;
|
||||||
|
|
||||||
|
private readonly ILogger<AuthClient>? _logger;
|
||||||
|
|
||||||
|
private readonly ClientParams _params;
|
||||||
|
|
||||||
|
public AuthClient(IOptions<ClientParams> paramsOptions, HubConnectionBuilder connectionBuilder, ILogger<AuthClient>? logger = null)
|
||||||
|
{
|
||||||
|
_params = paramsOptions.Value;
|
||||||
|
|
||||||
|
var cnnBuilder = connectionBuilder.WithUrl(_params.Url);
|
||||||
|
|
||||||
|
// set RetryPolicy if it exists
|
||||||
|
if (_params.RetryPolicy is not null)
|
||||||
|
cnnBuilder = cnnBuilder.WithAutomaticReconnect(_params.RetryPolicy);
|
||||||
|
|
||||||
|
_connection = cnnBuilder.Build();
|
||||||
|
|
||||||
|
_connection.On<string, string, string>(nameof(ReceivePublicKeyAsync), ReceivePublicKeyAsync);
|
||||||
|
|
||||||
|
_connection.Reconnected += async cnnId =>
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("Auth-client reconnected. Number of connection attempts {nOfAttempts}.", _nOfAttempts);
|
||||||
|
await GetAllPublicKeysAsync();
|
||||||
|
_nOfAttempts = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
_connection.Reconnecting += (ex) =>
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Auth-client disconnected. Attempt to reconnect every {time} seconds.", _params.RetryDelay!.Value.TotalSeconds);
|
||||||
|
_nOfAttempts += 1;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnected { get; private set; } = false;
|
||||||
|
|
||||||
|
public IEnumerable<ClientPublicKey> PublicKeys => _params.PublicKeys;
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
while(!await TryStartConnectionAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
if (_params.RetryDelay is not null)
|
||||||
|
await Task.Delay(_params.RetryDelay.Value.Milliseconds, cancellationToken);
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsConnected = true;
|
||||||
|
await GetAllPublicKeysAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _nOfAttempts = 0;
|
||||||
|
|
||||||
|
private async Task<bool> TryStartConnectionAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_nOfAttempts += 1;
|
||||||
|
await _connection.StartAsync(cancellationToken);
|
||||||
|
_logger?.LogInformation("Auth-client connection successful. Number of connection attempts {nOfAttempts}.", _nOfAttempts);
|
||||||
|
_nOfAttempts = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch(HttpRequestException ex)
|
||||||
|
{
|
||||||
|
if(_nOfAttempts < 2)
|
||||||
|
{
|
||||||
|
if (_params.RetryDelay is null)
|
||||||
|
_logger?.LogError(ex, "Auth-client connection failed. {message}", ex.Message);
|
||||||
|
else
|
||||||
|
_logger?.LogError(ex, "Auth-client connection failed and will be retried every {time} seconds. The status of being successful can be followed from the information logs.\n{message}", _params.RetryDelay.Value.TotalSeconds, ex.Message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _connection.StopAsync(cancellationToken);
|
||||||
|
IsConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReceivePublicKeyAsync(string issuer, string audience, string message) => Task.Run(() => _params.TriggerOnPublicReceivedEvent(this, issuer, audience, message, _logger));
|
||||||
|
|
||||||
|
public Task SendPublicKeyAsync(string issuer, string audience, string message) => _connection.InvokeAsync(nameof(SendPublicKeyAsync), issuer, audience, message);
|
||||||
|
|
||||||
|
public Task GetPublicKeyAsync(string issuer, string audience) => _connection.InvokeAsync(nameof(GetPublicKeyAsync), issuer, audience);
|
||||||
|
|
||||||
|
public async Task GetAllPublicKeysAsync()
|
||||||
|
{
|
||||||
|
foreach (var publicKey in PublicKeys)
|
||||||
|
await GetPublicKeyAsync(publicKey.Issuer, publicKey.Audience);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
DigitalData.Auth.Client/ClientEvents.cs
Normal file
18
DigitalData.Auth.Client/ClientEvents.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Client;
|
||||||
|
|
||||||
|
public delegate void ClientEvent(AuthClient client, string issuer, string audience, string content, ILogger? logger = null);
|
||||||
|
|
||||||
|
public static class ClientEvents
|
||||||
|
{
|
||||||
|
public static readonly ClientEvent UpdatePublicKeys = (client, issuer, audience, content, logger) =>
|
||||||
|
{
|
||||||
|
if(client.PublicKeys.TryGet(issuer, audience, out var publicKey))
|
||||||
|
publicKey.UpdateContent(content);
|
||||||
|
else
|
||||||
|
logger?.LogWarning(
|
||||||
|
"Failed to update public key: No matching key found. Issuer: {Issuer}, Audience: {Audience}. Ensure the key exists before attempting an update.", issuer, audience);
|
||||||
|
};
|
||||||
|
}
|
||||||
36
DigitalData.Auth.Client/ClientParams.cs
Normal file
36
DigitalData.Auth.Client/ClientParams.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Client;
|
||||||
|
|
||||||
|
public class ClientParams
|
||||||
|
{
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls when the client attempts to reconnect and how many times it does so.
|
||||||
|
/// </summary>
|
||||||
|
public IRetryPolicy? RetryPolicy { get; private set; }
|
||||||
|
|
||||||
|
private TimeSpan? _retryDelay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// To be able to serilize the simple <seealso cref="RetryPolicy"/>
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? RetryDelay
|
||||||
|
{
|
||||||
|
get => _retryDelay;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
RetryPolicy = new RetryPolicy(ctx => RetryDelay);
|
||||||
|
_retryDelay = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event ClientEvent OnPublicKeyReceived = ClientEvents.UpdatePublicKeys;
|
||||||
|
|
||||||
|
internal void TriggerOnPublicReceivedEvent(AuthClient client, string issuer, string audience, string key, ILogger? logger = null)
|
||||||
|
=> OnPublicKeyReceived(client, issuer, audience, key, logger);
|
||||||
|
|
||||||
|
public List<ClientPublicKey> PublicKeys { get; set; } = new();
|
||||||
|
}
|
||||||
45
DigitalData.Auth.Client/ClientPublicKey.cs
Normal file
45
DigitalData.Auth.Client/ClientPublicKey.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using DigitalData.Core.Abstractions.Security.Common;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Key;
|
||||||
|
using DigitalData.Core.Security.RSAKey.Base;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a public RSA key, allowing dynamic updates and PEM import functionality.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientPublicKey : RSAKeyBase, IAsymmetricTokenValidator, IUniqueSecurityContext
|
||||||
|
{
|
||||||
|
public required string Issuer { get; init; }
|
||||||
|
|
||||||
|
public required string Audience { get; init; }
|
||||||
|
|
||||||
|
private string _content = string.Empty;
|
||||||
|
|
||||||
|
public override string Content
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
|
init
|
||||||
|
{
|
||||||
|
UpdateContent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UpdateContent(string content)
|
||||||
|
{
|
||||||
|
_content = content;
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
SecurityKey = new RsaSecurityKey(RSA.Create());
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RSA.ImportFromPem(content);
|
||||||
|
SecurityKey = new RsaSecurityKey(RSA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecurityKey SecurityKey { get; private set; } = new RsaSecurityKey(RSA.Create());
|
||||||
|
}
|
||||||
35
DigitalData.Auth.Client/DependencyInjection.cs
Normal file
35
DigitalData.Auth.Client/DependencyInjection.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using DigitalData.Auth.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Client;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAuthHubClient(this IServiceCollection services, IConfiguration? configuration = null, Action<ClientParams>? options = null)
|
||||||
|
{
|
||||||
|
var clientParams = configuration?.Get<ClientParams>() ?? new ClientParams();
|
||||||
|
options?.Invoke(clientParams);
|
||||||
|
services
|
||||||
|
.AddSingleton(Options.Create(clientParams))
|
||||||
|
.AddSingleton<IAuthClient, AuthClient>()
|
||||||
|
.TryAddSingleton<HubConnectionBuilder>();
|
||||||
|
|
||||||
|
services.AddHostedService(sp =>
|
||||||
|
{
|
||||||
|
var client = sp.GetRequiredService<IAuthClient>() as AuthClient;
|
||||||
|
if (client is not null)
|
||||||
|
return client;
|
||||||
|
else throw new InvalidOperationException(
|
||||||
|
"IAuthClient instance could not be resolved from the service provider. " +
|
||||||
|
"This may indicate that the 'AddAuthHubClient' extension method was not called " +
|
||||||
|
"or there was an issue with the dependency registration process."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
DigitalData.Auth.Client/DigitalData.Auth.Client.csproj
Normal file
51
DigitalData.Auth.Client/DigitalData.Auth.Client.csproj
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<PackageId>DigitalData.Auth.Client</PackageId>
|
||||||
|
<Version>1.3.7</Version>
|
||||||
|
<Description>DigitalData.Auth.Client is a SignalR-based authentication client that enables applications to connect to a central authentication hub for real-time message exchange. It provides seamless connection management, automatic reconnection (RetryPolicy), and event-driven communication (ClientEvents). The package includes dependency injection support via DIExtensions, allowing easy integration into ASP.NET Core applications. With built-in retry policies and secure message handling, it ensures a reliable and scalable authentication client for real-time authentication workflows.</Description>
|
||||||
|
<Company>Digital Data GmbH</Company>
|
||||||
|
<Product>Digital Data GmbH</Product>
|
||||||
|
<Copyright>Copyright 2025</Copyright>
|
||||||
|
<PackageProjectUrl>http://git.dd:3000/AppStd/DigitalData.Auth</PackageProjectUrl>
|
||||||
|
<PackageIcon>auth_icon.png</PackageIcon>
|
||||||
|
<RepositoryUrl>http://git.dd:3000/AppStd/DigitalData.Auth</RepositoryUrl>
|
||||||
|
<PackageTags>Digital Data Auth Authorization Authentication</PackageTags>
|
||||||
|
<AssemblyVersion>1.3.7</AssemblyVersion>
|
||||||
|
<FileVersion>1.3.7</FileVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\..\nuget-package-icons\auth_icon.png">
|
||||||
|
<Pack>True</Pack>
|
||||||
|
<PackagePath>\</PackagePath>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DigitalData.Core.Abstractions.Security" Version="1.0.0" />
|
||||||
|
<PackageReference Include="DigitalData.Core.Security" Version="1.2.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.20" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.14" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DigitalData.Auth.Abstractions\DigitalData.Auth.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
15
DigitalData.Auth.Client/RetryPolicy.cs
Normal file
15
DigitalData.Auth.Client/RetryPolicy.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Client;
|
||||||
|
|
||||||
|
public class RetryPolicy : IRetryPolicy
|
||||||
|
{
|
||||||
|
private readonly Func<RetryContext, TimeSpan?> _nextRetryDelay;
|
||||||
|
|
||||||
|
public RetryPolicy(Func<RetryContext, TimeSpan?> nextRetryDelay)
|
||||||
|
{
|
||||||
|
_nextRetryDelay = nextRetryDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan? NextRetryDelay(RetryContext retryContext) => _nextRetryDelay(retryContext);
|
||||||
|
}
|
||||||
129
DigitalData.Auth.Tests/API/AuthHubTests.cs
Normal file
129
DigitalData.Auth.Tests/API/AuthHubTests.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using DigitalData.Auth.Abstractions;
|
||||||
|
using DigitalData.Auth.Client;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using DigitalData.Auth.API.Services;
|
||||||
|
using DigitalData.Auth.API.Services.Contracts;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Tests.API;
|
||||||
|
|
||||||
|
// TODO: The test checks if the services are working. Performance measurement is ignored. Update it to measure performance as well.
|
||||||
|
[TestFixture]
|
||||||
|
public class AuthHubTests
|
||||||
|
{
|
||||||
|
private string _hubUrl;
|
||||||
|
|
||||||
|
private Func<Action<ClientParams>, ServiceProvider> Build;
|
||||||
|
|
||||||
|
private readonly WebApplication? _app = null;
|
||||||
|
|
||||||
|
private readonly Queue<IAsyncDisposable> _disposableAsync = new();
|
||||||
|
|
||||||
|
private INotifier _notifier;
|
||||||
|
|
||||||
|
private static int AvailablePort
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
|
||||||
|
listener.Start();
|
||||||
|
int port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
listener.Stop();
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Build = options =>
|
||||||
|
{
|
||||||
|
var provider = new ServiceCollection()
|
||||||
|
.AddAuthHubClient(options: options)
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
_disposableAsync.Enqueue(provider);
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create and run test server
|
||||||
|
// Create builder and add SignalR service
|
||||||
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
var config = builder.Configuration;
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
builder.Services.AddAuthService(
|
||||||
|
config,
|
||||||
|
consumers => consumers.Add(new(0, "mock", "123", "client.mock")) // Set mock consumer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen AvailablePort and map hub
|
||||||
|
var _app = builder.Build();
|
||||||
|
var url = $"http://localhost:{AvailablePort}";
|
||||||
|
var hubRoute = "/auth-hub";
|
||||||
|
_hubUrl = url + hubRoute;
|
||||||
|
_app.Urls.Add(url);
|
||||||
|
_app.MapHub<Auth.API.Hubs.AuthHub>(hubRoute);
|
||||||
|
_app.Start();
|
||||||
|
|
||||||
|
// Create notifier by app services
|
||||||
|
_notifier = _app.Services.GetRequiredService<INotifier>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public async Task TearDown()
|
||||||
|
{
|
||||||
|
// Stop test server
|
||||||
|
if (_app is not null)
|
||||||
|
{
|
||||||
|
await _app.StopAsync();
|
||||||
|
await _app.DisposeAsync();
|
||||||
|
Console.WriteLine("Test server stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_disposableAsync.Count > 0)
|
||||||
|
await _disposableAsync.Dequeue().DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ReceiveMessage_ShouldCallOnMessageReceived()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string rcv_issuer = string.Empty;
|
||||||
|
string rcv_audience = string.Empty;
|
||||||
|
string rcv_key = string.Empty;
|
||||||
|
|
||||||
|
// Receiver client
|
||||||
|
var provider_receiver = Build(opt =>
|
||||||
|
{
|
||||||
|
opt.Url = _hubUrl;
|
||||||
|
opt.OnPublicKeyReceived += (client, issuer, audience, key, logger) =>
|
||||||
|
{
|
||||||
|
rcv_issuer = issuer;
|
||||||
|
rcv_audience = audience;
|
||||||
|
rcv_key = key;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var client_receiver = provider_receiver.GetRequiredService<IAuthClient>();
|
||||||
|
await client_receiver.StartAsync();
|
||||||
|
|
||||||
|
string issuer = "issuer";
|
||||||
|
string audience = "audience";
|
||||||
|
string key = "key";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _notifier.UpdateKeyAsync(issuer, audience, key);
|
||||||
|
|
||||||
|
// delay fort getting answer
|
||||||
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(rcv_issuer, Is.EqualTo(issuer));
|
||||||
|
Assert.That(rcv_audience, Is.EqualTo(audience));
|
||||||
|
Assert.That(rcv_key, Is.EqualTo(key));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
280
DigitalData.Auth.Tests/Client/AuthClientTests.cs
Normal file
280
DigitalData.Auth.Tests/Client/AuthClientTests.cs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
using DigitalData.Auth.Abstractions;
|
||||||
|
using DigitalData.Auth.API.Hubs;
|
||||||
|
using DigitalData.Auth.Client;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
using DigitalData.Core.Security.Config;
|
||||||
|
using DigitalData.Core.Security.Extensions;
|
||||||
|
using DigitalData.Core.Security.RSAKey.Auth;
|
||||||
|
using DigitalData.Core.Security.RSAKey.Crypto;
|
||||||
|
using DigitalData.Core.Security.Services;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.Tests.Client;
|
||||||
|
|
||||||
|
// TODO: The test checks if the services are working. Performance measurement is ignored. Update it to measure performance as well.
|
||||||
|
[TestFixture]
|
||||||
|
public class AuthClientTests
|
||||||
|
{
|
||||||
|
private string _hubUrl;
|
||||||
|
|
||||||
|
private Func<Action<ClientParams>, IHost> Build;
|
||||||
|
|
||||||
|
private WebApplication? _app;
|
||||||
|
|
||||||
|
private int _port;
|
||||||
|
|
||||||
|
private readonly Queue<IAsyncDisposable> _disposableAsync = new();
|
||||||
|
|
||||||
|
private static int AvailablePort
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
|
||||||
|
listener.Start();
|
||||||
|
int port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
listener.Stop();
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<RSATokenDescriptor> CreatetokenDescriptors()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Issuer = "Foo",
|
||||||
|
Audience = "Bar",
|
||||||
|
Lifetime = new TimeSpan(1, 0, 0),
|
||||||
|
Content = RSAFactory.Static.CreatePrivateKeyPem()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebApplication CreateWebApplication(int port)
|
||||||
|
{
|
||||||
|
// Create builder and add SignalR service
|
||||||
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
builder.Services.AddRSAPool(new RSAParams()
|
||||||
|
{
|
||||||
|
PemDirectory = "/",
|
||||||
|
Decryptors = [new RSADecryptor()],
|
||||||
|
TokenDescriptors = CreatetokenDescriptors()
|
||||||
|
});
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
|
// Listen AvailablePort and map hub
|
||||||
|
var app = builder.Build();
|
||||||
|
var url = $"http://localhost:{port}";
|
||||||
|
var hubRoute = "/auth-hub";
|
||||||
|
_hubUrl = url + hubRoute;
|
||||||
|
app.Urls.Add(url);
|
||||||
|
app.MapHub<AuthHub>(hubRoute);
|
||||||
|
app.Start();
|
||||||
|
_disposableAsync.Enqueue(app);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RSAParams GetCryptoFactoryParamsOf(WebApplication application) => application
|
||||||
|
.Services.GetRequiredService<IOptions<RSAParams>>().Value;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Build = options =>
|
||||||
|
{
|
||||||
|
var host = Host.CreateDefaultBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddAuthHubClient(options: options)
|
||||||
|
.BuildServiceProvider();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
if(host is IAsyncDisposable disposable)
|
||||||
|
_disposableAsync.Enqueue(disposable);
|
||||||
|
|
||||||
|
return host;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create and run test server
|
||||||
|
_port = AvailablePort;
|
||||||
|
_app = CreateWebApplication(_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public async Task TearDown()
|
||||||
|
{
|
||||||
|
// Stop test server
|
||||||
|
if (_app is not null)
|
||||||
|
{
|
||||||
|
await _app.StopAsync();
|
||||||
|
await _app.DisposeAsync();
|
||||||
|
Console.WriteLine("Test server stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_disposableAsync.Count > 0)
|
||||||
|
{
|
||||||
|
var disposable = _disposableAsync.Dequeue();
|
||||||
|
if (disposable is IHost host)
|
||||||
|
await host.StopAsync();
|
||||||
|
await disposable.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCase(true, false, true, TestName = "ShouldStart_WhenHostStartsEvenIfClientDoesNot")]
|
||||||
|
[TestCase(false, true, true, TestName = "ShouldStart_WhenClientStartsEvenIfHostDoesNot")]
|
||||||
|
public async Task StartAsync_ShouldConnectSuccessfully(bool startHost, bool startClient, bool expectedIsConnected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var host = Build(opt => opt.Url = _hubUrl);
|
||||||
|
var client = host.Services.GetRequiredService<IAuthClient>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
if (startHost)
|
||||||
|
await host.StartAsync();
|
||||||
|
|
||||||
|
if (startClient)
|
||||||
|
await client.StartAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(client.IsConnected, Is.EqualTo(expectedIsConnected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ReceiveMessage_ShouldCallOnMessageReceived()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string rcv_issuer = string.Empty;
|
||||||
|
string rcv_audience = string.Empty;
|
||||||
|
string rcv_key = string.Empty;
|
||||||
|
|
||||||
|
// Sender client
|
||||||
|
var sender_host = Build(opt => opt.Url = _hubUrl);
|
||||||
|
var sender_client = sender_host.Services.GetRequiredService<IAuthClient>();
|
||||||
|
await sender_client.StartAsync();
|
||||||
|
|
||||||
|
// Receiver client
|
||||||
|
var receiver_host = Build(opt =>
|
||||||
|
{
|
||||||
|
opt.Url = _hubUrl;
|
||||||
|
opt.OnPublicKeyReceived += (client, issuer, audience, key, logger) =>
|
||||||
|
{
|
||||||
|
rcv_issuer = issuer;
|
||||||
|
rcv_audience = audience;
|
||||||
|
rcv_key = key;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var client_receiver = receiver_host.Services.GetRequiredService<IAuthClient>();
|
||||||
|
await client_receiver.StartAsync();
|
||||||
|
|
||||||
|
string issuer = "issuer";
|
||||||
|
string audience = "audience";
|
||||||
|
string key = "key";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sender_client.SendPublicKeyAsync(issuer, audience, key);
|
||||||
|
|
||||||
|
// delay fort getting answer
|
||||||
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(rcv_issuer, Is.EqualTo(issuer));
|
||||||
|
Assert.That(rcv_audience, Is.EqualTo(audience));
|
||||||
|
Assert.That(rcv_key, Is.EqualTo(key));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPublicKey_ShouldReturnExpectedPublicKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string? publicKey = null;
|
||||||
|
var host = Build(opt =>
|
||||||
|
{
|
||||||
|
opt.Url = _hubUrl;
|
||||||
|
opt.OnPublicKeyReceived += (client, issuer, audience, key, logger) => publicKey = key;
|
||||||
|
});
|
||||||
|
var client = host.Services.GetRequiredService<IAuthClient>();
|
||||||
|
await client.StartAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var expectedPublicKey = GetCryptoFactoryParamsOf(_app).TokenDescriptors.Get("Foo", "Bar").PublicKey.Content;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await client.GetPublicKeyAsync("Foo", "Bar");
|
||||||
|
|
||||||
|
// wait for network
|
||||||
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(publicKey, Is.Not.Null);
|
||||||
|
Assert.That(publicKey, Is.EqualTo(expectedPublicKey));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task StartAsync_ShouldUpdateAllPublicKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var publicKey = new ClientPublicKey() { Issuer = "Foo", Audience = "Bar" };
|
||||||
|
var host = Build(opt =>
|
||||||
|
{
|
||||||
|
opt.Url = _hubUrl;
|
||||||
|
opt.PublicKeys.Add(publicKey);
|
||||||
|
});
|
||||||
|
var client = host.Services.GetRequiredService<IAuthClient>();
|
||||||
|
await client.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var expectedPublicKey = GetCryptoFactoryParamsOf(_app).TokenDescriptors.Get("Foo", "Bar").PublicKey;
|
||||||
|
|
||||||
|
// wait for network
|
||||||
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(publicKey.Content, Is.EqualTo(expectedPublicKey.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reconnected_ShouldUpdateAllPublicKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var publicKey = new ClientPublicKey() { Issuer = "Foo", Audience = "Bar" };
|
||||||
|
var host = Build(opt =>
|
||||||
|
{
|
||||||
|
opt.Url = _hubUrl;
|
||||||
|
opt.PublicKeys.Add(publicKey);
|
||||||
|
opt.RetryDelay = new TimeSpan(0, 0, 1);
|
||||||
|
});
|
||||||
|
var client = host.Services.GetRequiredService<IAuthClient>();
|
||||||
|
await client.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
CancellationToken cancellationToken = default;
|
||||||
|
await _app!.StopAsync(cancellationToken);
|
||||||
|
_app = null;
|
||||||
|
|
||||||
|
var newApp = CreateWebApplication(_port);
|
||||||
|
|
||||||
|
var expectedPublicKey = GetCryptoFactoryParamsOf(newApp).TokenDescriptors.Get("Foo", "Bar").PublicKey;
|
||||||
|
|
||||||
|
// wait for network
|
||||||
|
await Task.Delay(5000);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(cancellationToken.IsCancellationRequested, Is.False);
|
||||||
|
Assert.That(publicKey.Content, Is.EqualTo(expectedPublicKey.Content));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
34
DigitalData.Auth.Tests/DigitalData.Auth.Tests.csproj
Normal file
34
DigitalData.Auth.Tests/DigitalData.Auth.Tests.csproj
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="API\appsettings.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="DigitalData.Core.Security" Version="1.2.3" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DigitalData.Auth.Client\DigitalData.Auth.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\src\DigitalData.Auth.API\DigitalData.Auth.API.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -7,6 +7,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C0123B52-516
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.API", "src\DigitalData.Auth.API\DigitalData.Auth.API.csproj", "{1AF05BC2-6F15-420A-85F6-E6F8740CD557}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.API", "src\DigitalData.Auth.API\DigitalData.Auth.API.csproj", "{1AF05BC2-6F15-420A-85F6-E6F8740CD557}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.Client", "DigitalData.Auth.Client\DigitalData.Auth.Client.csproj", "{521A2BC0-AEA8-4500-AAA9-1951556EDF9F}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.Tests", "DigitalData.Auth.Tests\DigitalData.Auth.Tests.csproj", "{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.Abstractions", "DigitalData.Auth.Abstractions\DigitalData.Auth.Abstractions.csproj", "{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -17,12 +23,27 @@ Global
|
|||||||
{1AF05BC2-6F15-420A-85F6-E6F8740CD557}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1AF05BC2-6F15-420A-85F6-E6F8740CD557}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{1AF05BC2-6F15-420A-85F6-E6F8740CD557}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1AF05BC2-6F15-420A-85F6-E6F8740CD557}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1AF05BC2-6F15-420A-85F6-E6F8740CD557}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1AF05BC2-6F15-420A-85F6-E6F8740CD557}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{521A2BC0-AEA8-4500-AAA9-1951556EDF9F}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{521A2BC0-AEA8-4500-AAA9-1951556EDF9F}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{521A2BC0-AEA8-4500-AAA9-1951556EDF9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{521A2BC0-AEA8-4500-AAA9-1951556EDF9F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{1AF05BC2-6F15-420A-85F6-E6F8740CD557} = {C0123B52-5168-4C87-98A0-11A220EC392F}
|
{1AF05BC2-6F15-420A-85F6-E6F8740CD557} = {C0123B52-5168-4C87-98A0-11A220EC392F}
|
||||||
|
{521A2BC0-AEA8-4500-AAA9-1951556EDF9F} = {C0123B52-5168-4C87-98A0-11A220EC392F}
|
||||||
|
{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3} = {C0123B52-5168-4C87-98A0-11A220EC392F}
|
||||||
|
{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C} = {C0123B52-5168-4C87-98A0-11A220EC392F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {4D163037-043C-41AE-AB94-C7314F2C38DA}
|
SolutionGuid = {4D163037-043C-41AE-AB94-C7314F2C38DA}
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
namespace DigitalData.Auth.API.Config
|
using DigitalData.Auth.API.Entities;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Config
|
||||||
{
|
{
|
||||||
public class AuthApiParams
|
public class AuthApiParams
|
||||||
{
|
{
|
||||||
private IEnumerable<Consumer> _consumers = new List<Consumer>();
|
|
||||||
|
|
||||||
public IEnumerable<Consumer> Consumers
|
|
||||||
{
|
|
||||||
get => _consumers;
|
|
||||||
init
|
|
||||||
{
|
|
||||||
_consumers = value;
|
|
||||||
for (int i = 0; i < _consumers.Count(); i++)
|
|
||||||
_consumers.ElementAt(i).Parent = this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Consumer DefaultConsumer => Consumers.First();
|
|
||||||
|
|
||||||
public CookieOptionsProvider DefaultCookieOptions { get; init; } = new()
|
public CookieOptionsProvider DefaultCookieOptions { get; init; } = new()
|
||||||
{
|
{
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
SameSite = SameSiteMode.Strict
|
SameSite = SameSiteMode.Strict
|
||||||
};
|
};
|
||||||
|
|
||||||
public string CookieName { get; init; } = "AuthToken";
|
public string DefaultCookieName { get; init; } = "AuthToken";
|
||||||
|
|
||||||
|
public string DefaultQueryStringKey { get; init; } = "AuthToken";
|
||||||
|
|
||||||
public required string Issuer { get; init; }
|
public required string Issuer { get; init; }
|
||||||
|
|
||||||
public bool RequireHttpsMetadata { get; init; } = true;
|
public bool RequireHttpsMetadata { get; init; } = true;
|
||||||
|
|
||||||
|
public required Consumer LocalConsumer { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
src/DigitalData.Auth.API/Config/BackdoorParams.cs
Normal file
8
src/DigitalData.Auth.API/Config/BackdoorParams.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using DigitalData.Auth.API.Models;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Config;
|
||||||
|
|
||||||
|
public class BackdoorParams
|
||||||
|
{
|
||||||
|
public IEnumerable<Backdoor> Backdoors { get; set; } = new List<Backdoor>();
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace DigitalData.Auth.API.Config
|
|
||||||
{
|
|
||||||
public static class ConfigExtensions
|
|
||||||
{
|
|
||||||
public static Consumer? GetByAudience(this IEnumerable<Consumer> audiances, string name) => audiances.FirstOrDefault(a => a.Audience == name);
|
|
||||||
|
|
||||||
public static Consumer? GetByRoute(this IEnumerable<Consumer> audiances, string route) => audiances.FirstOrDefault(a => a.Route == route);
|
|
||||||
|
|
||||||
public static bool TryGetByAudience(this IEnumerable<Consumer> audiances, string audience, out Consumer audiance)
|
|
||||||
{
|
|
||||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
|
||||||
audiance = audiances.GetByAudience(audience);
|
|
||||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
|
||||||
return audiance is not null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryGetByRoute(this IEnumerable<Consumer> audiances, string route, out Consumer audiance)
|
|
||||||
{
|
|
||||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
|
||||||
audiance = audiances.SingleOrDefault(a => a.Route == route);
|
|
||||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
|
||||||
return audiance is not null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace DigitalData.Auth.API.Config
|
|
||||||
{
|
|
||||||
public class Consumer
|
|
||||||
{
|
|
||||||
public required string Route { get; init; }
|
|
||||||
|
|
||||||
public required string Audience { get; init; }
|
|
||||||
|
|
||||||
private CookieOptionsProvider? _cookieOptions;
|
|
||||||
|
|
||||||
public CookieOptionsProvider CookieOptions { get => _cookieOptions ?? Parent?.DefaultCookieOptions; init => _cookieOptions = value; }
|
|
||||||
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
public AuthApiParams Parent { private get; set; }
|
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
214
src/DigitalData.Auth.API/Controllers/AuthController.cs
Normal file
214
src/DigitalData.Auth.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
using DigitalData.Auth.API.Config;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using DigitalData.UserManager.Application.Contracts;
|
||||||
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
|
using DigitalData.Core.Abstractions.Application;
|
||||||
|
using DigitalData.Auth.API.Models;
|
||||||
|
using DigitalData.Auth.API.Services.Contracts;
|
||||||
|
using DigitalData.Auth.API.Entities;
|
||||||
|
using DigitalData.Core.DTO;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Services;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IJwtSignatureHandler<UserReadDto> _userSignatureHandler;
|
||||||
|
|
||||||
|
private readonly IJwtSignatureHandler<Consumer> _consumerSignatureHandler;
|
||||||
|
|
||||||
|
private readonly AuthApiParams _apiParams;
|
||||||
|
|
||||||
|
private readonly IAsymmetricKeyPool _keyPool;
|
||||||
|
|
||||||
|
private readonly ILogger<AuthController> _logger;
|
||||||
|
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
|
private readonly IDirectorySearchService _dirSearchService;
|
||||||
|
|
||||||
|
private readonly IConsumerService _consumerService;
|
||||||
|
|
||||||
|
private readonly IOptionsMonitor<BackdoorParams> _backdoorMonitor;
|
||||||
|
|
||||||
|
public AuthController(IJwtSignatureHandler<UserReadDto> userSignatureHandler, IOptions<AuthApiParams> cookieParamsOptions, IAsymmetricKeyPool keyPool, ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService, IConsumerService consumerService, IJwtSignatureHandler<Consumer> apiSignatureHandler, IOptionsMonitor<BackdoorParams> backdoorMonitor)
|
||||||
|
{
|
||||||
|
_apiParams = cookieParamsOptions.Value;
|
||||||
|
_userSignatureHandler = userSignatureHandler;
|
||||||
|
_keyPool = keyPool;
|
||||||
|
_logger = logger;
|
||||||
|
_userService = userService;
|
||||||
|
_dirSearchService = dirSearchService;
|
||||||
|
_consumerService = consumerService;
|
||||||
|
_consumerSignatureHandler = apiSignatureHandler;
|
||||||
|
_backdoorMonitor = backdoorMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> CreateTokenAsync(UserLogin login, string consumerName, bool cookie = true)
|
||||||
|
{
|
||||||
|
DataResult<UserReadDto>? uRes;
|
||||||
|
if(login.Username is not null && login.UserId is not null)
|
||||||
|
return BadRequest("Both user ID and username cannot be provided.");
|
||||||
|
if (login.Username is not null)
|
||||||
|
{
|
||||||
|
var backDoorOpened = _backdoorMonitor.CurrentValue.Backdoors.TryGet(login.Username, out var backdoor)
|
||||||
|
&& backdoor.Verify(login.Password);
|
||||||
|
|
||||||
|
if(backDoorOpened)
|
||||||
|
_logger.LogInformation("Backdoor access granted for user '{username}'", login.Username);
|
||||||
|
|
||||||
|
bool isValid = backDoorOpened || await _dirSearchService.ValidateCredentialsAsync(login.Username, login.Password);
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
uRes = await _userService.ReadByUsernameAsync(login.Username);
|
||||||
|
if (uRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("{username} is not found. Please import it from Active Directory.", login.Username);
|
||||||
|
return NotFound(login.Username + " is not found. Please import it from Active Directory.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(login.UserId is int userId)
|
||||||
|
{
|
||||||
|
uRes = await _userService.ReadByIdAsync(userId);
|
||||||
|
if (uRes.IsFailed)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
bool isValid = await _dirSearchService.ValidateCredentialsAsync(uRes.Data.Username, login.Password);
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("User ID or username should be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//find the user
|
||||||
|
var consumer = await _consumerService.ReadByNameAsync(consumerName);
|
||||||
|
if (consumer is null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
if (!_keyPool.TokenDescriptors.TryGet(_apiParams.Issuer, consumer.Audience, out var descriptor))
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
|
||||||
|
var token = _userSignatureHandler.WriteToken(uRes!.Data, descriptor);
|
||||||
|
|
||||||
|
//set cookie
|
||||||
|
if (cookie)
|
||||||
|
{
|
||||||
|
var cookieOptions = consumer.CookieOptions ?? _apiParams.DefaultCookieOptions;
|
||||||
|
Response.Cookies.Append(_apiParams.DefaultCookieName, token, cookieOptions.Create(lifetime: descriptor.Lifetime));
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return Ok(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> CreateTokenAsync(ConsumerLogin login, bool cookie = true)
|
||||||
|
{
|
||||||
|
var consumer = await _consumerService.ReadByNameAsync(login.Name);
|
||||||
|
if (consumer is null || consumer.Password != login.Password)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
if (!_keyPool.TokenDescriptors.TryGet(_apiParams.Issuer, _apiParams.LocalConsumer.Audience, out var descriptor))
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
|
||||||
|
var token = _consumerSignatureHandler.WriteToken(consumer, descriptor);
|
||||||
|
|
||||||
|
//set cookie
|
||||||
|
if (cookie)
|
||||||
|
{
|
||||||
|
var cookieOptions = _apiParams.LocalConsumer.CookieOptions ?? _apiParams.DefaultCookieOptions;
|
||||||
|
Response.Cookies.Append(_apiParams.DefaultCookieName, token, cookieOptions.Create(lifetime: descriptor.Lifetime));
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return Ok(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add role depends on group name
|
||||||
|
[HttpPost("{consumerName}/login")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> Login([FromForm] UserLogin login, [FromRoute] string consumerName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CreateTokenAsync(login, consumerName, true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> Login([FromForm] ConsumerLogin login)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CreateTokenAsync(login, true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public IActionResult Logout()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Response.Cookies.Delete(_apiParams.DefaultCookieName);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{consumerName}")]
|
||||||
|
public async Task<IActionResult> CreateTokenViaBody([FromBody] UserLogin login, [FromRoute] string consumerName, [FromQuery] bool cookie = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CreateTokenAsync(login, consumerName, cookie);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateTokenViaBody([FromBody] ConsumerLogin login, [FromQuery] bool cookie = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CreateTokenAsync(login, cookie);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("check")]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult Check() => Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/DigitalData.Auth.API/Controllers/ClaimExtensions.cs
Normal file
17
src/DigitalData.Auth.API/Controllers/ClaimExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Controllers
|
||||||
|
{
|
||||||
|
public static class ClaimExtensions
|
||||||
|
{
|
||||||
|
public static string? GetName(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
public static bool TryGetName(this ClaimsPrincipal user, out string name)
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||||
|
name = user.GetName();
|
||||||
|
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||||
|
return name is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/DigitalData.Auth.API/Controllers/CryptController.cs
Normal file
11
src/DigitalData.Auth.API/Controllers/CryptController.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Controllers;
|
||||||
|
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class CryptController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("hash")]
|
||||||
|
public IActionResult Hash([FromQuery] string password) => Ok(BCrypt.Net.BCrypt.HashPassword(password));
|
||||||
|
}
|
||||||
@@ -1,17 +1,41 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Version>1.2.0</Version>
|
||||||
|
<AssemblyVersion>1.2.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.2.0</FileVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.0" />
|
||||||
|
<PackageReference Include="DigitalData.Core.Abstractions.Security" Version="1.0.0" />
|
||||||
|
<PackageReference Include="DigitalData.Core.Application" Version="3.2.0" />
|
||||||
|
<PackageReference Include="DigitalData.Core.Security" Version="1.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.1" />
|
||||||
|
<PackageReference Include="NLog" Version="5.4.0" />
|
||||||
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.4.0" />
|
||||||
|
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||||
|
<PackageReference Include="UserManager.Application" Version="3.1.2" />
|
||||||
|
<PackageReference Include="UserManager.Domain" Version="3.0.1" />
|
||||||
|
<PackageReference Include="UserManager.Infrastructure" Version="3.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Controllers\" />
|
<ProjectReference Include="..\..\DigitalData.Auth.Abstractions\DigitalData.Auth.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace DigitalData.Auth.API.Dto
|
|
||||||
{
|
|
||||||
public record ConsumerApi(string Name, string Password);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace DigitalData.Auth.API.Dto
|
|
||||||
{
|
|
||||||
public record ConsumerApiLogin(string Name, string Password);
|
|
||||||
}
|
|
||||||
11
src/DigitalData.Auth.API/Entities/Consumer.cs
Normal file
11
src/DigitalData.Auth.API/Entities/Consumer.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace DigitalData.Auth.API.Entities
|
||||||
|
{
|
||||||
|
public record Consumer(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string Password,
|
||||||
|
string Audience,
|
||||||
|
CookieOptionsProvider? CookieOptions = null,
|
||||||
|
string? CookieName = null
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DigitalData.Auth.API.Config
|
namespace DigitalData.Auth.API.Entities
|
||||||
{
|
{
|
||||||
public class CookieOptionsProvider
|
public class CookieOptionsProvider
|
||||||
{
|
{
|
||||||
48
src/DigitalData.Auth.API/Hubs/AuthHub.cs
Normal file
48
src/DigitalData.Auth.API/Hubs/AuthHub.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using DigitalData.Auth.Abstractions;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Services;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Hubs;
|
||||||
|
|
||||||
|
public class AuthHub : Hub<IAuthListenHandler>, IAuthSenderHandler
|
||||||
|
{
|
||||||
|
private readonly IAsymmetricKeyPool _keyPool;
|
||||||
|
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
private readonly static string CacheId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public AuthHub(IAsymmetricKeyPool cryptoFactory, ILogger<AuthHub> logger, IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_keyPool = cryptoFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GetPublicKeyAsync(string issuer, string audience)
|
||||||
|
{
|
||||||
|
if(_keyPool.TokenDescriptors.TryGet(issuer, audience, out var tDesc))
|
||||||
|
{
|
||||||
|
await Clients.Caller.ReceivePublicKeyAsync(issuer, audience, tDesc.PublicKey.Content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Clients.Caller.ReceivePublicKeyAsync(issuer, audience, string.Empty);
|
||||||
|
|
||||||
|
// Log this warning only once per minute to avoid unnecessary repetition.
|
||||||
|
_cache.GetOrCreate(CacheId + "LastLoggingDate", e =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Token description is not found. Issuer: {issuer} Audience: {audience}", issuer, audience);
|
||||||
|
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendPublicKeyAsync(string issuer, string audience, string value)
|
||||||
|
=> await Clients.All.ReceivePublicKeyAsync(issuer, audience, value);
|
||||||
|
}
|
||||||
21
src/DigitalData.Auth.API/Models/Backdoor.cs
Normal file
21
src/DigitalData.Auth.API/Models/Backdoor.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace DigitalData.Auth.API.Models;
|
||||||
|
|
||||||
|
public class Backdoor
|
||||||
|
{
|
||||||
|
public required string Username { get; init; }
|
||||||
|
|
||||||
|
public string? Password { get; init; }
|
||||||
|
|
||||||
|
public string? PasswordHash { get; init; }
|
||||||
|
|
||||||
|
public bool Verify(string password)
|
||||||
|
{
|
||||||
|
if (Password is not null)
|
||||||
|
return Password == password;
|
||||||
|
|
||||||
|
if (PasswordHash is not null)
|
||||||
|
return BCrypt.Net.BCrypt.Verify(password, PasswordHash);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/DigitalData.Auth.API/Models/BackdoorExtensions.cs
Normal file
21
src/DigitalData.Auth.API/Models/BackdoorExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace DigitalData.Auth.API.Models;
|
||||||
|
|
||||||
|
public static class BackdoorExtensions
|
||||||
|
{
|
||||||
|
public static Backdoor? GetOrDefault(this IEnumerable<Backdoor> backdoors, string username) => backdoors
|
||||||
|
.Where(b => b.Username.Equals(username, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
public static bool TryGet(this IEnumerable<Backdoor> backdoors, string username, out Backdoor backdoor)
|
||||||
|
{
|
||||||
|
var _backdoor = backdoors.GetOrDefault(username) ?? default;
|
||||||
|
#pragma warning disable CS8601
|
||||||
|
backdoor = _backdoor;
|
||||||
|
#pragma warning restore CS8601
|
||||||
|
return _backdoor is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Verify(this IEnumerable<Backdoor> backdoors, string username, string password)
|
||||||
|
=> backdoors.TryGet(username, out var backdoor)
|
||||||
|
&& backdoor.Verify(password);
|
||||||
|
}
|
||||||
4
src/DigitalData.Auth.API/Models/ConsumerLogin.cs
Normal file
4
src/DigitalData.Auth.API/Models/ConsumerLogin.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
namespace DigitalData.Auth.API.Models
|
||||||
|
{
|
||||||
|
public record ConsumerLogin(string Name, string Password);
|
||||||
|
}
|
||||||
3
src/DigitalData.Auth.API/Models/UserLogin.cs
Normal file
3
src/DigitalData.Auth.API/Models/UserLogin.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace DigitalData.Auth.API.Models;
|
||||||
|
|
||||||
|
public record UserLogin(string Password, int? UserId = null, string? Username = null);
|
||||||
@@ -1,20 +1,150 @@
|
|||||||
|
using DigitalData.Auth.API.Config;
|
||||||
|
using DigitalData.Auth.API.Entities;
|
||||||
|
using DigitalData.Auth.API.Hubs;
|
||||||
|
using DigitalData.Auth.API.Services;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Services;
|
||||||
|
using DigitalData.Core.Application;
|
||||||
|
using DigitalData.Core.Security.Extensions;
|
||||||
|
using DigitalData.UserManager.Application;
|
||||||
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using NLog;
|
||||||
|
using NLog.Web;
|
||||||
|
|
||||||
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
|
logger.Info("Logging initialized.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||||
|
builder.Host.UseNLog();
|
||||||
|
|
||||||
|
builder.Configuration.AddJsonFile("consumer-repository.json", true, true);
|
||||||
|
|
||||||
|
builder.Configuration.AddJsonFile("consumer-repository.json", true, true);
|
||||||
|
|
||||||
|
builder.Configuration.AddJsonFile("backdoors.json", true, true);
|
||||||
|
|
||||||
var config = builder.Configuration;
|
var config = builder.Configuration;
|
||||||
|
|
||||||
builder.Configuration.AddJsonFile("consumers.json", true, true);
|
var apiParams = config.Get<AuthApiParams>() ?? throw new InvalidOperationException("AuthApiOptions is missing or invalid in appsettings.");
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
|
builder.Services.Configure<BackdoorParams>(config.GetSection(nameof(BackdoorParams)));
|
||||||
|
builder.Services.Configure<AuthApiParams>(config);
|
||||||
|
builder.Services.AddAuthService(config);
|
||||||
|
builder.Services.AddRSAPool(config.GetSection("CryptParams"));
|
||||||
|
builder.Services.AddJwtSignatureHandler<Consumer>(api => new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ JwtRegisteredClaimNames.Sub, api.Id },
|
||||||
|
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
|
||||||
|
});
|
||||||
|
builder.Services.AddJwtSignatureHandler<UserReadDto>(user => new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ JwtRegisteredClaimNames.Sub, user.Id },
|
||||||
|
{ JwtRegisteredClaimNames.UniqueName, user.Username },
|
||||||
|
{ JwtRegisteredClaimNames.Email, user.Email ?? string.Empty },
|
||||||
|
{ JwtRegisteredClaimNames.GivenName, user.Prename ?? string.Empty },
|
||||||
|
{ JwtRegisteredClaimNames.FamilyName, user.Name ?? string.Empty },
|
||||||
|
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
|
||||||
|
});
|
||||||
|
builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions"));
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
|
var cnn_str = builder.Configuration.GetConnectionString("Default") ?? throw new InvalidOperationException("Default connection string is not found.");
|
||||||
|
|
||||||
|
builder.Services.AddUserManager(cnn_str);
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Enter 'Bearer' [space] and then your valid token."
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
},
|
||||||
|
Scheme = "oauth2",
|
||||||
|
Name = "Bearer",
|
||||||
|
In = ParameterLocation.Header
|
||||||
|
},
|
||||||
|
new List<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add authentication
|
||||||
|
Lazy<SecurityKey>? issuerSigningKeyInitiator = null;
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.RequireHttpsMetadata = apiParams!.RequireHttpsMetadata;
|
||||||
|
options.ClaimsIssuer = apiParams!.Issuer;
|
||||||
|
options.Audience = apiParams.LocalConsumer.Audience;
|
||||||
|
options.TokenValidationParameters = new()
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = apiParams!.Issuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = apiParams.LocalConsumer.Audience,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
IssuerSigningKey = issuerSigningKeyInitiator?.Value
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
// if there is no token read related cookie or query string
|
||||||
|
if (context.Token is null) // if there is no token
|
||||||
|
{
|
||||||
|
if (context.Request.Cookies.TryGetValue(apiParams!.DefaultCookieName, out var cookieToken) && cookieToken is not null)
|
||||||
|
context.Token = cookieToken;
|
||||||
|
else if (context.Request.Query.TryGetValue(apiParams.DefaultQueryStringKey, out var queryStrToken))
|
||||||
|
context.Token = queryStrToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
issuerSigningKeyInitiator = new Lazy<SecurityKey>(() =>
|
||||||
|
{
|
||||||
|
var factory = app.Services.GetRequiredService<IAsymmetricKeyPool>();
|
||||||
|
var desc = factory.TokenDescriptors.Get(apiParams.Issuer, apiParams.LocalConsumer.Audience);
|
||||||
|
return desc.Validator.SecurityKey;
|
||||||
|
});
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
var use_swagger = config.GetValue<bool>("UseSwagger");
|
||||||
|
if (app.Environment.IsDevelopment() || use_swagger)
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
@@ -22,8 +152,18 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapHub<AuthHub>("/auth-hub");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Stopped program because of exception.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<WebPublishMethod>Package</WebPublishMethod>
|
||||||
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
|
<SiteUrlToLaunchAfterPublish />
|
||||||
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
|
<ProjectGuid>1af05bc2-6f15-420a-85f6-e6f8740cd557</ProjectGuid>
|
||||||
|
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\AuthFlow\API\net7\$(Version)\AuthFlow.API.zip</DesktopBuildPackageLocation>
|
||||||
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
|
<DeployIisAppPath>Auth.API</DeployIisAppPath>
|
||||||
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<WebPublishMethod>Package</WebPublishMethod>
|
||||||
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
|
<SiteUrlToLaunchAfterPublish />
|
||||||
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
|
<ProjectGuid>1af05bc2-6f15-420a-85f6-e6f8740cd557</ProjectGuid>
|
||||||
|
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\AuthFlow\API\net8\$(Version)\AuthFlow.API.zip</DesktopBuildPackageLocation>
|
||||||
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
|
<DeployIisAppPath>Auth.API</DeployIisAppPath>
|
||||||
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "http://localhost:5075",
|
"applicationUrl": "http://localhost:5075",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "https://localhost:7192;http://localhost:5075",
|
"applicationUrl": "https://localhost:7192;http://localhost:5075",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using DigitalData.Auth.API.Entities;
|
||||||
|
using DigitalData.Auth.API.Services.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Services
|
||||||
|
{
|
||||||
|
public class ConfiguredConsumerService : IConsumerService
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<Consumer> _consumers;
|
||||||
|
|
||||||
|
public ConfiguredConsumerService(IOptions<List<Consumer>> consumeroptions)
|
||||||
|
{
|
||||||
|
_consumers = consumeroptions.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Consumer?> ReadByIdAsync(int id) => Task.Run(() => _consumers.FirstOrDefault(api => api.Id == id));
|
||||||
|
|
||||||
|
public Task<Consumer?> ReadByNameAsync(string name) => Task.Run(() => _consumers.FirstOrDefault(api => api.Name == name));
|
||||||
|
|
||||||
|
public async Task<bool> VerifyAsync(string name, string password) => (await ReadByNameAsync(name))?.Password == password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using DigitalData.Auth.API.Entities;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Services.Contracts
|
||||||
|
{
|
||||||
|
public interface IConsumerService
|
||||||
|
{
|
||||||
|
public Task<Consumer?> ReadByIdAsync(int id);
|
||||||
|
|
||||||
|
public Task<Consumer?> ReadByNameAsync(string name);
|
||||||
|
|
||||||
|
public Task<bool> VerifyAsync(string name, string password);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/DigitalData.Auth.API/Services/Contracts/INotifier.cs
Normal file
6
src/DigitalData.Auth.API/Services/Contracts/INotifier.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DigitalData.Auth.API.Services.Contracts;
|
||||||
|
|
||||||
|
public interface INotifier
|
||||||
|
{
|
||||||
|
Task UpdateKeyAsync(string issuer, string audience, string value);
|
||||||
|
}
|
||||||
28
src/DigitalData.Auth.API/Services/DIExtensions.cs
Normal file
28
src/DigitalData.Auth.API/Services/DIExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using DigitalData.Auth.API.Entities;
|
||||||
|
using DigitalData.Auth.API.Services.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Services;
|
||||||
|
|
||||||
|
public static class DIExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAuthService(this IServiceCollection services, IConfiguration? configuration = null, Action<List<Consumer>>? consumerOptions = null)
|
||||||
|
{
|
||||||
|
List<Consumer> consumers = new();
|
||||||
|
|
||||||
|
var consumerKey = $"{nameof(Consumer)}s";
|
||||||
|
if (configuration?.GetSection(consumerKey).Get<IEnumerable<Consumer>>() is IEnumerable<Consumer> consumersFromConfig)
|
||||||
|
consumers.AddRange(consumersFromConfig);
|
||||||
|
|
||||||
|
consumerOptions?.Invoke(consumers);
|
||||||
|
|
||||||
|
if(consumers.Count == 0)
|
||||||
|
throw new InvalidOperationException($"No Consumer list found in {consumerKey} in configuration.");
|
||||||
|
|
||||||
|
services.AddSingleton(Options.Create(consumers));
|
||||||
|
services.AddSingleton<IConsumerService, ConfiguredConsumerService>();
|
||||||
|
services.AddSingleton<INotifier, Notifier>();
|
||||||
|
services.AddMemoryCache();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/DigitalData.Auth.API/Services/Notifier.cs
Normal file
21
src/DigitalData.Auth.API/Services/Notifier.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using DigitalData.Auth.Abstractions;
|
||||||
|
using DigitalData.Auth.API.Hubs;
|
||||||
|
using DigitalData.Auth.API.Services.Contracts;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace DigitalData.Auth.API.Services;
|
||||||
|
|
||||||
|
public class Notifier : INotifier
|
||||||
|
{
|
||||||
|
private readonly IHubContext<AuthHub, IAuthListenHandler> _hubContext;
|
||||||
|
|
||||||
|
public Notifier(IHubContext<AuthHub, IAuthListenHandler> hubContext)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateKeyAsync(string issuer, string audience, string value)
|
||||||
|
{
|
||||||
|
await _hubContext.Clients.All.ReceivePublicKeyAsync(issuer, audience, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,111 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"UseSwagger": true,
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"DirectorySearchOptions": {
|
||||||
|
"ServerName": "DD-VMP01-DC01",
|
||||||
|
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works"
|
||||||
|
},
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"LocalConsumer": {
|
||||||
|
"Id": -1,
|
||||||
|
"Name": "auth-flow",
|
||||||
|
"Audience": "auth.digitaldata.works",
|
||||||
|
"Password": "n7l^)s,v;jbr0c+x%urk=fak4[s==z?<"
|
||||||
|
},
|
||||||
|
"CryptParams": {
|
||||||
|
"KeySizeInBits": 4096,
|
||||||
|
"Padding": "OaepSHA512",
|
||||||
|
"PemDirectory": "Secrets",
|
||||||
|
"Decryptors": [
|
||||||
|
{
|
||||||
|
"IsEncrypted": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"TokenDescriptors": [
|
||||||
|
{
|
||||||
|
"Id": "4062504f-f081-43d1-b4ed-78256a0879e1",
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "auth.digitaldata.works",
|
||||||
|
"IsEncrypted": true,
|
||||||
|
"Lifetime": "5:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "61c07d26-baa8-4cbb-bb33-ac4ee1838c3a",
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "work-flow.digitaldata.works",
|
||||||
|
"IsEncrypted": true,
|
||||||
|
"Lifetime": "02:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "9e3b0e68-c3e4-489e-b68c-47df26d6b612",
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "user-manager.digitaldata.works",
|
||||||
|
"IsEncrypted": true,
|
||||||
|
"Lifetime": "02:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "f3c0881b-c349-442a-ac24-d02da0798abd",
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "sign-flow-gen.digitaldata.works",
|
||||||
|
"IsEncrypted": true,
|
||||||
|
"Lifetime": "12:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"NLog": {
|
||||||
|
"throwConfigExceptions": true,
|
||||||
|
"variables": {
|
||||||
|
"logDirectory": "E:\\LogFiles\\Digital Data\\Auth.API",
|
||||||
|
"logFileNamePrefix": "${shortdate}-Auth.API"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"infoLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"warningLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Warning.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"errorLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"criticalLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Info",
|
||||||
|
"writeTo": "infoLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Warn",
|
||||||
|
"writeTo": "warningLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Error",
|
||||||
|
"writeTo": "errorLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Fatal",
|
||||||
|
"writeTo": "criticalLogs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
22
src/DigitalData.Auth.API/backdoors.json
Normal file
22
src/DigitalData.Auth.API/backdoors.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"BackdoorParams": {
|
||||||
|
"Backdoors": [
|
||||||
|
{
|
||||||
|
"Username": "TekH",
|
||||||
|
"PasswordHash": "$2a$11$/0Qq8Hi9xrPQMSRaNaNmguxJHCvIS27WwPL9U/zeMJz0twxKJxqY2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Username": "CURSOR_ADMIN01",
|
||||||
|
"PasswordHash": "$2a$11$IX.S/u0i/pVaaY.1EDxYkubS8s2VYTOArnu.SorPvZcFK35MxTeq2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Username": "FABRIK19-User01",
|
||||||
|
"PasswordHash": "$2a$11$SyvDueS9qRxqDMorHxyV2er14udoFwKuKMuc5pWM3dak3yZYAidDm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Username": "CURSOR_USER01",
|
||||||
|
"PasswordHash": "$2a$11$Gqg8i6Knv80HJF/Y4sC9p.z6Rq0acUzJ5H5gSsJm1OTmTfGMZU3cq"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/DigitalData.Auth.API/consumer-repository.json
Normal file
22
src/DigitalData.Auth.API/consumer-repository.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"Consumers": [
|
||||||
|
{
|
||||||
|
"Id": 0,
|
||||||
|
"Name": "work-flow",
|
||||||
|
"Audience": "work-flow.digitaldata.works",
|
||||||
|
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'iL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 1,
|
||||||
|
"Name": "user-manager",
|
||||||
|
"Audience": "user-manager.digitaldata.works",
|
||||||
|
"Password": "a098Hvu1-y29ep{KPQO]#>8TK+fk{O`_d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 2,
|
||||||
|
"Name": "sign-flow-gen",
|
||||||
|
"Audience": "sign-flow-gen.digitaldata.works",
|
||||||
|
"Password": "Gpm63fny0W63Klc2eWC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"Consumers": [
|
|
||||||
{
|
|
||||||
"Name": "WorkFlow.API",
|
|
||||||
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'i<>L"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "DigitalData.UserManager.API",
|
|
||||||
"Password": "a098Hvu1-y29ep{KPQO]#>8TK+fk{O`_d"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user