Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4bbf686
chore: update dependencies
engineering87 Dec 18, 2024
7f6eb48
Merge branch 'develop' of https://github.com/engineering87/WART into …
engineering87 Dec 18, 2024
34b3319
feat!: upgrade project to .NET 9
engineering87 Dec 28, 2024
4ffc939
chore(deps): update dependencies to latest versions
engineering87 Jan 9, 2025
94a45c8
chore(deps): update dependencies to latest versions
engineering87 Jan 12, 2025
fde7807
chore: update packages to latest versions
engineering87 Jan 15, 2025
dbe7024
chore: update packages to latest versions
engineering87 Jan 15, 2025
cceeb8f
chore(deps): update dependencies to latest versions
engineering87 Jan 18, 2025
c253fc3
chore(deps): update dependencies to latest versions
engineering87 Jan 24, 2025
0840705
chore(deps): update dependencies to latest versions
engineering87 Feb 17, 2025
c89428f
chore(deps): update dependencies to latest versions
engineering87 Feb 22, 2025
ec12a98
chore(deps): update dependencies to latest versions
engineering87 Mar 3, 2025
aa338a1
chore(deps): update dependencies to latest versions
engineering87 Mar 10, 2025
9436158
chore(deps): update dependencies to latest versions
engineering87 Mar 16, 2025
2dc0a5c
chore(deps): update dependencies to latest versions
engineering87 Mar 19, 2025
8eb6c69
chore(deps): update dependencies to latest versions
engineering87 Mar 23, 2025
69aba65
chore(deps): update dependencies to latest versions
engineering87 Apr 1, 2025
603a436
chore(deps): update dependencies to latest versions
engineering87 Apr 9, 2025
1f6da01
chore(deps): update dependencies to latest versions
engineering87 Apr 14, 2025
06983d2
chore(deps): update dependencies to latest versions
engineering87 May 6, 2025
615ea4f
chore(deps): update dependencies to latest versions
engineering87 May 14, 2025
9534b3c
chore(deps): update dependencies to latest versions
engineering87 May 18, 2025
2d2c567
feat(signalr): add cookie-based authentication support
engineering87 May 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ WART is a C# .NET library that enables you to extend any Web API controller and

