Compare commits

...

34 Commits

Author SHA1 Message Date
Developer 02
6ac2c86520 fix(AuthClientTests): StartAsync_ShouldUpdateAllPublicKey aktualisiert 2025-03-07 10:17:48 +01:00
Developer 02
4e941ed35f feat(AuthClientTests): StartAsync_ShouldUpdateAllPublicKey Testmethode hinzufügen, um zu testen, ob der öffentliche Schlüssel nach StartAsync aktualisiert wird 2025-03-07 09:49:48 +01:00
Developer 02
e925c175a0 refactor(ClientParams): OnMessageReceived aktualisieren, damit es mit UpdatePublicKeys initialisiert wird 2025-03-06 17:02:30 +01:00
Developer 02
eaf41adb58 refactor: OnMessageReceived in ein Ereignis umgewandelt für bessere Ereignisbehandlung
- `OnMessageReceived` von einem Delegaten in ein Ereignis umgewandelt, um die Ereignisabonnierung und -behandlung zu verbessern.
2025-03-06 16:54:50 +01:00
Developer 02
a2c74cbdd9 feat: Refaktorierung von ClientEvents zur Verwendung eines Delegaten für öffentliche Schlüsselaktualisierungen
- Ersetzt `Action<string, string, string, ILogger?>` durch den `ClientEvent`-Delegaten für eine bessere Struktur.
- `ClientEvent`-Delegaten mit `AuthClient`-Referenz eingeführt, um öffentliche Schlüssel direkt zu aktualisieren.
2025-03-06 16:48:47 +01:00
Developer 02
63c37551be feat(AsymmetricPublicKey): Zur Vereinfachung nach Auth.Client verschoben 2025-03-06 14:32:23 +01:00
Developer 02
6198008475 feat(AuthClient): implementierte Methode PublicKeys.get
- GetAllPublicKeysAsync Methode hinzugefügt, um GetPublicKeyAsync Methode für jeden öffentlichen Schlüssel aufzurufen.
 - Aktualisiert, um GetAllPublicKeysAsync Methode nach erfolgreichem Start und Wiederverbindung aufzurufen
