Skip to content

Adding Authentication

In the previous step, you added persistence with EF Core and automatic audit trails. The audit fields (CreatedBy, ModifiedBy) are still empty because the API does not know who is calling it. In this step, you will add JWT Bearer authentication with Keycloak claims transformation so that every request carries a verified identity.

Terminal window
dotnet add package Granit.Authentication.JwtBearer
dotnet add package Granit.Authentication.Keycloak

Granit.Authentication.JwtBearer configures JWT Bearer validation from your appsettings.json. Granit.Authentication.Keycloak adds a claims transformation that extracts Keycloak roles from realm_access and resource_access into standard .NET claims.

Open TaskManagementModule.cs and add the Keycloak authentication dependency:

using Granit.Core.Modularity;
using Granit.Timing;
using Granit.Persistence;
using Granit.Authentication.Keycloak;
namespace TaskManagement.Api;
[DependsOn(typeof(GranitTimingModule))]
[DependsOn(typeof(GranitPersistenceModule))]
[DependsOn(typeof(GranitAuthenticationKeycloakModule))]
public sealed class TaskManagementModule : GranitModule
{
// ConfigureServices stays the same...
}

Add a JwtBearer section to appsettings.Development.json:

{
"JwtBearer": {
"Authority": "http://localhost:8080/realms/task-management",
"Audience": "task-management-api",
"RequireHttpsMetadata": false
}
}

Open Program.cs and add the authentication and authorization middleware between UseGranit() and your route mappings:

var app = builder.Build();
app.UseGranit();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", (IClock clock) => new
{
Message = "Task Management API",
CurrentTime = clock.Now
});
// ... your other endpoints
app.Run();

Middleware order matters. UseAuthentication() must come before UseAuthorization(), and both must come after UseGranit().

Add .RequireAuthorization() to your route group so that all task endpoints require a valid JWT:

var tasks = app.MapGroup("/api/tasks")
.RequireAuthorization();
tasks.MapGet("/", async (TaskManagementDbContext db) =>
{
var items = await db.Tasks.ToListAsync();
return Results.Ok(items);
});
// ... other endpoints in the group inherit the authorization requirement

Any request without a valid Authorization: Bearer <token> header now receives a 401 Unauthorized response.

With a single [DependsOn] attribute, the framework wires up several behaviors behind the scenes:

  • JWT validationGranitJwtBearerModule reads the JwtBearer configuration section and sets up token validation parameters (issuer, audience, signing keys).
  • Claims transformationKeycloakClaimsTransformation extracts roles from the Keycloak-specific realm_access and resource_access JWT claims and maps them to standard .NET role claims.
  • Current userICurrentUserService is populated from the JWT claims on every authenticated request. Inject it anywhere you need the caller’s identity.
  • Audit trailAuditedEntityInterceptor reads ICurrentUserService to fill CreatedBy and ModifiedBy on every entity save. No extra code required.

Obtain a token from your Keycloak instance (or any OIDC provider) and pass it in the Authorization header:

Terminal window
# Get a token from Keycloak using the password grant (development only)
TOKEN=$(curl -s -X POST \
"http://localhost:8080/realms/task-management/protocol/openid-connect/token" \
-d "client_id=task-management-api" \
-d "username=testuser" \
-d "password=testpassword" \
-d "grant_type=password" | jq -r '.access_token')
# Call the protected endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/tasks

A valid token returns your task list. An expired or missing token returns 401 Unauthorized.

Create a task with a valid token and inspect the database row. The CreatedBy and ModifiedBy columns now contain the authenticated user’s ID extracted from the JWT sub claim. Every task created or modified records who did it — zero extra code, just a [DependsOn].

Once authentication is in place, consider adding Granit.Observability to get structured logging and distributed tracing. The setup follows the same pattern:

Terminal window
dotnet add package Granit.Observability

Add [DependsOn(typeof(GranitObservabilityModule))] to your module and configure the OTLP endpoint in appsettings.json. Authentication events, database queries, and HTTP requests are traced automatically.

See the Observability reference for the full configuration guide.

You now have a modular API with persistence, audit trails, and authentication. Instead of building this from scratch each time, Granit provides project templates that scaffold the full stack in one command.

Project Templates