Compare commits
157 Commits
45c161d3cd
...
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 | ||
|
|
6553104f8d | ||
|
|
a14ada60b4 | ||
|
|
65cb699989 | ||
|
|
2ddb26c69f | ||
|
|
310d480f59 | ||
|
|
3d26e5d521 | ||
|
|
d7023ddbe8 | ||
|
|
b903dc9152 | ||
|
|
d2d9dbfc34 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -412,3 +412,4 @@ FodyWeavers.xsd
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
/src/DigitalData.Auth.API/Secrets
|
||||
|
||||
@@ -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>
|
||||
@@ -5,7 +5,13 @@ VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C0123B52-5168-4C87-98A0-11A220EC392F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -17,11 +23,29 @@ Global
|
||||
{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.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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {4D163037-043C-41AE-AB94-C7314F2C38DA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
23
src/DigitalData.Auth.API/Config/AuthApiParams.cs
Normal file
23
src/DigitalData.Auth.API/Config/AuthApiParams.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using DigitalData.Auth.API.Entities;
|
||||
|
||||
namespace DigitalData.Auth.API.Config
|
||||
{
|
||||
public class AuthApiParams
|
||||
{
|
||||
public CookieOptionsProvider DefaultCookieOptions { get; init; } = new()
|
||||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Strict
|
||||
};
|
||||
|
||||
public string DefaultCookieName { get; init; } = "AuthToken";
|
||||
|
||||
public string DefaultQueryStringKey { get; init; } = "AuthToken";
|
||||
|
||||
public required string Issuer { get; init; }
|
||||
|
||||
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>();
|
||||
}
|
||||
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>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.2.0</Version>
|
||||
<AssemblyVersion>1.2.0</AssemblyVersion>
|
||||
<FileVersion>1.2.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
<Folder Include="Controllers\" />
|
||||
<ProjectReference Include="..\..\DigitalData.Auth.Abstractions\DigitalData.Auth.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
@DigitalData.Auth.API_HostAddress = http://localhost:5075
|
||||
|
||||
GET {{DigitalData.Auth.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
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
|
||||
);
|
||||
}
|
||||
73
src/DigitalData.Auth.API/Entities/CookieOptionsProvider.cs
Normal file
73
src/DigitalData.Auth.API/Entities/CookieOptionsProvider.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
namespace DigitalData.Auth.API.Entities
|
||||
{
|
||||
public class CookieOptionsProvider
|
||||
{
|
||||
public TimeSpan Lifetime { get; init; } = new(1, 0, 0);
|
||||
|
||||
private readonly CookieOptions _optionsBase = new();
|
||||
|
||||
#region CookieOptions
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the domain to associate the cookie with.
|
||||
//
|
||||
// Returns:
|
||||
// The domain to associate the cookie with.
|
||||
public string? Domain { get => _optionsBase.Domain; set => _optionsBase.Domain = value; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the cookie path.
|
||||
//
|
||||
// Returns:
|
||||
// The cookie path.
|
||||
public string? Path { get => _optionsBase.Path; set => _optionsBase.Path = value; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the expiration date and time for the cookie.
|
||||
//
|
||||
// Returns:
|
||||
// The expiration date and time for the cookie.
|
||||
public DateTimeOffset? Expires { get => _optionsBase.Expires; set => _optionsBase.Expires = value; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets a value that indicates whether to transmit the cookie using Secure
|
||||
// Sockets Layer (SSL)--that is, over HTTPS only.
|
||||
//
|
||||
// Returns:
|
||||
// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false.
|
||||
public bool Secure { get => _optionsBase.Secure; set => _optionsBase.Secure = value; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the value for the SameSite attribute of the cookie. The default
|
||||
// value is Microsoft.AspNetCore.Http.SameSiteMode.Unspecified
|
||||
//
|
||||
// Returns:
|
||||
// The Microsoft.AspNetCore.Http.SameSiteMode representing the enforcement mode
|
||||
// of the cookie.
|
||||
public SameSiteMode SameSite { get => _optionsBase.SameSite; set => _optionsBase.SameSite = value; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets a value that indicates whether a cookie is inaccessible by client-side
|
||||
// script.
|
||||
//
|
||||
// Returns:
|
||||
// true if a cookie must not be accessible by client-side script; otherwise, false.
|
||||
public bool HttpOnly { get => _optionsBase.HttpOnly; set => _optionsBase.HttpOnly = value; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the max-age for the cookie.
|
||||
//
|
||||
// Returns:
|
||||
// The max-age date and time for the cookie.
|
||||
public TimeSpan? MaxAge { get => _optionsBase.MaxAge; set => _optionsBase.MaxAge = value; }
|
||||
//
|
||||
// Summary:
|
||||
// Indicates if this cookie is essential for the application to function correctly.
|
||||
// If true then consent policy checks may be bypassed. The default value is false.
|
||||
public bool IsEssential { get => _optionsBase.IsEssential; set => _optionsBase.IsEssential = value; }
|
||||
#endregion
|
||||
|
||||
public CookieOptions Create(TimeSpan? lifetime = null) => new(_optionsBase) { Expires = DateTime.UtcNow.AddTicks(lifetime?.Ticks ?? Lifetime.Ticks) };
|
||||
}
|
||||
}
|
||||
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,25 +1,169 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
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;
|
||||
|
||||
// Add services to the container.
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
logger.Info("Logging initialized.");
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
try
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
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 apiParams = config.Get<AuthApiParams>() ?? throw new InvalidOperationException("AuthApiOptions is missing or invalid in appsettings.");
|
||||
|
||||
// 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.AddEndpointsApiExplorer();
|
||||
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();
|
||||
|
||||
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.
|
||||
var use_swagger = config.GetValue<bool>("UseSwagger");
|
||||
if (app.Environment.IsDevelopment() || use_swagger)
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapHub<AuthHub>("/auth-hub");
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
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": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5075",
|
||||
"environmentVariables": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7192;http://localhost:5075",
|
||||
"environmentVariables": {
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user