Skip to main content
BuildingBlocks.Keycloak API’yi iki ayrı Keycloak realm’ından token kabul edecek şekilde yapılandırır: Vatandaş (citizen SPA) ve Personel (yönetim paneli). Tek AddKeycloak(...) çağrısı iki JwtBearer şeması kurar, claims transformation zincirini kaydeder ve ICurrentUserService ile aktif kullanıcı bağlamını sunar.
Geniş kapsamlı Keycloak konuları (realm yapısı, provisioning, ortamlar, sorun giderme) Keycloak grubundadır. Bu sayfa paketin kod tarafına odaklanır.

Yapılandırma — KeycloakOptions

appsettings.json’daki Keycloak bölümüyle bind edilir. İki alt bölüm vardır: Vatandas ve Personel, her ikisi de KeycloakSchemeOptions tipinde.
ÜyeTipAçıklama
VatandasKeycloakSchemeOptionsVatandaş SPA realm’ı (citizen)
PersonelKeycloakSchemeOptionsYönetim paneli realm’ı (staff)

KeycloakSchemeOptions

ÜyeTipAçıklama
BaseUrlstringAPI’nin Keycloak’a ulaşacağı URL. JWKS metadata ve Authority bunun üzerinden hesaplanır
PublicBaseUrlstring?Browser’ın gördüğü URL — token iss claim’inde bu olabilir. Boşsa BaseUrl kabul edilir
RealmstringKeycloak realm adı (örn. diyanet-vatandas-dev-realm)
ClientIdstringSPA’ın Keycloak client ID’si; audience (aud) doğrulaması bu değere göre yapılır
RequireHttpsMetadataboolHTTPS metadata zorunluluğu (prod’da true)
ValidateAudienceboolaud claim’i doğrulanacak mı
CookieNamestringAccess token’ın saklandığı HTTP-only cookie adı
ScopesList<string>OIDC scope’ları (varsayılan openid, profile, email)
Authoritystring (computed){BaseUrl}/realms/{Realm}
Issuerstring (computed)Authority ile aynı (internal issuer)
PublicIssuerstring (computed){PublicBaseUrl}/realms/{Realm} — boşsa Issuer
DiscoveryEndpointstring (computed){Authority}/.well-known/openid-configuration
TokenEndpointstring (computed){Authority}/protocol/openid-connect/token
JwksUristring (computed){Authority}/protocol/openid-connect/certs

KeycloakSchemeNames

[Authorize] attribute’larında kullanılan sabit şema adları:
SabitDeğerKullanım
Vatandas"VatandasScheme"Sadece vatandaş token’ları
Personel"PersonelScheme"Sadece personel token’ları
Any"VatandasScheme,PersonelScheme"Her iki realm
Default şema Personel’dir (AddAuthentication(KeycloakSchemeNames.Personel)). Şema belirtmeyen [Authorize] attribute’ları Personel realm’ını kullanır; vatandaş endpoint’leri açıkça KeycloakSchemeNames.Vatandas belirtmelidir.

Arayüzler

ArayüzİmzaAmaç
ICurrentUserServiceGuid? UserId, Guid? TenantId, Guid? BranchId, string? Email/Phone/FullName, IReadOnlyList<string> Roles/Permissions, bool IsAuthenticatedJWT claim’lerinden aktif kullanıcı bağlamı
bool HasPermission(string), bool HasRole(string), bool IsInTenant(Guid)İzin/rol/tenant kontrol kısayolları
IUserContextProviderTask<UserContextData> GetAsync(string sub, CancellationToken)sub üzerinden tenant + permission bağlamı (DB/cache)
Task InvalidateAsync(string sub, CancellationToken)Rol/permission değişiminde cache geçersizleştirme
ICurrentUserService HTTP context tabanlı CurrentUserService ile implement edilir; UserId önce user_id, yoksa Keycloak sub claim’ini okur. IUserContextProvider varsayılan olarak no-op NullUserContextProvider’dir — Application katmanı kendi implementasyonunu services.AddScoped<IUserContextProvider, UserContextProvider>() ile kaydeder.

DI kaydı — AddKeycloak

