> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# BuildingBlocks.Keycloak

> Çift-realm Keycloak SSO — iki JwtBearer şeması, claims transformation, ICurrentUserService.

`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.

<Info>
  Geniş kapsamlı Keycloak konuları (realm yapısı, provisioning, ortamlar, sorun giderme) [Keycloak](/keycloak/overview) grubundadır. Bu sayfa paketin **kod tarafına** odaklanır.
</Info>

## 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.

| Üye        | Tip                     | Açıklama                       |
| ---------- | ----------------------- | ------------------------------ |
| `Vatandas` | `KeycloakSchemeOptions` | Vatandaş SPA realm'ı (citizen) |
| `Personel` | `KeycloakSchemeOptions` | Yönetim paneli realm'ı (staff) |

### `KeycloakSchemeOptions`

| Üye                    | Tip                 | Açıklama                                                                                  |
| ---------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
| `BaseUrl`              | `string`            | API'nin Keycloak'a ulaşacağı URL. JWKS metadata ve `Authority` bunun üzerinden hesaplanır |
| `PublicBaseUrl`        | `string?`           | Browser'ın gördüğü URL — token `iss` claim'inde bu olabilir. Boşsa `BaseUrl` kabul edilir |
| `Realm`                | `string`            | Keycloak realm adı (örn. `diyanet-vatandas-dev-realm`)                                    |
| `ClientId`             | `string`            | SPA'ın Keycloak client ID'si; audience (`aud`) doğrulaması bu değere göre yapılır         |
| `RequireHttpsMetadata` | `bool`              | HTTPS metadata zorunluluğu (prod'da `true`)                                               |
| `ValidateAudience`     | `bool`              | `aud` claim'i doğrulanacak mı                                                             |
| `CookieName`           | `string`            | Access token'ın saklandığı HTTP-only cookie adı                                           |
| `Scopes`               | `List<string>`      | OIDC scope'ları (varsayılan `openid, profile, email`)                                     |
| `Authority`            | `string` (computed) | `{BaseUrl}/realms/{Realm}`                                                                |
| `Issuer`               | `string` (computed) | `Authority` ile aynı (internal issuer)                                                    |
| `PublicIssuer`         | `string` (computed) | `{PublicBaseUrl}/realms/{Realm}` — boşsa `Issuer`                                         |
| `DiscoveryEndpoint`    | `string` (computed) | `{Authority}/.well-known/openid-configuration`                                            |
| `TokenEndpoint`        | `string` (computed) | `{Authority}/protocol/openid-connect/token`                                               |
| `JwksUri`              | `string` (computed) | `{Authority}/protocol/openid-connect/certs`                                               |

### `KeycloakSchemeNames`

`[Authorize]` attribute'larında kullanılan sabit şema adları:

| Sabit      | Değer                             | Kullanım                   |
| ---------- | --------------------------------- | -------------------------- |
| `Vatandas` | `"VatandasScheme"`                | Sadece vatandaş token'ları |
| `Personel` | `"PersonelScheme"`                | Sadece personel token'ları |
| `Any`      | `"VatandasScheme,PersonelScheme"` | Her iki realm              |

<Note>
  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.
</Note>

## Arayüzler

| Arayüz                 | İmza                                                                                                                                                  | Amaç                                                   |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `ICurrentUserService`  | `Guid? UserId`, `Guid? TenantId`, `Guid? BranchId`, `string? Email/Phone/FullName`, `IReadOnlyList<string> Roles/Permissions`, `bool IsAuthenticated` | JWT claim'lerinden aktif kullanıcı bağlamı             |
|                        | `bool HasPermission(string)`, `bool HasRole(string)`, `bool IsInTenant(Guid)`                                                                         | İzin/rol/tenant kontrol kısayolları                    |
| `IUserContextProvider` | `Task<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`

```csharp theme={null}
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 = false` — `sub` 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ı

```json theme={null}
{
  "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

```csharp theme={null}
// 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)));
```

<Warning>
  `MapInboundClaims = false` ayarı atlanırsa `JwtSecurityTokenHandler` `sub` → `ClaimTypes.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.
</Warning>

## İlgili

<CardGroup cols={2}>
  <Card title="Keycloak (genel)" icon="key" href="/keycloak/overview">
    Realm yapısı, provisioning, ortamlar, sorun giderme.
  </Card>

  <Card title="Authorization" icon="user-shield" href="/security/authorization">
    Permission policy provider ve RBAC akışı.
  </Card>

  <Card title="JWT" icon="ticket" href="/building-blocks/jwt">
    Backoffice + OTPChallenge custom JWT şemaları.
  </Card>

  <Card title="OAuth" icon="right-to-bracket" href="/building-blocks/oauth">
    Google/Meta/Keycloak PKCE akışı.
  </Card>
</CardGroup>
