Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6efdd30
feat(AuthCallbackHandler): Add a AuthCallback Handler instance that c…
DevTKSS Sep 6, 2025
f367dc5
chore(UriExtensions): update GetParameter
DevTKSS Sep 6, 2025
06a0e34
chore: move AuthCallbackHandler to separate Project
DevTKSS Sep 7, 2025
2aee684
chore: Update UriExtensions along Review suggestions
DevTKSS Sep 7, 2025
f96dbca
chore: Update to use Registration method
DevTKSS Sep 7, 2025
40923cb
chore: move Registration Extension to appropriate folder
DevTKSS Sep 7, 2025
f4fdb70
chore: Add Named Options Registration
DevTKSS Sep 7, 2025
1f37d71
chore: adjust sdk version setup in global.json
DevTKSS Nov 11, 2025
9895989
chore(deps): Apply required Version Bump for logging packages
DevTKSS Nov 11, 2025
5ab6844
chore: Update Project and Namespace naming to match solution structure
DevTKSS Nov 11, 2025
0523e30
test(UnoAuthHandler): Add Test Project to solution
DevTKSS Nov 11, 2025
4f4e09d
chore(OAuthHandlerExt): Align with GuardExtensions
DevTKSS Nov 12, 2025
6013b95
test(OAuthExt): fix test build and update to use v3 xUnit and impleme…
DevTKSS Nov 12, 2025
41f96a3
test(OAuthHandler): Add tests to proof OAuthHandler works with expect…
DevTKSS Nov 12, 2025
0f22af5
chore(SocketException): comment out the failing test case until issue…
DevTKSS Nov 12, 2025
80a4eff
chore: Align extension comments with GuardExtensions
DevTKSS Nov 12, 2025
1b2b2c4
docs(Readme): Add How-To for OAuthCallbackHandler
DevTKSS Nov 12, 2025
dc5e80f
ci(net10): Add .NET 10.0 Setup step
DevTKSS Nov 19, 2025
c63352c
chore(deps): Version bump Uno.Sdk version
DevTKSS Nov 20, 2025
1c42b27
chore: Remove Service Key and name from AuthCallbackHandler
DevTKSS Dec 6, 2025
2d2711b
chore(AuthCallbackHandlerOptions): Change init to get, so configure o…
DevTKSS Dec 6, 2025
bbc0e39
test(AuthCallbackExtensions): Ensure the registration resolves the Ha…
DevTKSS Dec 6, 2025
2903753
chore: lower Accessor level of methods to correctly encapsulate the H…
DevTKSS Dec 6, 2025
358a20a
test(OAuthCallback): Add Test coverage for Handler and Options
DevTKSS Dec 6, 2025
e3c1c82
chore: reduce Dependencys by only Including the Uno.Extensions.Authen…
DevTKSS Dec 6, 2025
df8e04a
chore: Add Uno Auth WinUI Package also in Directory.Packages.props
DevTKSS Dec 6, 2025
fa20eb5
chore(deps): Version bump packages and add analyzer updates to Direct…
DevTKSS Dec 6, 2025
6571324
feat(ServerOptions): Add ServerOptionsExtensions with relative Path t…
DevTKSS Dec 6, 2025
92508dd
chore: Update Readme, as https scheme usage in callback path is not g…
DevTKSS Dec 6, 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
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0'

- name: Setup .NET 10.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: '10.0'

- name: Build
run: dotnet build Yllibed.HttpServer.slnx /p:Configuration=Release
Expand Down
11 changes: 11 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,16 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Update="xunit.runner.visualstudio"
PrivateAssets="all"
IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive"/>

<PackageReference Update="coverlet.collector"
PrivateAssets="all"
IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />

<PackageReference Update="coverlet.msbuild"
PrivateAssets="all"
IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
</ItemGroup>
</Project>
26 changes: 16 additions & 10 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
<Project>
<ItemGroup>
<PackageVersion Include="AwesomeAssertions" Version="9.1.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.25" />
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.215" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.39" />
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.257" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.7.115" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.8" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.8" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
<PackageVersion Include="Uno.Extensions.Authentication.WinUI" Version="7.0.4" />
</ItemGroup>
</Project>
<ItemGroup Label="xUnit Testing">
<PackageVersion Include="xunit.v3" Version="3.2.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
</ItemGroup>
</Project>
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ If you need to expose it on a public or untrusted network:
- Alternatively, use a secure tunnel (SSH, Cloudflare Tunnel, etc.).
- Bind to loopback only (127.0.0.1 / ::1) when you want to ensure local-only access.

Note: Authentication/authorization is not built-in; implement it in your handlers or at the proxy layer as needed.
> [!NOTE]
> Authentication/authorization is not built-in; implement it in your handlers or at the proxy layer as needed.

