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

# Authentication

> Token edinimi (OIDC + PKCE), API JWT doğrulama, token kaynak önceliği, refresh rotation ve token version

Authentication iki adımdan oluşur: **token edinimi** (SPA ↔ Keycloak) ve **token doğrulama**
(API). Token üretimi tamamen Keycloak'a aittir; API simetrik anahtar tutmaz, JWKS üzerinden
RS256 imzayı doğrular.

## 1. Token edinimi — SPA → Keycloak (Authorization Code + PKCE)

Her iki SPA da (Website, Admin) public client'tır ve `keycloak-js` ile **Authorization Code +
PKCE (S256)** akışını kullanır. Client'lar `PublicClient: true`, `StandardFlowEnabled: true`,
`PkceRequired: true` olarak provision edilir; `DirectAccessGrantsEnabled: false`.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant U as Kullanıcı
    participant SPA as SPA (keycloak-js)
    participant KC as Keycloak Realm
    U->>SPA: /login
    SPA->>SPA: code_verifier üret<br/>code_challenge = SHA256(verifier)
    SPA->>KC: GET /protocol/openid-connect/auth<br/>response_type=code & code_challenge=... & code_challenge_method=S256
    KC->>U: Login formu (custom theme, kc_locale=tr)
    U->>KC: Kullanıcı adı + parola (+ MFA)
    KC-->>SPA: redirect_uri?code=<code>
    SPA->>KC: POST /protocol/openid-connect/token<br/>code + code_verifier
    KC-->>SPA: access_token + refresh_token + id_token
```

Backend-driven bir varyant da mevcuttur (`KeycloakOAuthClient`,
`src/DiyanetCleanArchitecture.Infrastructure.Services.OAuth/Keycloak/KeycloakOAuthClient.cs`):
PKCE `code_verifier`'ı Redis'te `keycloak_pkce_{state}` anahtarıyla 10 dakika saklar, callback'te
tek kullanımlık olarak okuyup siler.

## 2. API tarafında JWT doğrulama

Her scheme `BuildingBlocks.Keycloak/DependencyInjection.cs` → `ConfigureScheme` ile yapılandırılır.
Önemli noktalar:

```csharp theme={null}
options.MapInboundClaims = false;             // sub/name/role auto-rename KAPALI
options.Authority = scheme.Authority;         // OIDC discovery → JWKS otomatik
options.RequireHttpsMetadata = scheme.RequireHttpsMetadata;

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer           = true,
    ValidIssuers             = validIssuers,            // internal + public issuer
    ValidateAudience         = scheme.ValidateAudience,
    ValidAudiences           = [scheme.ClientId, "account"],
    ValidateLifetime         = true,
    ClockSkew                = TimeSpan.FromSeconds(30),
    NameClaimType            = "preferred_username",
    RoleClaimType            = ClaimTypes.Role,
    ValidateIssuerSigningKey = true
    // IssuerSigningKey ayarlanmaz — JWKS OIDC discovery ile otomatik alınır
};
```

<Warning>
  `MapInboundClaims = false` kritiktir. Açık kalırsa .NET, `sub` claim'ini uzun-form
  `ClaimTypes.NameIdentifier` URI'sine yeniden adlandırır; `UserContextClaimsTransformation`
  `FindFirstValue("sub")` ile boş döner → `account_status="Unknown"` → `ActiveAccountRequirement`
  fail → her yerde 403.
</Warning>

### Issuer: internal vs public

Docker dev'de API Keycloak'a iç ağ üzerinden (`http://diyanet-keycloak:8080`) ulaşır ama
tarayıcı `http://localhost:8080`'i görür. Token'ın `iss` claim'i public URL'i taşır. Bu yüzden
hem internal hem public issuer kabul edilir:

```csharp theme={null}
var validIssuers = scheme.Issuer == scheme.PublicIssuer
    ? new[] { scheme.Issuer }
    : new[] { scheme.Issuer, scheme.PublicIssuer };
```

`KeycloakSchemeOptions` (`BuildingBlocks.Keycloak/KeycloakOptions.cs`) URL'leri `BaseUrl`
(internal) ve `PublicBaseUrl` (browser) üzerinden hesaplar:

```text theme={null}
Authority        = {BaseUrl}/realms/{Realm}
Issuer           = Authority
PublicIssuer     = {PublicBaseUrl}/realms/{Realm}   (boşsa Issuer ile aynı)
JwksUri          = {Authority}/protocol/openid-connect/certs
```

Audience: client ID **ve** Keycloak'ın standart `account` audience'ı kabul edilir.

## 3. Token kaynak önceliği — `OnMessageReceived`

API token'ı üç kaynaktan, şu öncelikle okur:

