Skip to main content
Vatandaş tarafı kimlik doğrulama iki kademelidir: önce challenge (kim olduğunu telefonla/şifresiz bildir), sonra verify (OTP SMS kodu veya TOTP authenticator kodu). Verify başarılı olunca access token + refresh token üretilir ve bir oturum (UserSession) başlatılır. Bu sayfa SignInUser, VerifyOtp, VerifyTotp ve ResendOtp akışlarının tamamını gerçek koddan anlatır.
İlgili dosyalar: Application/Features/Authentication/Website/Commands/{SignInUser,VerifyOtp,VerifyTotp,ResendOtp}/* · Domain/AggregatesModel/UserAggregate/{User,UserOtpChallenge}.cs · Application/Services/Identity/IUserAuthService.cs

Challenge token şeması

Hem SignIn hem SignUp, doğrudan oturum açmaz; bir challenge token döner. Bu, OTPChallenge JWT şemasıyla imzalanır (OtpType.Sms/Email/Authenticator’a göre), kısa ömürlüdür ve userId + challengeId’yi taşır. SPA bunu saklayıp verify-* çağrılarında bearer olarak gönderir; handler tarafında IOtpChallengeContext token’dan bu değerleri okur.
public interface IUserAuthService
{
    Task<JwtAccessToken> CreateAccessTokenAsync(User user, Guid? tenantId = null, CancellationToken ct = default);
    JwtRefreshToken CreateRefreshToken(bool isTrustedDevice);
    string CreateOtpChallengeToken(User user, OtpType type, Guid? challengeId = null);
    string CreateTotpChallengeToken(User user);
    (Guid UserId, Guid? ChallengeId)? ValidateOtpToken(string token);
}

Sequence — telefon ile giriş

Adım adım

1

SignIn — kullanıcıyı bul, yöntemi seç

SignInUserCommandHandler telefonla kullanıcıyı bulur. TOTP etkinse OTP SMS göndermek yerine doğrudan TOTP challenge token döner:
User? user = await _userRepository.SingleOrDefaultAsync(new UserByPhoneNumberSpecification(phone), ct);
if (user is null) throw new ApplicationException("Bu telefon numarası ile kayıtlı kullanıcı bulunamadı");

if (user.IsTotpEnabled)
{
    var totpChallengeToken = _authService.CreateTotpChallengeToken(user);
    return ResponseWrapper<OtpChallengeDto>.Success(new OtpChallengeDto
    {
        ChallengeToken = totpChallengeToken,
        IsTotpEnabled  = user.IsTotpEnabled
    });
}

var otpCode = OtpCode.Generate();
var otpType = !string.IsNullOrEmpty(request.Phone) ? OtpType.Sms : OtpType.Email;
user?.AddOtpChallenge(otpCode, otpType);
await _unitOfWork.SaveEntitiesAsync(ct);
IsTotpEnabled, Totp is { IsEnabled: true } demektir. Dönen DTO’daki IsTotpEnabled flag’i SPA’ya hangi ekranı (SMS kodu mu, authenticator mı) göstereceğini bildirir.
2

OTP üretimi ve geçerlilik

User.AddOtpChallenge bir UserOtpChallenge oluşturur. Geçerlilik süresi tipe bağlıdır — SMS için 1 dk, e-posta için 2 dk:
var validityMinutes = type == OtpType.Sms ? 1 : 2;
var challenge = new UserOtpChallenge(this, GuidFactory.New(), code, type,
    DateTime.UtcNow.AddMinutes(validityMinutes));
OtpChallenges.Add(challenge);
AddDomainEvent(new UserOtpGeneratedDomainEvent(this, code, type, this.Phone, this.Email));
UserOtpGeneratedDomainEvent SaveChanges sonrası dispatch edilir; SendOtpSmsDomainEventHandler SMS gönderir (tip SMS değilse no-op).
3

VerifyOtp — kod doğrula, oturum başlat

VerifyOtpCommand yalnızca Code ve IsTrustedDevice taşır; userId/challengeId challenge token’dan (IOtpChallengeContext) okunur:
Guid userId      = _otpChallengeContext.UserId;
Guid challengeId = _otpChallengeContext.ChallengeId;

var user = await _userRepository.SingleOrDefaultAsync(
    new UserByOtpChallengeSpecification(userId, challengeId), ct);

if (!user.VerifyChallenge(challengeId, request.Code))
    throw new ApplicationException("Geçersiz veya süresi dolmuş kod");

var accessToken  = await _authService.CreateAccessTokenAsync(user, ct: ct);
var refreshToken = _authService.CreateRefreshToken(request.IsTrustedDevice);
// device + ip topla ...
user.StartSession(RefreshTokenInfo.Create(refreshToken.RefreshToken, refreshToken.ExpiresAt),
                  request.IsTrustedDevice, deviceInfo, clientInfo);
await _unitOfWork.SaveEntitiesAsync(ct);
User.VerifyChallenge kodu doğrularsa tipine göre IsPhoneNumberVerified / IsEmailVerified bayrağını set eder ve ilgili domain event’i (UserPhoneNumberVerifiedDomainEvent vb.) yayar. UserOtpChallenge.Verify her gerçek denemede AttemptCount’u artırır, süre dolduysa false döner.
4

VerifyTotp — authenticator kodu

TOTP etkinse kullanıcı authenticator uygulamasındaki 6 haneli kodu girer. VerifyTotpCommandHandler kodu IAuthenticationTotpService ile doğrular (OtpNet, RFC 6238, pencere ±1):
if (!_totpService.VerifyCode(user.Totp.Secret.EncryptedValue, request.Code, DateTime.UtcNow, out var timeStep))
    throw new ApplicationException("Geçersiz doğrulama kodu");

user.VerifyTotp(timeStep);   // replay koruması: aynı timeStep tekrar kullanılamaz
var accessToken  = await _authService.CreateAccessTokenAsync(user, ct: ct);
var refreshToken = _authService.CreateRefreshToken(request.IsTrustedDevice);
user.StartSession(...);
User.VerifyTotp(timeStep), Totp.CanAccept(timeStep) ile aynı zaman penceresinin tekrar kullanımını reddeder (“Kod tekrar kullanılamaz”) ve MarkUsed ile işaretler. TOTP yapılandırması için Authenticator Servisi.
5

Sonuç — token'lar ve cookie

Hem VerifyOtp hem VerifyTotp bir SignInResult(accessToken, refreshToken, user) döner. Controller refresh token’ı HttpOnly cookie’ye yazar (XSS koruması), gövdede yalnızca access token + kullanıcıyı döner:
Response.AppendRefreshToken(result.Response.RefreshToken);
return Ok(new VerifyOtpResponse(result.Response.AccessToken, result.Response.User));

ResendOtp — cooldown ve limitler

Kod ulaşmazsa SPA resend-otp çağırır (challenge token ile yetkilendirilir). ResendOtpCommand gövdesizdir; bağlam token’dan gelir. TOTP etkinse yine TOTP challenge döner; aksi halde mevcut challenge yenilenir:
var otpCode = OtpCode.Generate();
user.ResendOtp(otpCode, otpType);   // → challenge.Refresh(...)
await _unitOfWork.SaveEntitiesAsync(ct);
Asıl iş kuralları UserOtpChallenge.Refresh içindedir:
public void Refresh(OtpCode newCode, DateTime newExpiry)
{
    if (IsVerified)
        throw new DomainException("Onaylanmış bir oturum tekrar gönderilemez.");

    if (ResendCount >= 3)
        throw new DomainException("Maksimum SMS gönderim sınırına ulaştınız.");

    // Cooldown: son gönderimden bu yana en az 60 saniye geçmiş olmalı
    if (LastResentAt.HasValue && LastResentAt.Value.AddSeconds(60) > DateTime.UtcNow)
        throw new DomainException("Yeni bir kod istemeden önce lütfen bekleyin.");

    this.Code         = newCode;
    this.ExpiryDate   = newExpiry;
    this.ResendCount++;
    this.LastResentAt = DateTime.UtcNow;
    this.AttemptCount = 0;   // yeni kodla hatalı deneme sayacı sıfırlanır
}
Limitler challenge başına: en fazla 3 yeniden gönderim, gönderimler arası 60 sn cooldown. Yeni kod gelince AttemptCount sıfırlanır ama ResendCount birikir. Sınır aşılırsa kullanıcı yeni bir sign-in/sign-up ile yeni challenge başlatmalıdır.

OtpType referansı

ÜyeIdAçıklama
Sms1Sunucu üretimi 4 haneli kod, NetGSM ile SMS
Email2Sunucu üretimi kod, e-posta ile
Authenticator3TOTP (OtpNet, RFC 6238) — authenticator uygulaması üretir

Hata senaryoları

DurumNeredeSonuç
Telefon kayıtlı değilSignInUserCommandHandlerApplicationException400
Kod yanlış / süresi dolmuşUser.VerifyChallenge → handlerApplicationException400
TOTP kodu geçersizVerifyTotpCommandHandlerApplicationException400
TOTP kodu tekrar kullanıldıUser.VerifyTotpDomainException400
Resend cooldown ihlaliUserOtpChallenge.RefreshDomainException (“lütfen bekleyin”) → 400
3 resend aşıldıUserOtpChallenge.RefreshDomainException400

İlgili

Vatandaş Kaydı

SignUp → ilk OTP doğrulama akışı.

Authenticator (TOTP)

Secret üretimi, QR, doğrulama penceresi.

JWT BuildingBlock

Access / refresh / challenge token şemaları.

SMS Servisi

NetGSM OTP gönderimi.