Ana içeriğe geç

OpenID Connect ve Token Yönetimi

OpenID Connect, kimlik doğrulama için standart bir protokoldür; yanlış token yönetimi güvenlik açıklarına ve kötü kullanıcı deneyimine yol açar.


1. Token’ı LocalStorage’da Saklamak

Yanlış Kullanım: JWT token’ı tarayıcı localStorage’ında tutmak.

// Frontend - XSS saldırısına açık
localStorage.setItem("access_token", response.token);
fetch("/api/orders", {
    headers: { "Authorization": `Bearer ${localStorage.getItem("access_token")}` }
});

İdeal Kullanım: HttpOnly cookie ile token yönetimi yapın.

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.ExpireTimeSpan = TimeSpan.FromHours(1);
    options.SlidingExpiration = true;
})
.AddOpenIdConnect(options =>
{
    options.Authority = "https://auth.example.com";
    options.ClientId = "web-app";
    options.ClientSecret = builder.Configuration["Auth:ClientSecret"];
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("api");
});

2. Refresh Token Yönetimi Yapmamak

Yanlış Kullanım: Access token süresi dolduğunda kullanıcıyı tekrar login’e yönlendirmek.

app.MapGet("/api/data", async (HttpContext context) =>
{
    var token = await context.GetTokenAsync("access_token");
    // Token expire olduysa 401 döner, kullanıcı tekrar giriş yapmak zorunda
    _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    return await _client.GetFromJsonAsync<Data>("/data");
});

İdeal Kullanım: Otomatik token yenileme mekanizması kurun.

public class TokenRefreshHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITokenService _tokenService;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        var context = _httpContextAccessor.HttpContext!;
        var accessToken = await context.GetTokenAsync("access_token");
        var expiresAt = await context.GetTokenAsync("expires_at");

        if (DateTimeOffset.Parse(expiresAt!) < DateTimeOffset.UtcNow.AddMinutes(1))
        {
            var refreshToken = await context.GetTokenAsync("refresh_token");
            var newTokens = await _tokenService.RefreshAsync(refreshToken!);

            var authInfo = await context.AuthenticateAsync();
            authInfo.Properties!.UpdateTokenValue("access_token", newTokens.AccessToken);
            authInfo.Properties!.UpdateTokenValue("refresh_token", newTokens.RefreshToken);
            authInfo.Properties!.UpdateTokenValue("expires_at",
                newTokens.ExpiresAt.ToString("o"));

            await context.SignInAsync(authInfo.Principal!, authInfo.Properties);
            accessToken = newTokens.AccessToken;
        }

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        return await base.SendAsync(request, ct);
    }
}

3. Claims Mapping Yapmamak

Yanlış Kullanım: Claim’leri doğrudan token’dan okumaya çalışmak.

app.MapGet("/api/profile", (ClaimsPrincipal user) =>
{
    var name = user.FindFirst("name")?.Value;           // null olabilir
    var email = user.FindFirst("email")?.Value;          // Farklı provider'larda farklı claim adı
    var role = user.FindFirst("role")?.Value;             // Mapping olmadan boş
    return new { name, email, role };
});

İdeal Kullanım: Claims transformation ile standart claim mapping yapın.

public class CustomClaimsTransformation : IClaimsTransformation
{
    private readonly IUserService _userService;

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var identity = (ClaimsIdentity)principal.Identity!;
        var externalId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (externalId is null) return principal;

        var user = await _userService.GetByExternalIdAsync(externalId);
        if (user is null) return principal;

        identity.AddClaim(new Claim("app_user_id", user.Id.ToString()));
        identity.AddClaim(new Claim(ClaimTypes.Role, user.Role));

        foreach (var permission in user.Permissions)
            identity.AddClaim(new Claim("permission", permission));

        return principal;
    }
}

builder.Services.AddTransient<IClaimsTransformation, CustomClaimsTransformation>();

4. Tek Bir Authentication Scheme Kullanmak

Yanlış Kullanım: Sadece bir kimlik doğrulama yöntemi desteklemek.

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://auth.example.com";
    });
// Sadece JWT, API key veya farklı provider desteği yok

İdeal Kullanım: Birden fazla scheme ile esnek kimlik doğrulama yapın.

builder.Services.AddAuthentication()
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://auth.example.com";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidAudience = "api"
        };
    })
    .AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>("ApiKey", null);

// Policy-based scheme seçimi
builder.Services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes("Bearer", "ApiKey")
        .Build();

    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin")
              .AddAuthenticationSchemes("Bearer"));
});

5. Token Validation’ı Eksik Yapmak

Yanlış Kullanım: Token doğrulama parametrelerini atlamak.

builder.Services.AddAuthentication().AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,      // Herhangi bir issuer kabul edilir
        ValidateAudience = false,    // Herhangi bir audience kabul edilir
        ValidateLifetime = false,    // Süresi dolmuş token kabul edilir
        ValidateIssuerSigningKey = false
    };
});

İdeal Kullanım: Tüm token doğrulama parametrelerini aktif edin.

builder.Services.AddAuthentication().AddJwtBearer(options =>
{
    options.Authority = "https://auth.example.com";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = "https://auth.example.com",
        ValidateAudience = true,
        ValidAudience = "api",
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromMinutes(1),
        ValidateIssuerSigningKey = true,
        RequireExpirationTime = true,
        RequireSignedTokens = true
    };

    options.Events = new JwtBearerEvents
    {
        OnTokenValidated = async context =>
        {
            var userService = context.HttpContext.RequestServices
                .GetRequiredService<IUserService>();
            var userId = context.Principal!.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            if (userId is null || !await userService.IsActiveAsync(userId))
                context.Fail("Kullanıcı aktif değil");
        }
    };
});