2025-03-06 14:18:55 +01:00
Developer 02
8682f1f9e0 feat(AsymmetricPublicKey): zu Abstractions.Models hinzugefügt, um den Empfang öffentlicher Schlüssel zu behandeln.
- AsymmetricPublicKey-Liste mit dem Namen Public Keys zu IAuthClient hinzugefügt.
2025-03-06 13:59:40 +01:00
Developer 02
fb486296f2 fix(AuthClientTest): den Methodennamen aktualisiert, um den Pascal-Fall zu implementieren 2025-03-05 16:23:58 +01:00
Developer 02
aa2572fd17 fix(ClientExtensions): entfernt 2025-03-05 16:22:39 +01:00
Developer 02
7153d6ec46 fix(AuthClient): remove _lazyInitiator 2025-03-05 15:57:56 +01:00
Developer 02
4e3448b4d4 refactor(AuthClient): Entfernen von ConnectionError und Aktualisierung von tryStartAsync zur Protokollierung 2025-03-05 15:53:57 +01:00
Developer 02
36891b5abb feat(AuthClientTests): Added GetpublicKey_ShouldReturnExpectedPublicKey to test GetPublicKeyAsync method of AuthHub 2025-03-05 15:31:24 +01:00
Developer 02
6664a1f342 fix(AuthClientTests): Abhängigkeiten hinzufügen 2025-03-05 14:43:56 +01:00
Developer 02
b2a287cab5 feat(DIExtensions): Add memory cache. 2025-03-05 13:24:03 +01:00
Developer 02
db52e97d03 feat(AuthClient): Added GetPublicKeyAsync method to handle client request to get the key 2025-03-05 13:09:29 +01:00
Developer 02
4c001d4087 feat(AuthHub): Added GetPublicKeyAsync method to send the key to caller 2025-03-05 13:06:07 +01:00
Developer 02
3c37176d5e Reapply "feat(IAuthSenderHandler): GetPublicKeyAsync hinzugefügt, um den öffentlichen Schlüssel des Aufrufers zu aktualisieren"
This reverts commit 0935573b93.
2025-03-05 11:57:18 +01:00
Developer 02
0935573b93 Revert "feat(IAuthSenderHandler): GetPublicKeyAsync hinzugefügt, um den öffentlichen Schlüssel des Aufrufers zu aktualisieren"
This reverts commit f30f1f127d.
2025-03-05 11:56:00 +01:00
Developer 02
f30f1f127d feat(IAuthSenderHandler): GetPublicKeyAsync hinzugefügt, um den öffentlichen Schlüssel des Aufrufers zu aktualisieren 2025-03-04 12:25:49 +01:00
Developer 02
1fe3fb9008 refactor: IAuthListenHandler und IAuthSenderHandler aktualisiert, um Issuer und Audiance anstelle von Name zu verwenden 2025-03-04 12:20:26 +01:00
Developer 02
d21da5028e Revert "refactor(AuthHub): SendKeyAsync aktualisiert, um Caller anstelle von All zu verwenden"
This reverts commit 062942b2d2.
2025-03-04 11:53:05 +01:00
Developer 02
062942b2d2 refactor(AuthHub): SendKeyAsync aktualisiert, um Caller anstelle von All zu verwenden 2025-03-04 11:45:44 +01:00
Developer 02
c47197606b Revert "feat: Hinzufügen der Methode GetPublicKeyAsync zu IAuthListenHandler und IAuthSenderHandler"
This reverts commit 137ccaa563.
2025-03-04 10:16:23 +01:00
Developer 02
137ccaa563 feat: Hinzufügen der Methode GetPublicKeyAsync zu IAuthListenHandler und IAuthSenderHandler 2025-03-04 09:20:36 +01:00
Developer 02
4062fe750a refactor(IAuthSenderHandler): rename subject input to name. 2025-03-03 16:09:15 +01:00
Developer 02
cb6ec8b5e6 feat(DigitalData.Auth): configured package properties of Abstractions and Client 2025-03-03 16:04:11 +01:00
Developer 02
7873542aca fix(DIExtensions): ConsumerOptions-Eingabe in AddAuthService-Methode hinzugefügt, um Konsuemrn nach der Übernahme aus der Config arrangieren zu können.
- Standardiezd consumerKey Benennung
2025-02-11 13:20:54 +01:00
Developer 02
6694e4b626 feat(AuthHubTests): Erstellt, um Hub und Melder zu testen 2025-02-11 11:05:46 +01:00
Developer 02
0cce082cb7 refactor(RetryPolicy): Ungenutzte Eigenschaft entfernt 2025-02-11 10:34:57 +01:00
Developer 02
7f39cbe24a feat(AuthClient): Konfiguration der Wiederholungsrichtlinie im Falle eines Verbindungsverlustes hinzugefügt. 2025-02-11 10:33:37 +01:00
Developer 02
484cc86a29 feat(Melder): Erstellt, um aktuelle Schlüssel an den Kunden zu senden 2025-02-11 09:38:11 +01:00
Developer 02
5ab1f24ce5 feat: Aktualisiert, um Token durch Query-String zu behandeln 2025-02-11 08:56:29 +01:00
Developer 02
33ead6ebf4 fix: UniqueName aktualisiert, um den Benutzernamen in den Ansprüchen des Benutzernamens zu halten. 2025-02-10 14:09:15 +01:00
22 changed files with 484 additions and 94 deletions

View File

@@ -1,6 +0,0 @@
namespace DigitalData.Auth.Abstractions;
public static class ClientExtensions
{
public static bool IsConnectionFailed(this IAuthClient client) => client.ConnectionError is not null;
}

View File

@@ -4,6 +4,27 @@
<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.0.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>
</PropertyGroup>
<ItemGroup>
<Compile Remove="PublicKey.cs" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\nuget-package-icons\auth_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@@ -4,11 +4,7 @@ public interface IAuthClient : IAuthListenHandler, IAuthSenderHandler
{
bool IsConnected { get; }
Exception? ConnectionError { get; }
bool IsConnectionFailed => ConnectionError is not null;
Task StartAsync();
Task<bool> TryStartAsync();
}
}

View File

@@ -2,5 +2,5 @@
public interface IAuthListenHandler
{
Task ReceiveKeyAsync(string topic, string key);
Task ReceivePublicKeyAsync(string issuer, string audience, string value);
}

View File

@@ -2,5 +2,7 @@
public interface IAuthSenderHandler
{
Task SendKeyAsync(string topic, string key);
Task SendPublicKeyAsync(string issuer, string audience, string key);
Task GetPublicKeyAsync(string issuer, string audience);
}

View File

@@ -0,0 +1,14 @@
using DigitalData.Core.Abstractions.Security;
namespace DigitalData.Auth.Client;
public class AsymmetricPublicKey : IUniqueSecurityContext, IAsymmetricPublicKey
{
public required string Issuer { get; init; }
public required string Audience { get; init; }
public string? Id { get; init; }
public string Content { get; internal set; } = string.Empty;
}

