Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 56 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MiniJwtOptions>(builder.Configuration.GetSection("MiniJwt"));
// The service depends on IOptions<MiniJwtOptions> and ILogger<MiniJwtService>
builder.Services.AddSingleton<IMiniJwtService, MiniJwtService>();
// 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<MiniJwtService>` 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

Expand Down Expand Up @@ -149,11 +153,35 @@ else

## Unit test examples

If you create a service instance manually in a test, provide an `ILogger<MiniJwtService>`. 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<IMiniJwtService>();
```

### 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
{
Expand All @@ -163,7 +191,14 @@ var options = Options.Create(new MiniJwtOptions
ExpirationMinutes = 60
});

var svc = new MiniJwtService(options, NullLogger<MiniJwtService>.Instance, new JwtSecurityTokenHandler());
var optionsMonitor = Options.CreateMonitor(options);
var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false };

var svc = new MiniJwtService(
optionsMonitor,
NullLogger<MiniJwtService>.Instance,
tokenHandler
);
```

### Testing with TimeProvider
Expand All @@ -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<MiniJwtService>.Instance,
new JwtSecurityTokenHandler(),
new JwtSecurityTokenHandler { MapInboundClaims = false },
fakeTimeProvider
);

Expand Down
44 changes: 32 additions & 12 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -121,18 +123,19 @@ builder.Services.AddMiniJwt(options =>
### 2. Using IConfiguration Binding

```csharp
builder.Services.Configure<MiniJwtOptions>(
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";
Expand Down Expand Up @@ -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");
Expand All @@ -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<string, IMiniJwtService> _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
{
Expand All @@ -209,11 +222,18 @@ public class TenantJwtService
ExpirationMinutes = 60
};

return new MiniJwtService(
Options.CreateMonitor(Options.Create(options)),
sp.GetRequiredService<ILogger<MiniJwtService>>(),
new JwtSecurityTokenHandler { MapInboundClaims = false }
);
var optionsMonitor = Options.CreateMonitor(Options.Create(options));
var logger = _serviceProvider.GetRequiredService<ILogger<MiniJwtService>>();
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)
Expand Down
70 changes: 67 additions & 3 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMiniJwtService>();

// 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;
Expand Down Expand Up @@ -373,13 +415,35 @@ app.UseMiddleware<JwtValidationMiddleware>();
### 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<IMiniJwtService>();
}

// Option 2: Manual instantiation
private IMiniJwtService CreateServiceManually()
{
var options = Options.Create(new MiniJwtOptions
{
Expand All @@ -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);
Expand All @@ -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);
Expand Down
42 changes: 40 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMiniJwtService>();

// 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;
Expand All @@ -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(
Expand Down
19 changes: 17 additions & 2 deletions src/MiniJwt.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,8 +22,22 @@ public static IServiceCollection AddMiniJwt(this IServiceCollection services, Ac
builder.ValidateOnStart();

services.AddLogging();
services.AddSingleton(new JwtSecurityTokenHandler { MapInboundClaims = false });
services.AddSingleton<IMiniJwtService, MiniJwtService>();

// Register IMiniJwtService with a factory to create a local JwtSecurityTokenHandler
// This avoids polluting the consumer's DI container
services.AddSingleton<IMiniJwtService>(sp =>
{
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<MiniJwtOptions>>();
var logger = sp.GetRequiredService<ILogger<MiniJwtService>>();

// Resolve TimeProvider (available in .NET 8+ DI) or fallback to System
var timeProvider = sp.GetService<TimeProvider>() ?? 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;
}
Expand Down
Loading