Skip to main content
Bookify uses Keycloak as its identity provider for JWT-based authentication. This guide walks you through setting up authentication, obtaining tokens, and protecting your API endpoints.

Overview

The authentication system consists of:
  • Keycloak: Identity and access management server
  • JWT Tokens: Bearer tokens for API authentication
  • ASP.NET Core JWT Bearer: Middleware for token validation
  • Authorization Policies: Role-based and permission-based access control

Keycloak Setup

Configuration

Keycloak settings are configured in appsettings.Development.json:
"Authentication": {
  "Audience": "account",
  "ValidIssuer": "http://bookify-idp:8080/realms/Bookify",
  "MetadataUrl": "http://bookify-idp:8080/realms/Bookify/.well-known/openid-configuration",
  "RequireHttpsMetadata": false
},
"Keycloak": {
  "BaseUrl": "http://bookify-idp:8080",
  "AdminUrl": "http://bookify-idp:8080/admin/realms/Bookify/",
  "TokenUrl": "http://bookify-idp:8080/realms/Bookify/protocol/openid-connect/token",
  "AdminClientId": "bookify-admin-client",
  "AdminClientSecret": "igu5nMmf4ucHnafsprKAyXq5uCpZztWs",
  "AuthClientId": "bookify-auth-client",
  "AuthClientSecret": "rZjRoaaamQmVtluhM8RN227GqtKzzZg5"
}
In production, store client secrets in environment variables or a secure secrets manager, not in appsettings files.

Docker Compose Configuration

Keycloak runs as a containerized service:
services:
  bookify-idp:
    image: quay.io/keycloak/keycloak:latest
    container_name: Bookify.Identity
    command: start-dev --import-realm
    environment:
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=admin
    volumes:
      - ./.containers/identity:/opt/keycloak/data
      - ./.files/bookify-realm-export.json:/opt/keycloak/data/import/realm.json
    ports:
      - 18080:8080
Access the Keycloak admin console at http://localhost:18080 with credentials admin/admin.

Authentication Flow

1
Register a new user
2
Send a POST request to /api/users/register:
3
curl -X POST http://localhost:5001/api/users/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe",
    "password": "SecurePassword123!"
  }'
4
This creates a user in both the Bookify database and Keycloak.
5
Obtain an access token
6
Login using the /api/users/login endpoint:
7
curl -X POST http://localhost:5001/api/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "SecurePassword123!"
  }'
8
Response:
9
{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
10
Make authenticated requests
11
Include the access token in the Authorization header:
12
curl http://localhost:5001/api/apartments \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

Implementation Details

JWT Service

The JwtService (src/Bookify.Infrastructure/Authentication/JwtService.cs:24) handles token acquisition:
public async Task<Result<string>> GetAccessTokenAsync(
    string email,
    string password,
    CancellationToken cancellationToken = default)
{
    var authRequestParameters = new KeyValuePair<string, string>[]
    {
        new("client_id", _keycloakOptions.AuthClientId),
        new("client_secret", _keycloakOptions.AuthClientSecret),
        new("scope", "openid email"),
        new("grant_type", "password"),
        new("username", email),
        new("password", password)
    };

    var authorizationRequestContent = new FormUrlEncodedContent(authRequestParameters);
    var response = await _httpClient.PostAsync("", authorizationRequestContent, cancellationToken);
    
    response.EnsureSuccessStatusCode();
    
    var authorizationToken = await response.Content.ReadFromJsonAsync<AuthorizationToken>(cancellationToken: cancellationToken);
    
    return authorizationToken.AccessToken;
}

User Registration

The AuthenticationService (src/Bookify.Infrastructure/Authentication/AuthenticationService.cs:19) registers users in Keycloak:
public async Task<string> RegisterAsync(
    User user,
    string password,
    CancellationToken cancellationToken = default)
{
    var userRepresentationModel = UserRepresentationModel.FromUser(user);

    userRepresentationModel.Credentials = new CredentialRepresentationModel[]
    {
        new()
        {
            Value = password,
            Temporary = false,
            Type = "password"
        }
    };

    var response = await _httpClient.PostAsJsonAsync(
        "users",
        userRepresentationModel,
        cancellationToken);

    return ExtractIdentityIdFromLocationHeader(response);
}

Authorization

Protecting Endpoints

By default, all endpoints require authentication. Use [AllowAnonymous] for public endpoints:
[AllowAnonymous]
[HttpPost("register")]
public async Task<IActionResult> Register(
    RegisterUserRequest request,
    CancellationToken cancellationToken)
{
    // Registration logic
}

Role-Based Authorization

Bookify includes role-based authorization. The Registered role is defined in src/Bookify.Api/Controllers/Roles.cs:4:
public static class Roles
{
    public const string Registered = "Registered";
}
Apply role requirements using the [Authorize] attribute:
[Authorize(Roles = Roles.Registered)]
[HttpPost]
public async Task<IActionResult> ReserveBooking(
    ReserveBookingRequest request,
    CancellationToken cancellationToken)
{
    // Booking logic
}

Accessing User Context

Inject IUserContext to access the authenticated user:
public class MyService
{
    private readonly IUserContext _userContext;

    public MyService(IUserContext userContext)
    {
        _userContext = userContext;
    }

    public void DoSomething()
    {
        var userId = _userContext.UserId;
        var email = _userContext.Email;
    }
}

Dependency Injection Setup

Authentication is configured in src/Bookify.Infrastructure/DependencyInjection.cs:84:
private static void AddAuthentication(IServiceCollection services, IConfiguration configuration)
{
    services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer();

    services.Configure<AuthenticationOptions>(configuration.GetSection("Authentication"));
    services.ConfigureOptions<JwtBearerOptionsSetup>();
    services.Configure<KeycloakOptions>(configuration.GetSection("Keycloak"));

    services.AddTransient<AdminAuthorizationDelegatingHandler>();

    services.AddHttpClient<IAuthenticationService, AuthenticationService>((serviceProvider, httpClient) =>
    {
        var keycloakOptions = serviceProvider.GetRequiredService<IOptions<KeycloakOptions>>().Value;
        httpClient.BaseAddress = new Uri(keycloakOptions.AdminUrl);
    })
    .AddHttpMessageHandler<AdminAuthorizationDelegatingHandler>();

    services.AddHttpClient<IJwtService, JwtService>((serviceProvider, httpClient) =>
    {
        var keycloakOptions = serviceProvider.GetRequiredService<IOptions<KeycloakOptions>>().Value;
        httpClient.BaseAddress = new Uri(keycloakOptions.TokenUrl);
    });

    services.AddHttpContextAccessor();
    services.AddScoped<IUserContext, UserContext>();
}

Troubleshooting

Token Validation Fails

Ensure the ValidIssuer matches your Keycloak realm configuration. Use the metadata URL to verify:
curl http://localhost:18080/realms/Bookify/.well-known/openid-configuration

401 Unauthorized

Check that:
  1. The token is not expired
  2. The Authorization header format is Bearer <token>
  3. The token was issued by the correct realm
  4. The API audience matches the configuration

User Registration Fails

Verify:
  1. Keycloak is running and accessible
  2. The admin client credentials are correct
  3. The user email is not already registered

Next Steps

Health Checks

Monitor Keycloak availability

API Reference

View authentication endpoints

Build docs developers (and LLMs) love