```csharp theme={null}
OnMessageReceived = ctx =>
{
    // 1) HTTP-only cookie (kc_vatandas_token / kc_personel_token)
    if (string.IsNullOrEmpty(ctx.Token) &&
        ctx.Request.Cookies.TryGetValue(scheme.CookieName, out var cookieToken) &&
        !string.IsNullOrEmpty(cookieToken))
    {
        ctx.Token = cookieToken;
        return Task.CompletedTask;
    }

    // 2) Authorization: Bearer header — framework otomatik okur (Swagger, servis-arası)

    // 3) Query string access_token — yalnız /hubs ve /files (SignalR/FileServer)
    var accessToken = ctx.Request.Query["access_token"];
    var path = ctx.HttpContext.Request.Path;
    if (!string.IsNullOrEmpty(accessToken) && path.HasValue &&
        (path.StartsWithSegments("/hubs") || path.StartsWithSegments("/files")))
    {
        ctx.Token = accessToken;
    }
    return Task.CompletedTask;
};
```

Cookie adları scheme başına farklıdır: vatandaş `kc_vatandas_token`, personel `kc_personel_token`
(`appsettings.Development.json` → `Keycloak:*:CookieName`).

<Note>
  Query string `access_token` yalnızca `/hubs` ve `/files` yollarında kabul edilir — çünkü
  `EventSource` (SSE) ve dosya `<img>`/indirme istekleri özel header taşıyamaz. Diğer yollarda
  query token yok sayılır.
</Note>

### Hata teşhisi

`OnAuthenticationFailed` her başarısız doğrulamada response header'a teşhis bilgisi yazar
(`curl -v` / DevTools'tan görünür):

```text theme={null}
X-Auth-Fail-PersonelScheme: SecurityTokenExpiredException: ...
Token-Expired: true                 # token süresi dolduysa
```

`OnChallenge` ise default 401'i bastırıp RFC 7807 `application/problem+json` döner; `scheme`,
`detail` ve `traceId` alanlarıyla hangi scheme'in neden reddettiği görünür.

## 4. Vatandaş portali — OTP / TOTP (özet)

Vatandaş portalinde Keycloak SSO dışında telefon/e-posta OTP ve TOTP ile **challenge token**
tabanlı bir akış da vardır. Kullanıcı oluşturma → `OtpCode.Generate()` → domain event ile
SMS/e-posta gönderimi → doğrulama → oturum başlatma adımlarını izler. Challenge token kısa
ömürlüdür (`Jwt:OTPChallenge`, `Expires: 1` dk).

<Tip>
  Bu akışın uçtan uca anlatımı için bkz. [OTP doğrulama playbook'u](/playbooks/otp-flow).
</Tip>

## 5. Refresh token rotation

Refresh davranışı iki yerde tanımlıdır:

* **Keycloak client** (provisioning): `RefreshTokenRotationEnabled: true`. Vatandaş client'ı
  `RefreshTokenLifespanSeconds: 3600`, personel client'ı `1800`.
* **Uygulamanın kendi backoffice refresh token'ı** (`appsettings` → `Jwt:RefreshToken:Backoffice`):
  `RotationEnabled: true`, `ReuseDetectionEnabled: true`, `Expires: 43200` (30 gün),
  `TrustedDeviceExpires: 129600` (90 gün).

Rotation'da her kullanımda yeni refresh token verilir; eski token reuse edilirse
reuse-detection devreye girer. Domain tarafında `RefreshTokenInfo` value object'i token'ı
SHA256 hash'ler — plaintext saklanmaz.

## 6. Token version invalidasyonu

`TokenVersionMiddleware` (`SeedWork/Middlewares/TokenVersionMiddleware.cs`) bir feature flag'e
bağlıdır (`appsettings` → `Security:TokenVersionValidation`, dev'de `true`):

```csharp theme={null}
if (!_options.TokenVersionValidation ||
    context.User?.Identity?.IsAuthenticated != true)
{
    await _next(context);
    return;
}
// JWT token_version ile DB User.TokenVersion karşılaştır
if (user == null || user.TokenVersion != tokenVersion)
{
    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
    await context.Response.WriteAsync("Token geçersiz kılınmış.");
    return;
}
```

Middleware `app.UseAuthentication()` ile `app.UseAuthorization()` arasında çalışır
(`app.UseTokenVersionValidation()`). Rol/izin değişiminde `User.TokenVersion` artırılır; eski
token'lar bir sonraki istekte 401 alır. Bu, "izni geri al → anında etkili olsun" senaryosunu
token süresini beklemeden çözer.

## İlgili

<CardGroup cols={2}>
  <Card title="Authorization" href="/security/authorization">
    Permission policy ve handler'lar.
  </Card>

  <Card title="OTP Flow" href="/playbooks/otp-flow">
    Vatandaş OTP/TOTP challenge akışı.
  </Card>

  <Card title="Client Yapılandırması" href="/keycloak/client-configuration">
    PKCE, redirect URI, web origins.
  </Card>

  <Card title="Sorun Giderme" href="/keycloak/troubleshooting">
    Issuer mismatch, invalid audience, JWKS unreachable.
  </Card>
</CardGroup>
