Skip to main content
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. 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.csConfigureScheme ile yapılandırılır. Önemli noktalar:
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
};
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.

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:
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:
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:
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.jsonKeycloak:*:CookieName).
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.

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):
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).
Bu akışın uçtan uca anlatımı için bkz. OTP doğrulama playbook’u.

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’ı (appsettingsJwt: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 (appsettingsSecurity:TokenVersionValidation, dev’de true):
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

Authorization

Permission policy ve handler’lar.

OTP Flow

Vatandaş OTP/TOTP challenge akışı.

Client Yapılandırması

PKCE, redirect URI, web origins.

Sorun Giderme

Issuer mismatch, invalid audience, JWKS unreachable.