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.
1. Add the authentication packages
Section titled “1. Add the authentication packages”dotnet add package Granit.Authentication.JwtBearerdotnet add package Granit.Authentication.KeycloakGranit.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.
2. Update the module dependencies
Section titled “2. Update the module dependencies”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...}3. Add configuration
Section titled “3. Add configuration”Add a JwtBearer section to appsettings.Development.json:
{ "JwtBearer": { "Authority": "http://localhost:8080/realms/task-management", "Audience": "task-management-api", "RequireHttpsMetadata": false }}{ "JwtBearer": { "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0", "Audience": "api://{client-id}", "RequireHttpsMetadata": true }}{ "JwtBearer": { "Authority": "https://cognito-idp.{region}.amazonaws.com/{userPoolId}", "Audience": "{clientId}", "RequireHttpsMetadata": true }, "Cognito": { "UserPoolId": "{userPoolId}", "ClientId": "{clientId}", "Region": "{region}" }}4. Add middleware
Section titled “4. Add middleware”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 endpointsapp.Run();Middleware order matters. UseAuthentication() must come before UseAuthorization(), and both must come after UseGranit().
5. Protect your endpoints
Section titled “5. Protect your endpoints”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 requirementAny request without a valid Authorization: Bearer <token> header now receives a 401 Unauthorized response.
6. What happens automatically
Section titled “6. What happens automatically”With a single [DependsOn] attribute, the framework wires up several behaviors behind the scenes:
- JWT validation —
GranitJwtBearerModulereads theJwtBearerconfiguration section and sets up token validation parameters (issuer, audience, signing keys). - Claims transformation —
KeycloakClaimsTransformationextracts roles from the Keycloak-specificrealm_accessandresource_accessJWT claims and maps them to standard .NET role claims. - Current user —
ICurrentUserServiceis populated from the JWT claims on every authenticated request. Inject it anywhere you need the caller’s identity. - Audit trail —
AuditedEntityInterceptorreadsICurrentUserServiceto fillCreatedByandModifiedByon every entity save. No extra code required.
7. Test with a token
Section titled “7. Test with a token”Obtain a token from your Keycloak instance (or any OIDC provider) and pass it in the Authorization header:
# 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 endpointcurl -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/tasksA valid token returns your task list. An expired or missing token returns 401 Unauthorized.
8. The audit trail now works
Section titled “8. The audit trail now works”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].
9. Observability bonus
Section titled “9. Observability bonus”Once authentication is in place, consider adding Granit.Observability to get structured logging and distributed tracing. The setup follows the same pattern:
dotnet add package Granit.ObservabilityAdd [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.
Next step
Section titled “Next step”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.