## Features
- Converts REST API calls into SignalR events, enabling real-time communication.
- Provides controllers (`WartController`, `WartControllerJwt`) for automatic SignalR event broadcasting.
- Provides controllers (`WartController`, `WartControllerJwt`, `WartControllerCookie`) for automatic SignalR event broadcasting.
- Supports JWT authentication for SignalR hub connections.
- Allows API exclusion from event broadcasting with `[ExcludeWart]` attribute.
- Enables group-specific event dispatching with `[GroupWart("group_name")]`.
Expand Down Expand Up @@ -134,6 +134,18 @@ hubConnection.On<string>("Send", (data) =>

In the source code you can find a simple test client and WebApi project.

## Supported Authentication Modes

The project supports three authentication modes for accessing the SignalR Hub:

| Mode | Description | Hub Class | Required Middleware |
|--------------------------|---------------------------------------------------------------------------|----------------------|---------------------------|
| **No Authentication** | Open access without identity verification | `WartHub` | None |
| **JWT (Bearer Token)** | Authentication via JWT token in the `Authorization: Bearer <token>` header | `WartHubJwt` | `UseJwtMiddleware()` |
| **Cookie Authentication**| Authentication via HTTP cookies issued after login | `WartHubCookie` | `UseCookieMiddleware()` |

> ⚙️ Authentication mode is selected through the `HubType` configuration in the application startup.

### Excluding APIs from Event Propagation
There might be scenarios where you want to exclude specific APIs from propagating events to connected clients. This can be particularly useful when certain endpoints should not trigger updates, notifications, or other real-time messages through SignalR. To achieve this, you can use a custom filter called `ExcludeWartAttribute`. By decorating the desired API endpoints with this attribute, you can prevent them from being included in the SignalR event propagation logic, for example:

Expand Down
27 changes: 19 additions & 8 deletions src/WART-Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,27 @@ private static async Task Main()

Console.WriteLine($"Connecting to {wartHubUrl}");

var auth = configuration["AuthenticationJwt"];
var auth = configuration["AuthenticationType"] ?? "NoAuth";

if (bool.Parse(auth))
switch (auth.ToLowerInvariant())
{
var key = configuration["Key"];
await WartTestClientJwt.ConnectAsync(wartHubUrl, key);
}
else
{
await WartTestClient.ConnectAsync(wartHubUrl);
default:
case "noauth":
{
await WartTestClient.ConnectAsync(wartHubUrl);
break;
}
case "jwt":
{
var key = configuration["Key"];
await WartTestClientJwt.ConnectAsync(wartHubUrl, key);
break;
}
case "cookie":
{
await WartTestClientCookie.ConnectAsync(wartHubUrl);
break;
}
}

Console.WriteLine($"Connected to {wartHubUrl}");
Expand Down
10 changes: 5 additions & 5 deletions src/WART-Client/WART-Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>WART_Client</RootNamespace>
<StartupObject>WART_Client.Program</StartupObject>
<IsPackable>false</IsPackable>
Expand All @@ -21,10 +21,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.10.0" />
</ItemGroup>

</Project>
91 changes: 91 additions & 0 deletions src/WART-Client/WartTestClientCookie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
// This code is licensed under MIT license (see LICENSE.txt for details)
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace WART_Client
{
/// <summary>
/// A simple SignalR WART test client with Cookie authentication.
/// </summary>
public static class WartTestClientCookie
{
public static async Task ConnectAsync(string hubUrl)
{
try
{
var cookieContainer = new CookieContainer();
var handler = new HttpClientHandler
{
CookieContainer = cookieContainer,
UseCookies = true,
AllowAutoRedirect = true
};

using var httpClient = new HttpClient(handler);

var loginContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", "test_username"),
new KeyValuePair<string, string>("password", "test_password")
});

var loginUri = new Uri(new Uri(hubUrl), "/api/TestCookie/login");
var loginResponse = await httpClient.PostAsync(loginUri, loginContent);
loginResponse.EnsureSuccessStatusCode();

Console.WriteLine("Login successful. Connecting to SignalR...");

//var uri = new Uri(hubUrl);
//cookieContainer.Add(uri, new Cookie("WART.AuthCookie", "sample_value"));

var hubConnection = new HubConnectionBuilder()
.WithUrl(hubUrl, options =>
{
options.HttpMessageHandlerFactory = _ => handler;
options.Transports = HttpTransportType.WebSockets |
HttpTransportType.ServerSentEvents |
HttpTransportType.LongPolling;
})
.WithAutomaticReconnect()
.Build();

hubConnection.On<string>("Send", (data) =>
{
Console.WriteLine(data);
Console.WriteLine($"Message size: {Encoding.UTF8.GetBytes(data).Length} bytes");
Console.WriteLine();
});

hubConnection.Closed += async (ex) =>
{
Console.WriteLine($"Connection closed: {ex?.Message}");
await Task.Delay(new Random().Next(0, 5) * 1000);
if (hubConnection != null)
await hubConnection.StartAsync();
};

hubConnection.On<Exception>("ConnectionFailed", (ex) =>
{
Console.WriteLine($"Connection failed: {ex.Message}");
return Task.CompletedTask;
});

await hubConnection.StartAsync();
Console.WriteLine("SignalR connection started.");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

await Task.CompletedTask;
}
}
}
2 changes: 1 addition & 1 deletion src/WART-Client/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"Host": "localhost",
"Port": "54644",
"Hubname": "warthub",
"AuthenticationJwt": "true",
"AuthenticationType": "JWT",
"Key": "dn3341fmcscscwe28419brhwbwgbss4t",
"WartGroup": "SampleGroupName"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
// This code is licensed under MIT license (see LICENSE.txt for details)
using Microsoft.AspNetCore.Builder;