### GuardHandler (basic request filtering)
GuardHandler provides best-effort filtering of incoming requests, rejecting obviously problematic or oversized requests early. This is lightweight filtering against unsophisticated attacks, not comprehensive security.
Expand Down Expand Up @@ -240,6 +241,9 @@ services.AddYllibedHttpServer(opts =>
- Load balancer configurations requiring static endpoints
- Development scenarios where you need predictable URLs

> [!IMPORTANT]
> Remark: Only one `Server` instance (per address family) can bind to a given fixed port. Attempting to start a second server or test on the same port (e.g., two OAuth flows both forcing port 5001) will throw a `System.Net.Sockets.SocketException` (address already in use). Use dynamic ports (`Port = 0`) unless an OAuth provider mandates an exact fixed redirect URI.

### Basic Configuration Example

```csharp
Expand Down Expand Up @@ -450,3 +454,71 @@ Notes:
- Caching: Cache-Control: no-cache is added by default for SSE responses unless you override it via headers.
- Retry: The SSE spec allows the server to send a retry: <milliseconds> field to suggest a reconnection delay. This helper does not currently provide a dedicated API for retry frames. Most clients also implement their own backoff. If you need this, you can write raw lines through a custom handler or open an issue.
- CORS: If you need cross-origin access, add appropriate headers (e.g., Access-Control-Allow-Origin) via the headers parameter when starting the SSE session.

### OAuthCallbackHandler Usage

The `OAuthCallbackHandler` helps capture an OAuth2.0 (or similar) redirect to a localhost HTTP endpoint and provides a `WaitForCallbackAsync()` method to retrieve the result once the browser hits the callback URL.

Minimal manual registration (fixed port example):
```csharp
var server = new Server(5001); // Fixed port must match the registered redirect URI with the provider
var callbackHandler = new OAuthCallbackHandler(new Uri("http://localhost:5001/oauth/callback"));
server.RegisterHandler(callbackHandler);
var (uri4, _) = server.Start();
Console.WriteLine($"Listening for OAuth redirect at: {callbackHandler.CallbackUri}");

// Later, wait for the incoming redirect
var result = await callbackHandler.WaitForCallbackAsync();
Console.WriteLine($"Status: {result.ResponseStatus}, Raw URI: {result.ResponseData}");
```

Using Microsoft DI with automatic handler registration:
```csharp
var services = new ServiceCollection();
services.AddYllibedHttpServer(opts =>
{
opts.Port = 5001; // Fixed port required if provider expects an exact redirect URI
opts.Hostname4 = "localhost"; // Host portion of the redirect URI
opts.BindAddress4 = IPAddress.Loopback; // Listen only on loopback for safety
});

// Configure the expected callback URI (must match exactly what you registered with the OAuth provider)
services.AddOAuthCallbackHandlerAndRegister(o =>
{
o.CallbackUri = "http://localhost:5001/oauth/callback"; // Server will always use http scheme, no https!
});

var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<Server>();
var (uri4, _) = server.Start();
Console.WriteLine($"Server started: {uri4}");

// Obtain the handler via DI and await the redirect
var authHandler = sp.GetRequiredService<IAuthCallbackHandler>();
var authResult = await authHandler.WaitForCallbackAsync();

switch (authResult.ResponseStatus)
{
case WebAuthenticationStatus.Success:
Console.WriteLine("OAuth flow succeeded.");
break;
case WebAuthenticationStatus.UserCancel:
Console.WriteLine("User cancelled the flow.");
break;
case WebAuthenticationStatus.ErrorHttp:
Console.WriteLine($"HTTP error: {authResult.ResponseErrorDetail}");
break;
default:
Console.WriteLine("Unexpected status.");
break;
}
```

> [!NOTE]
>
> - Ensure the fixed port and path (`/oauth/callback` in the examples) match exactly the redirect URI registered with the OAuth provider.
> - `WaitForCallbackAsync()` returns once the first matching request arrives; subsequent requests are ignored for result completion.
> - The handler sets a simple text response indicating success, cancellation, or error so users can close the browser tab.
> - When running tests or multiple local flows, prefer dynamic ports unless the provider requires a fixed one.
> [!IMPORTANT]
> Only one Server instance (per address family) can bind to a given fixed port. Attempting to start a second server on the same port will throw a System.Net.Sockets.SocketException (address already in use).
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace Yllibed.HttpServer.Handlers.Uno.Tests;

public class AuthCallbackHandlerOptionsTests
{

private static List<ValidationResult> Validate(object model)
{
var results = new List<ValidationResult>();
var context = new ValidationContext(model);
Validator.TryValidateObject(model, context, results, validateAllProperties: true);
return results;
}

[Fact]
public void Validation_Fails_When_CallbackUri_Is_Null()
{
// Arrange
var opts = new AuthCallbackHandlerOptions { CallbackUri = null };

// Act
var results = Validate(opts);

// Assert
results.ShouldNotBeEmpty();
results.ShouldContain(r => r.MemberNames.Contains(nameof(AuthCallbackHandlerOptions.CallbackUri)));
}

[Fact]
public void Validation_Fails_When_CallbackUri_Is_Invalid_Url()
{
// Arrange
var opts = new AuthCallbackHandlerOptions { CallbackUri = "not-a-url" };

// Act
var results = Validate(opts);

// Assert
results.ShouldNotBeEmpty();
results.ShouldContain(r => r.MemberNames.Contains(nameof(AuthCallbackHandlerOptions.CallbackUri)));
}

[Fact]
public void Validation_Passes_With_Valid_Url()
{
// Arrange
var opts = new AuthCallbackHandlerOptions { CallbackUri = "http://example.com/callback" };

// Act
var results = Validate(opts);

// Assert
results.ShouldBeEmpty();
}
}
6 changes: 6 additions & 0 deletions Yllibed.HttpServer.Handlers.Uno.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
global using System.ComponentModel.DataAnnotations;
global using System.Net;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Options;
global using Yllibed.HttpServer.Extensions;
global using Yllibed.HttpServer.Handlers.Uno.Extensions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
namespace Yllibed.HttpServer.Handlers.Uno.Tests;

public class OAuthCallbackExtensionsTests
{
[Fact]
public void AddOAuthCallbackHandler_RegistersHandlerAndInterface()
{
var services = new ServiceCollection();
var options = new AuthCallbackHandlerOptions { CallbackUri = "http://localhost/callback" };
services.AddSingleton(Options.Create(options));

services.AddOAuthCallbackHandler();

using var sp = services.BuildServiceProvider();
var concrete = sp.GetService<OAuthCallbackHandler>();
var asInterface = sp.GetService<IAuthCallbackHandler>();

concrete.ShouldNotBeNull();
asInterface.ShouldNotBeNull();
ReferenceEquals(concrete, asInterface).ShouldBeTrue();
concrete!.CallbackUri.ShouldBe(new Uri("http://localhost/callback"));
}

[Fact]
public async Task AddOAuthCallbackHandlerAndRegister_RegistersIntoServerPipeline()
{
var services = new ServiceCollection();
services.AddLogging();

var namedOptions = new AuthCallbackHandlerOptions { CallbackUri = "http://localhost/callback" };
services.AddSingleton(Options.Create(namedOptions));

services.AddYllibedHttpServer();
services.AddOAuthCallbackHandlerAndRegister();

await using var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<Server>();
var (uri4, _) = server.Start();
var callbackUri = new Uri(uri4, "/callback?code=abc");

var client = new HttpClient();
var response = await client.GetAsync(callbackUri, TestContext.Current.CancellationToken);
response.StatusCode.ShouldBe(HttpStatusCode.OK);

var handler = sp.GetRequiredService<IAuthCallbackHandler>();
var result = await handler.WaitForCallbackAsync();
result.ResponseErrorDetail.ShouldBe((uint)200);
result.ResponseData.ShouldNotBeNull();
result.ResponseData!.ShouldContain("code=abc");
}

[Fact]
public void AddOAuthCallbackHandlerAndRegister_WithConfigure_RegistersHandlerAndAppliesOptions()
{
var services = new ServiceCollection();

services.AddOAuthCallbackHandlerAndRegister(o => o.CallbackUri = "http://localhost/configured-callback");

using var sp = services.BuildServiceProvider();
var concrete = sp.GetService<OAuthCallbackHandler>();
var asInterface = sp.GetService<IAuthCallbackHandler>();

concrete.ShouldNotBeNull();
asInterface.ShouldNotBeNull();
ReferenceEquals(concrete, asInterface).ShouldBeTrue();
concrete!.CallbackUri.ShouldBe(new Uri("http://localhost/configured-callback"));
}

[Fact]
public async Task AddOAuthCallbackHandlerAndRegister_WithConfigure_RegistersIntoServerPipeline()
{
var services = new ServiceCollection();
services.AddLogging();

services.AddYllibedHttpServer();
services.AddOAuthCallbackHandlerAndRegister(o => o.CallbackUri = "http://localhost/configured-callback");

await using var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<Server>();
var (uri4, _) = server.Start();
var callbackUri = new Uri(uri4, "/configured-callback?code=xyz");

var client = new HttpClient();
var response = await client.GetAsync(callbackUri, TestContext.Current.CancellationToken);
response.StatusCode.ShouldBe(HttpStatusCode.OK);

var handler = sp.GetRequiredService<IAuthCallbackHandler>();
var result = await handler.WaitForCallbackAsync();
result.ResponseErrorDetail.ShouldBe((uint)200);
result.ResponseData.ShouldNotBeNull();
result.ResponseData!.ShouldContain("code=xyz");
}
}
Loading