public static IServiceCollection AddKeycloak(
    this IServiceCollection services,
    IConfiguration          configuration)
{
    var opts = configuration.GetSection("Keycloak").Get<KeycloakOptions>()
        ?? throw new InvalidOperationException("'Keycloak' yapılandırma bölümü eksik.");

    services.Configure<KeycloakOptions>(configuration.GetSection("Keycloak"));

    // Current user + claims transformation zinciri
    services.AddHttpContextAccessor();
    services.AddScoped<ICurrentUserService, CurrentUserService>();
    services.AddScoped<KeycloakRoleClaimsTransformation>();   // realm/resource roller → ClaimTypes.Role
    services.AddScoped<UserContextClaimsTransformation>();    // tenant_id + permissions claim'leri
    services.AddScoped<IClaimsTransformation, CompositeClaimsTransformation>();
    services.TryAddScoped<IUserContextProvider, NullUserContextProvider>();

    // İki JwtBearer şeması — her biri kendi realm'ından doğrular (RS256, JWKS otomatik)
    services.AddAuthentication(KeycloakSchemeNames.Personel)
        .AddJwtBearer(KeycloakSchemeNames.Vatandas, o => ConfigureScheme(o, opts.Vatandas))
        .AddJwtBearer(KeycloakSchemeNames.Personel, o => ConfigureScheme(o, opts.Personel));

    services.AddAuthorization();

    // Her iki realm'ın OIDC discovery endpoint'i için health check (Degraded)
    services.AddHealthChecks()
        .AddUrlGroup(new Uri(opts.Vatandas.DiscoveryEndpoint), "keycloak-vatandas", HealthStatus.Degraded, ...)
        .AddUrlGroup(new Uri(opts.Personel.DiscoveryEndpoint), "keycloak-personel", HealthStatus.Degraded, ...);

    return services;
}
ConfigureScheme her şemada şunları yapar:
  • MapInboundClaims = falsesub claim’inin korunması kritik (aksi halde UserContextClaimsTransformation sub bulamaz, 403 üretir).
  • ValidIssuers = [Issuer, PublicIssuer] — Docker dev’de internal (http://keycloak:8080) ile public (http://localhost:8080) issuer farklı olabilir.
  • ValidAudiences = [ClientId, "account"].
  • NameClaimType = "preferred_username", ClockSkew = 30s.
  • OnMessageReceived token kaynak önceliği: 1) cookie (CookieName) → 2) Authorization: Bearer header → 3) query string access_token (yalnızca /hubs ve /files path’lerinde — SignalR/FileServer için).

Yapılandırma anahtarları

{
  "Keycloak": {
    "Vatandas": {
      "BaseUrl": "http://localhost:8080",
      "PublicBaseUrl": "http://localhost:8080",
      "Realm": "diyanet-vatandas-dev-realm",
      "ClientId": "diyanet-website",
      "RequireHttpsMetadata": false,
      "ValidateAudience": true,
      "CookieName": "kc_vatandas_token"
    },
    "Personel": {
      "BaseUrl": "http://localhost:8080",
      "PublicBaseUrl": "http://localhost:8080",
      "Realm": "diyanet-yonetim-dev-realm",
      "ClientId": "diyanet-admin",
      "RequireHttpsMetadata": false,
      "ValidateAudience": true,
      "CookieName": "kc_personel_token"
    }
  }
}

Kullanım — çift realm

// Sadece vatandaş
[Authorize(AuthenticationSchemes = KeycloakSchemeNames.Vatandas)]
[HttpGet("api/website/profile")]
public async Task<IActionResult> GetCitizenProfile()
{
    var userId = _currentUser.UserId;
    return Ok(await _mediator.Send(new GetCitizenProfileQuery(userId)));
}

// Sadece personel
[Authorize(AuthenticationSchemes = KeycloakSchemeNames.Personel)]
[HttpGet("api/admin/dashboard")]
public async Task<IActionResult> GetAdminDashboard()
    => Ok(await _mediator.Send(new GetAdminDashboardQuery()));

// Her iki realm
[Authorize(AuthenticationSchemes = KeycloakSchemeNames.Any)]
[HttpGet("api/website/notifications")]
public async Task<IActionResult> GetNotifications()
    => Ok(await _mediator.Send(new GetNotificationsQuery(_currentUser.UserId)));
MapInboundClaims = false ayarı atlanırsa JwtSecurityTokenHandler subClaimTypes.NameIdentifier dönüşümü yapar ve principal’da artık ham sub claim’i kalmaz. Bu durumda UserContextClaimsTransformation FindFirstValue("sub") ile boş döner → accountStatus çözülemez → her yerde 403. Paket bunu kasten kapatır.

İlgili

Keycloak (genel)

Realm yapısı, provisioning, ortamlar, sorun giderme.

Authorization

Permission policy provider ve RBAC akışı.

JWT

Backoffice + OTPChallenge custom JWT şemaları.

OAuth

Google/Meta/Keycloak PKCE akışı.