diff --git a/README.md b/README.md index e1454a7..8d14bd9 100644 --- a/README.md +++ b/README.md @@ -63,20 +63,24 @@ dotnet add package MiniJwt.Core ### 2) Register in DI (e.g. `Program.cs` for an ASP.NET Core app) ```csharp -using Microsoft.Extensions.DependencyInjection; -using MiniJwt.Core.Models; -using MiniJwt.Core.Services; +using MiniJwt.Core.Extensions; var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure(builder.Configuration.GetSection("MiniJwt")); -// The service depends on IOptions and ILogger -builder.Services.AddSingleton(); +// Register MiniJwt with configuration from appsettings.json +builder.Services.AddMiniJwt(options => +{ + var config = builder.Configuration.GetSection("MiniJwt"); + options.SecretKey = config["SecretKey"]; + options.Issuer = config["Issuer"]; + options.Audience = config["Audience"]; + options.ExpirationMinutes = double.Parse(config["ExpirationMinutes"] ?? "60"); +}); var app = builder.Build(); ``` -Note: `ILogger` is provided automatically by the framework DI. You can choose `AddSingleton`, `AddScoped` or `AddTransient` depending on your needs; the service is stateless after construction and computes the key bytes in the constructor, so `Singleton` is often suitable. +Note: The `AddMiniJwt` extension method registers `IMiniJwtService` as a singleton with all required dependencies, including a private `JwtSecurityTokenHandler` instance that won't conflict with other JWT libraries in your application. ### 3) Define a model with claims @@ -149,11 +153,35 @@ else ## Unit test examples -If you create a service instance manually in a test, provide an `ILogger`. Example using `NullLogger`: +If you create a service instance manually in a test, you have two options: + +### Option 1: Use AddMiniJwt (Recommended) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using MiniJwt.Core.Extensions; + +var services = new ServiceCollection(); +services.AddMiniJwt(options => +{ + options.SecretKey = "IntegrationTestSecretKey_LongEnough_For_HS256_0123456789"; + options.Issuer = "MiniJwt.Tests"; + options.Audience = "MiniJwt.Tests.Client"; + options.ExpirationMinutes = 60; +}); + +var serviceProvider = services.BuildServiceProvider(); +var svc = serviceProvider.GetRequiredService(); +``` + +### Option 2: Manual Instantiation + +For direct instantiation without DI, provide all dependencies explicitly: ```csharp using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using System.IdentityModel.Tokens.Jwt; var options = Options.Create(new MiniJwtOptions { @@ -163,7 +191,14 @@ var options = Options.Create(new MiniJwtOptions ExpirationMinutes = 60 }); -var svc = new MiniJwtService(options, NullLogger.Instance, new JwtSecurityTokenHandler()); +var optionsMonitor = Options.CreateMonitor(options); +var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + +var svc = new MiniJwtService( + optionsMonitor, + NullLogger.Instance, + tokenHandler +); ``` ### Testing with TimeProvider @@ -172,14 +207,24 @@ For testable time-dependent behavior, the library supports `TimeProvider` (built ```csharp using Microsoft.Extensions.Time.Testing; +using Microsoft.Extensions.Options; +using System.IdentityModel.Tokens.Jwt; var fakeTimeProvider = new FakeTimeProvider(); fakeTimeProvider.SetUtcNow(new DateTimeOffset(2024, 1, 15, 10, 0, 0, TimeSpan.Zero)); +var options = Options.Create(new MiniJwtOptions +{ + SecretKey = "IntegrationTestSecretKey_LongEnough_For_HS256_0123456789", + Issuer = "MiniJwt.Tests", + Audience = "MiniJwt.Tests.Client", + ExpirationMinutes = 60 +}); + var svc = new MiniJwtService( - options, + Options.CreateMonitor(options), NullLogger.Instance, - new JwtSecurityTokenHandler(), + new JwtSecurityTokenHandler { MapInboundClaims = false }, fakeTimeProvider ); diff --git a/docs/configuration.md b/docs/configuration.md index f472b3f..b70f4e0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -108,6 +108,8 @@ options.ExpirationMinutes = 0.5; // 30 seconds **Program.cs:** ```csharp +using MiniJwt.Core.Extensions; + builder.Services.AddMiniJwt(options => { var config = builder.Configuration.GetSection("MiniJwt"); @@ -121,18 +123,19 @@ builder.Services.AddMiniJwt(options => ### 2. Using IConfiguration Binding ```csharp -builder.Services.Configure( - builder.Configuration.GetSection("MiniJwt")); +using MiniJwt.Core.Extensions; -builder.Services.AddMiniJwt(options => +builder.Services.AddMiniJwt(options => { - // Options are bound from configuration automatically + builder.Configuration.GetSection("MiniJwt").Bind(options); }); ``` ### 3. Direct Configuration ```csharp +using MiniJwt.Core.Extensions; + builder.Services.AddMiniJwt(options => { options.SecretKey = "my-secret-key-at-least-32-bytes-long"; @@ -170,6 +173,8 @@ builder.Services.AddMiniJwt(options => **Program.cs (loading secret from environment):** ```csharp +using MiniJwt.Core.Extensions; + builder.Services.AddMiniJwt(options => { var config = builder.Configuration.GetSection("MiniJwt"); @@ -188,18 +193,26 @@ builder.Services.AddMiniJwt(options => For applications serving multiple tenants with different JWT configurations: ```csharp +using MiniJwt.Core.Models; +using MiniJwt.Core.Services; +using Microsoft.Extensions.Options; +using System.IdentityModel.Tokens.Jwt; + public class TenantJwtService { private readonly Dictionary _services = new(); + private readonly IServiceProvider _serviceProvider; public TenantJwtService(IServiceProvider serviceProvider) { + _serviceProvider = serviceProvider; + // Register services for each tenant - _services["tenant1"] = CreateServiceForTenant("tenant1", serviceProvider); - _services["tenant2"] = CreateServiceForTenant("tenant2", serviceProvider); + _services["tenant1"] = CreateServiceForTenant("tenant1"); + _services["tenant2"] = CreateServiceForTenant("tenant2"); } - private IMiniJwtService CreateServiceForTenant(string tenantId, IServiceProvider sp) + private IMiniJwtService CreateServiceForTenant(string tenantId) { var options = new MiniJwtOptions { @@ -209,11 +222,18 @@ public class TenantJwtService ExpirationMinutes = 60 }; - return new MiniJwtService( - Options.CreateMonitor(Options.Create(options)), - sp.GetRequiredService>(), - new JwtSecurityTokenHandler { MapInboundClaims = false } - ); + var optionsMonitor = Options.CreateMonitor(Options.Create(options)); + var logger = _serviceProvider.GetRequiredService>(); + var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + + return new MiniJwtService(optionsMonitor, logger, tokenHandler); + } + + private string GetSecretForTenant(string tenantId) + { + // Implement your logic to retrieve tenant-specific secrets + // This could be from a database, configuration, or secrets manager + return $"secret-for-{tenantId}-at-least-32-bytes-long"; } public IMiniJwtService GetServiceForTenant(string tenantId) diff --git a/docs/examples.md b/docs/examples.md index 663d9a6..952d308 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -200,6 +200,48 @@ public record LoginRequest(string Username, string Password); See [samples/ConsoleMinimal](../samples/ConsoleMinimal/) for a complete runnable example. +### Using Dependency Injection (Recommended) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MiniJwt.Core.Extensions; +using MiniJwt.Core.Services; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + services.AddMiniJwt(options => + { + options.SecretKey = "super-secret-key-at-least-32-bytes-long-hs256"; + options.Issuer = "ConsoleApp"; + options.Audience = "ConsoleClient"; + options.ExpirationMinutes = 1; + }); + }) + .Build(); + +var jwtService = host.Services.GetRequiredService(); + +// Generate token +var token = jwtService.GenerateToken(new { sub = "user1", role = "admin" }); +Console.WriteLine($"Token: {token}"); + +// Validate immediately +var principal = jwtService.ValidateToken(token); +Console.WriteLine($"Valid: {principal?.Identity?.Name ?? "null"}"); + +// Wait for expiration +Console.WriteLine("Waiting for token to expire..."); +await Task.Delay(TimeSpan.FromSeconds(65)); + +// Validate expired token +var expiredPrincipal = jwtService.ValidateToken(token); +Console.WriteLine($"After expiration: {(expiredPrincipal == null ? "Invalid" : "Valid")}"); +``` + +### Manual Instantiation (Without DI) + ```csharp using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -373,13 +415,35 @@ app.UseMiddleware(); ### Unit Testing ```csharp +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using MiniJwt.Core.Extensions; +using MiniJwt.Core.Models; +using MiniJwt.Core.Services; +using System.IdentityModel.Tokens.Jwt; using Xunit; public class JwtServiceTests { - private IMiniJwtService CreateService() + // Option 1: Using AddMiniJwt (Recommended) + private IMiniJwtService CreateServiceWithDI() + { + var services = new ServiceCollection(); + services.AddMiniJwt(options => + { + options.SecretKey = "test-secret-key-at-least-32-bytes-long"; + options.Issuer = "TestApp"; + options.Audience = "TestClient"; + options.ExpirationMinutes = 60; + }); + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider.GetRequiredService(); + } + + // Option 2: Manual instantiation + private IMiniJwtService CreateServiceManually() { var options = Options.Create(new MiniJwtOptions { @@ -399,7 +463,7 @@ public class JwtServiceTests [Fact] public void GenerateToken_ValidPayload_ReturnsToken() { - var service = CreateService(); + var service = CreateServiceWithDI(); var token = service.GenerateToken(new { sub = "test" }); Assert.NotNull(token); @@ -409,7 +473,7 @@ public class JwtServiceTests [Fact] public void ValidateToken_ValidToken_ReturnsPrincipal() { - var service = CreateService(); + var service = CreateServiceWithDI(); var token = service.GenerateToken(new { sub = "test123" }); var principal = service.ValidateToken(token); diff --git a/docs/getting-started.md b/docs/getting-started.md index 7d4c33e..8fbb822 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -161,7 +161,45 @@ return Ok(user); ## Console Application Example -For a minimal console application without dependency injection: +For a minimal console application: + +### Using Dependency Injection (Recommended) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MiniJwt.Core.Extensions; +using MiniJwt.Core.Services; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + services.AddMiniJwt(options => + { + options.SecretKey = "my-super-secret-key-at-least-32-bytes-long-hs256"; + options.Issuer = "MyConsoleApp"; + options.Audience = "MyConsoleClient"; + options.ExpirationMinutes = 60; + }); + }) + .Build(); + +var jwtService = host.Services.GetRequiredService(); + +// Generate token +var payload = new { sub = "user1", role = "admin" }; +var token = jwtService.GenerateToken(payload); +Console.WriteLine($"Generated Token: {token}"); + +// Validate token +var principal = jwtService.ValidateToken(token); +if (principal != null) +{ + Console.WriteLine($"Token is valid! Subject: {principal.FindFirst("sub")?.Value}"); +} +``` + +### Manual Instantiation (Without DI) ```csharp using Microsoft.Extensions.Logging.Abstractions; @@ -179,7 +217,7 @@ var options = new MiniJwtOptions }; // Create IOptionsMonitor for console usage -var optionsMonitor = Microsoft.Extensions.Options.Options.CreateMonitor(options); +var optionsMonitor = Options.CreateMonitor(Options.Create(options)); var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false }; var jwtService = new MiniJwtService( diff --git a/src/MiniJwt.Core/Extensions/ServiceCollectionExtensions.cs b/src/MiniJwt.Core/Extensions/ServiceCollectionExtensions.cs index 65ef27b..a612f5e 100644 --- a/src/MiniJwt.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/MiniJwt.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using System.IdentityModel.Tokens.Jwt; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MiniJwt.Core.Models; using MiniJwt.Core.Services; @@ -21,8 +22,22 @@ public static IServiceCollection AddMiniJwt(this IServiceCollection services, Ac builder.ValidateOnStart(); services.AddLogging(); - services.AddSingleton(new JwtSecurityTokenHandler { MapInboundClaims = false }); - services.AddSingleton(); + + // Register IMiniJwtService with a factory to create a local JwtSecurityTokenHandler + // This avoids polluting the consumer's DI container + services.AddSingleton(sp => + { + var optionsMonitor = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); + + // Resolve TimeProvider (available in .NET 8+ DI) or fallback to System + var timeProvider = sp.GetService() ?? TimeProvider.System; + + // Create a local instance specifically for MiniJwt to avoid global side effects + var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + + return new MiniJwtService(optionsMonitor, logger, tokenHandler, timeProvider); + }); return services; } diff --git a/src/MiniJwt.Tests/MiniJwtTests.Extension.cs b/src/MiniJwt.Tests/MiniJwtTests.Extension.cs index a941243..2a14525 100644 --- a/src/MiniJwt.Tests/MiniJwtTests.Extension.cs +++ b/src/MiniJwt.Tests/MiniJwtTests.Extension.cs @@ -141,4 +141,26 @@ public void ServiceCollectionExtensions_AddMiniJwt_LoggingServiceRegistered() // Ensure that the logger is functional logger.LogInformation("Logger is working in MiniJwtService test."); } + + [Fact] + public void ServiceCollectionExtensions_AddMiniJwt_DoesNotRegisterJwtSecurityTokenHandlerGlobally() + { + // Arrange + var services = new ServiceCollection(); + services.AddMiniJwt(BasicOptions); + + // Act + var serviceProvider = services.BuildServiceProvider(); + + // Assert - JwtSecurityTokenHandler should NOT be resolvable from the DI container + var handler = serviceProvider.GetService(); + Assert.Null(handler); + + // But IMiniJwtService should be available and functional + var miniJwtService = serviceProvider.GetService(); + Assert.NotNull(miniJwtService); + + var token = miniJwtService.GenerateToken(new { }); + Assert.NotNull(token); + } } \ No newline at end of file