namespace WART_Core.Authentication.Cookie
{
public static class CookieApplicationBuilderExtension
{
/// <summary>
/// Use Cookie authentication dependency to IApplicationBuilder.
/// </summary>
/// <param name="app">The IApplicationBuilder to configure the middleware pipeline.</param>
/// <returns></returns>
public static IApplicationBuilder UseCookieMiddleware(this IApplicationBuilder app)
{
app.UseAuthentication();
app.UseAuthorization();

return app;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
// This code is licensed under MIT license (see LICENSE.txt for details)
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using WART_Core.Hubs;
using WART_Core.Services;

namespace WART_Core.Authentication.Cookie
{
public static class CookieServiceCollectionExtension
{
/// <summary>
/// Adds Cookie authentication middleware to the service collection.
/// Configures the authentication parameters, SignalR settings, and response compression.
/// </summary>
/// <param name="services">The service collection to add the middleware to.</param>
/// <param name="loginPath">Optional path for the login redirect (default: /Account/Login).</param>
/// <param name="accessDeniedPath">Optional path for access denied redirect (default: /Account/Denied).</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddCookieMiddleware(
this IServiceCollection services,
string loginPath = "/Account/Login",
string accessDeniedPath = "/Account/AccessDenied")
{
// Configure forwarded headers (support for reverse proxy)
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

// Add logging support
services.AddLogging(configure => configure.AddConsole());

// Add Data Protection with key persistence
var keysPath = Path.Combine(AppContext.BaseDirectory, "keys");
Directory.CreateDirectory(keysPath);
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysPath))
.SetApplicationName("WART_App");

// Configure cookie-based authentication
services.AddAuthentication("WartCookieAuth")
.AddCookie("WartCookieAuth", options =>
{
options.LoginPath = loginPath;
options.AccessDeniedPath = accessDeniedPath;
options.Cookie.Name = "WART.AuthCookie";
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
});

// Register WART event queue service
services.AddSingleton<WartEventQueueService>();

// Register the WART event worker for the cookie-authenticated hub
services.AddHostedService<WartEventWorker<WartHubCookie>>();

// SignalR configuration
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
});

// Compression for SignalR WebSocket/Binary transport
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});

return services;
}
}
}
19 changes: 19 additions & 0 deletions src/WART-Core/Controllers/WartControllerCookie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
// This code is licensed under MIT license (see LICENSE.txt for details)
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using WART_Core.Hubs;

namespace WART_Core.Controllers
{
/// <summary>
/// The WART Controller with Cookie authentication
/// </summary>
public class WartControllerCookie : WartBaseController<WartHubCookie>
{
public WartControllerCookie(IHubContext<WartHubCookie> hubContext, ILogger<WartControllerCookie> logger)
: base(hubContext, logger)
{
}
}
}
8 changes: 7 additions & 1 deletion src/WART-Core/Enum/HubType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ public enum HubType
/// Simple SignalR hub without authentication
/// </summary>
NoAuthentication,

/// <summary>
/// SignalR hub with JWT authentication
/// </summary>
JwtAuthentication
JwtAuthentication,

/// <summary>
/// SignalR hub with Cookie authentication
/// </summary>
CookieAuthentication
}
}
16 changes: 16 additions & 0 deletions src/WART-Core/Hubs/WartHubCookie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
// This code is licensed under MIT license (see LICENSE.txt for details)
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;

namespace WART_Core.Hubs
{
/// <summary>
/// The WART SignalR hub with Cookie-based authentication.
/// </summary>
[Authorize]
public class WartHubCookie : WartHubBase
{
public WartHubCookie(ILogger<WartHubCookie> logger) : base(logger) { }
}
}
Loading