View File

@@ -9,61 +9,65 @@ public class AuthClient : IAuthClient, IAsyncDisposable
{
private readonly HubConnection _connection;
private readonly Lazy<Task<bool>> _lazyInitiator;
private readonly ILogger? _logger;
private readonly ILogger<AuthClient>? _logger;
private readonly ClientParams _params;
public AuthClient(IOptions<ClientParams> paramsOptions, HubConnectionBuilder connectionBuilder, ILogger<AuthClient>? logger = null)
{
_connection = connectionBuilder
.WithUrl(paramsOptions.Value.Url)
.Build();
_params = paramsOptions.Value;
_connection.On<string, string>(nameof(ReceiveKeyAsync), ReceiveKeyAsync);
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 => await GetAllPublicKeysAsync();
_logger = logger;
_params = paramsOptions.Value;
_lazyInitiator = new(async () =>
{
try
{
await _connection.StartAsync();
IsConnected = true;
return true;
}
catch(Exception ex)
{
ConnectionError = ex;
throw;
}
});
}
public bool IsConnected { get; private set; } = false;
public Exception? ConnectionError { get; private set; }
public IEnumerable<AsymmetricPublicKey> PublicKeys => _params.PublicKeys;
public async Task StartAsync() => await _lazyInitiator.Value;
public async Task StartAsync()
{
await _connection.StartAsync();
IsConnected = true;
await GetAllPublicKeysAsync();
}
public async Task<bool> TryStartAsync()
{
try
{
return await _lazyInitiator.Value;
await StartAsync();
return true;
}
catch
catch(Exception ex)
{
_logger?.LogError(ex, "{message}", ex.Message);
return false;
}
}
public Task ReceiveKeyAsync(string user, string message) => Task.Run(() => _params.Events.OnMessageReceived(user, message, _logger));
public Task ReceivePublicKeyAsync(string issuer, string audience, string message) => Task.Run(() => _params.TriggerOnMessageReceived(this, issuer, audience, message, _logger));
public Task SendKeyAsync(string user, string message) => _connection.InvokeAsync(nameof(SendKeyAsync), user, message);
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);
}
public virtual async ValueTask DisposeAsync()
{

View File

@@ -1,10 +1,18 @@
using Microsoft.Extensions.Logging;
using DigitalData.Core.Abstractions.Security;
using Microsoft.Extensions.Logging;
namespace DigitalData.Auth.Client
namespace DigitalData.Auth.Client;
public delegate void ClientEvent(AuthClient client, string issuer, string audience, string content, ILogger? logger = null);
public static class ClientEvents
{
public class ClientEvents
public static readonly ClientEvent UpdatePublicKeys = (client, issuer, audience, content, logger) =>
{
public Action<string, string, ILogger?> OnMessageReceived { get; set; } = (user, message, logger)
=> logger?.LogInformation("{user}: {message}", user, message);
}
if(client.PublicKeys.TryGet(issuer, audience, out var publicKey))
publicKey.Content = 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);
};
}

View File

@@ -1,10 +1,46 @@
namespace DigitalData.Auth.Client;
using DigitalData.Auth.Client;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace DigitalData.Auth.Client;
public class ClientParams
{
#pragma warning disable CS8618 // throw exception in DI extension if it not set
public string Url { get; set; }
#pragma warning restore CS8618
public string Url { get; set; } = string.Empty;
public readonly ClientEvents Events = new();
private readonly Lazy<IRetryPolicy?> _lazyRetryPolicy;
/// <summary>
/// Controls when the client attempts to reconnect and how many times it does so.
/// </summary>
public IRetryPolicy? RetryPolicy => _lazyRetryPolicy.Value;
/// <summary>
/// To simplify the assignment of <seealso cref="RetryPolicy"/>
/// </summary>
public Func<RetryContext, TimeSpan?>? NextRetryDelay { get; set; }
/// <summary>
/// To be able to serilize the simple <seealso cref="RetryPolicy"/>
/// </summary>
public TimeSpan? RetryDelay { get; set; }
public event ClientEvent OnMessageReceived = ClientEvents.UpdatePublicKeys;
internal void TriggerOnMessageReceived(AuthClient client, string issuer, string audience, string key, ILogger? logger = null)
=> OnMessageReceived(client, issuer, audience, key, logger);
public ClientParams()
{
_lazyRetryPolicy = new(() =>
{
if (RetryDelay is not null)
return new RetryPolicy(ctx => RetryDelay);
else if(NextRetryDelay is not null)
return new RetryPolicy(NextRetryDelay);
return null;
});
}
public List<AsymmetricPublicKey> PublicKeys { get; init; } = new();
}

View File

@@ -4,9 +4,29 @@
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>DigitalData.Auth.Client</PackageId>
<Version>1.0.0</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.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\nuget-package-icons\auth_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.3.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.1" />
</ItemGroup>

View 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);
}

View 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)
.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.OnMessageReceived += (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));
});
}
}

View File

@@ -1,6 +1,10 @@
using DigitalData.Auth.Abstractions;
using DigitalData.Auth.API.Hubs;
using DigitalData.Auth.Client;
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security;
using DigitalData.Core.Security.Config;
using DigitalData.Core.Security.RSAKey;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -31,6 +35,17 @@ public class AuthClientTests
}
}
private readonly IEnumerable<RSATokenDescriptor> _tokenDescriptors =
[
new()
{
Issuer = "Foo",
Audience = "Bar",
Lifetime = new TimeSpan(1, 0, 0),
Content = Instance.RSAFactory.CreatePrivateKeyPem()
}
];
[SetUp]
public void Setup()
{
@@ -49,6 +64,13 @@ public class AuthClientTests
// Create builder and add SignalR service
var builder = WebApplication.CreateBuilder();
builder.Services.AddSignalR();
builder.Services.AddCryptoFactory(new CryptoFactoryParams()
{
PemDirectory = "/",
Decryptors = [new RSADecryptor()],
TokenDescriptors = _tokenDescriptors
});
builder.Services.AddMemoryCache();
// Listen AvailablePort and map hub
var _app = builder.Build();
@@ -87,19 +109,15 @@ public class AuthClientTests
await client.StartAsync();
// Assert
Assert.Multiple(() =>
{
Assert.That(client.IsConnected);
Assert.That(!client.IsConnectionFailed);
Assert.That(client.ConnectionError, Is.Null);
});
Assert.That(client.IsConnected);
}
[Test]
public async Task ReceiveMessage_ShouldCallOnMessageReceived()
{
// Arrange
string rcv_topic = string.Empty;
string rcv_issuer = string.Empty;
string rcv_audience = string.Empty;
string rcv_key = string.Empty;
// Sender client
@@ -111,20 +129,22 @@ public class AuthClientTests
var provider_receiver = Build(opt =>
{
opt.Url = _hubUrl;
opt.Events.OnMessageReceived = (topic, key, logger) =>
opt.OnMessageReceived += (client, issuer, audience, key, logger) =>
{
rcv_topic = topic;
rcv_issuer = issuer;
rcv_audience = audience;
rcv_key = key;
};
});
var client_receiver = provider_receiver.GetRequiredService<IAuthClient>();
await client_receiver.StartAsync();
string topic = "topic";
string issuer = "issuer";
string audience = "audience";
string key = "key";
// Act
await sender_client.SendKeyAsync(topic, key);
await sender_client.SendPublicKeyAsync(issuer, audience, key);
// delay fort getting answer
await Task.Delay(2000);
@@ -132,8 +152,60 @@ public class AuthClientTests
// Assert
Assert.Multiple(() =>
{
Assert.That(rcv_topic, Is.EqualTo(topic));
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 provider = Build(opt =>
{
opt.Url = _hubUrl;
opt.OnMessageReceived += (client, issuer, audience, key, logger) => publicKey = key;
});
var client = provider.GetRequiredService<IAuthClient>();
await client.StartAsync();
var expectedPublicKey = _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 AsymmetricPublicKey() { Issuer = "Foo", Audience = "Bar" };
var provider = Build(opt =>
{
opt.Url = _hubUrl;
opt.PublicKeys.Add(publicKey);
});
var client = provider.GetRequiredService<IAuthClient>();
await client.StartAsync();
// Act
var expectedPublicKey = _tokenDescriptors.Get("Foo", "Bar").PublicKey;
// wait for network
await Task.Delay(2000);
// Assert
Assert.That(publicKey.Content, Is.EqualTo(expectedPublicKey.Content));
}
}

View File

@@ -9,8 +9,13 @@
<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.0.0" />
<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" />

View File

@@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.Client", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Auth.Tests", "DigitalData.Auth.Tests\DigitalData.Auth.Tests.csproj", "{AF517FD9-3EBE-4452-AAEC-DFF17CC270E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Auth.Abstractions", "DigitalData.Auth.Abstractions\DigitalData.Auth.Abstractions.csproj", "{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}"
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
@@ -23,16 +23,16 @@ 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 = Debug|Any CPU
{521A2BC0-AEA8-4500-AAA9-1951556EDF9F}.Debug|Any CPU.Build.0 = Debug|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 = Debug|Any CPU
{09FF9BF0-25BB-4EB2-B1B2-6D2873B9538C}.Debug|Any CPU.Build.0 = Debug|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

View File

@@ -12,6 +12,8 @@ namespace DigitalData.Auth.API.Config
public string DefaultCookieName { get; init; } = "AuthToken";
public string DefaultQueryStringKey { get; init; } = "AuthToken";
public required string Issuer { get; init; }
public bool RequireHttpsMetadata { get; init; } = true;

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.2.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.3.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.0" />
<PackageReference Include="DigitalData.Core.Security" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />

View File

@@ -1,16 +1,47 @@
using DigitalData.Auth.Abstractions;
using DigitalData.Core.Abstractions.Security;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
namespace DigitalData.Auth.API.Hubs;
public class AuthHub : Hub<IAuthListenHandler>, IAuthSenderHandler
{
public async Task SendKeyAsync(string user, string message)
=> await Clients.All.ReceiveKeyAsync(user, message);
private readonly ICryptoFactory _cFactory;
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.ReceiveKeyAsync(user, message);
private readonly ILogger _logger;
public async Task SendMessageToGroup(string user, string message)
=> await Clients.Group("Auth.API Consumers").ReceiveKeyAsync(user, message);
private readonly IMemoryCache _cache;
private readonly static string CacheId = Guid.NewGuid().ToString();
public AuthHub(ICryptoFactory cryptoFactory, ILogger<AuthHub> logger, IMemoryCache cache)
{
_cFactory = cryptoFactory;
_logger = logger;
_cache = cache;
}
public async Task GetPublicKeyAsync(string issuer, string audience)
{
if(_cFactory.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);
}

View File

@@ -22,7 +22,7 @@ var apiParams = config.Get<AuthApiParams>() ?? throw new InvalidOperationExcepti
// Add services to the container.
builder.Services.Configure<AuthApiParams>(config);
builder.Services.AddConsumerService(config);
builder.Services.AddAuthService(config);
builder.Services.AddCryptoFactory(config.GetSection("CryptParams"));
builder.Services.AddJwtSignatureHandler<Consumer>(api => new Dictionary<string, object>
{
@@ -32,7 +32,7 @@ builder.Services.AddJwtSignatureHandler<Consumer>(api => new Dictionary<string,
builder.Services.AddJwtSignatureHandler<UserReadDto>(user => new Dictionary<string, object>
{
{ JwtRegisteredClaimNames.Sub, user.Id },
{ JwtRegisteredClaimNames.UniqueName, user.Id },
{ JwtRegisteredClaimNames.UniqueName, user.Username },
{ JwtRegisteredClaimNames.Email, user.Email ?? string.Empty },
{ JwtRegisteredClaimNames.GivenName, user.Prename ?? string.Empty },
{ JwtRegisteredClaimNames.FamilyName, user.Name ?? string.Empty },
@@ -102,11 +102,14 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
{
OnMessageReceived = context =>
{
// if there is no token read related cookie
if (context.Token is null // if there is no token
&& context.Request.Cookies.TryGetValue(apiParams!.DefaultCookieName, out var token) // get token from cookies
&& token is not null)
context.Token = token;
// 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;
}
};

View File

@@ -0,0 +1,6 @@
namespace DigitalData.Auth.API.Services.Contracts;
public interface INotifier
{
Task UpdateKeyAsync(string issuer, string audience, string value);
}

View File

@@ -2,16 +2,27 @@
using DigitalData.Auth.API.Services.Contracts;
using Microsoft.Extensions.Options;
namespace DigitalData.Auth.API.Services
namespace DigitalData.Auth.API.Services;
public static class DIExtensions
{
public static class DIExtensions
public static IServiceCollection AddAuthService(this IServiceCollection services, IConfiguration? configuration = null, Action<List<Consumer>>? consumerOptions = null)
{
public static IServiceCollection AddConsumerService(this IServiceCollection services, IConfiguration configuration, string key = "Consumers")
{
var consumers = configuration.GetSection(key).Get<IEnumerable<Consumer>>() ?? throw new InvalidOperationException($"No Consumer list found in {key} in configuration.");
services.AddSingleton(Options.Create(consumers));
services.AddSingleton<IConsumerService, ConfiguredConsumerService>();
return services;
}
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;
}
}

View 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);
}
}