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

# OTP / TOTP Akışı

> SignIn → challenge token → VerifyOtp / VerifyTotp → access + refresh token + oturum. ResendOtp cooldown ve limitleri.

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.

<Info>
  İlgili dosyalar:
  `Application/Features/Authentication/Website/Commands/{SignInUser,VerifyOtp,VerifyTotp,ResendOtp}/*` ·
  `Domain/AggregatesModel/UserAggregate/{User,UserOtpChallenge}.cs` ·
  `Application/Services/Identity/IUserAuthService.cs`
</Info>

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

```csharp theme={null}
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ş

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant U as Kullanıcı
    participant C as AuthController
    participant SI as SignInUserCommandHandler
    participant AG as User (aggregate)
    participant SMS as SendOtpSmsDomainEventHandler
    participant VO as VerifyOtpCommandHandler

    U->>C: POST /api/auth/sign-in { phone }
    C->>SI: _mediator.Send(SignInUserCommand)
    SI->>SI: UserByPhoneNumberSpecification ile kullanıcı bul
    alt TOTP etkin (IsTotpEnabled)
        SI->>SI: CreateTotpChallengeToken(user)
        SI-->>C: OtpChallengeDto { ChallengeToken, IsTotpEnabled = true }
    else OTP (SMS)
        SI->>AG: AddOtpChallenge(OtpCode.Generate(), Sms)
        Note over AG: UserOtpGeneratedDomainEvent
        SI->>SMS: (dispatch) SMS gönder
        SI->>SI: CreateOtpChallengeToken(user, Sms, challenge.Id)
        SI-->>C: OtpChallengeDto { ChallengeToken, IsTotpEnabled = false }
    end
    C-->>U: 200 + challengeToken

    Note over U,C: SPA kodu ister
    U->>C: POST /api/auth/verify-otp { code } + Bearer challengeToken
    C->>VO: _mediator.Send(VerifyOtpCommand)
    VO->>AG: VerifyChallenge(challengeId, code)
    VO->>AG: CreateAccessTokenAsync + CreateRefreshToken
    VO->>AG: StartSession(refreshTokenInfo, device, ip)
    VO-->>C: SignInResult(accessToken, refreshToken, user)
    C->>C: AppendRefreshToken (HttpOnly cookie)
    C-->>U: 200 + accessToken + user
```

## Adım adım

<Steps>
  <Step title="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:

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

  <Step title="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:

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

  <Step title="VerifyOtp — kod doğrula, oturum başlat">
    `VerifyOtpCommand` yalnızca `Code` ve `IsTrustedDevice` taşır; `userId`/`challengeId` challenge token'dan (`IOtpChallengeContext`) okunur:

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

  <Step title="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):

    ```csharp theme={null}
    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](/services/authenticator).
  </Step>

  <Step title="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:

    ```csharp theme={null}
    Response.AppendRefreshToken(result.Response.RefreshToken);
    return Ok(new VerifyOtpResponse(result.Response.AccessToken, result.Response.User));
    ```
  </Step>
</Steps>

## 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**:

```csharp theme={null}
var otpCode = OtpCode.Generate();
user.ResendOtp(otpCode, otpType);   // → challenge.Refresh(...)
await _unitOfWork.SaveEntitiesAsync(ct);
```

Asıl iş kuralları `UserOtpChallenge.Refresh` içindedir:

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

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

## OtpType referansı

| Üye             | Id | Açıklama                                                  |
| --------------- | -- | --------------------------------------------------------- |
| `Sms`           | 1  | Sunucu üretimi 4 haneli kod, NetGSM ile SMS               |
| `Email`         | 2  | Sunucu üretimi kod, e-posta ile                           |
| `Authenticator` | 3  | TOTP (OtpNet, RFC 6238) — authenticator uygulaması üretir |

## Hata senaryoları

| Durum                       | Nerede                           | Sonuç                                         |
| --------------------------- | -------------------------------- | --------------------------------------------- |
| Telefon kayıtlı değil       | `SignInUserCommandHandler`       | `ApplicationException` → `400`                |
| Kod yanlış / süresi dolmuş  | `User.VerifyChallenge` → handler | `ApplicationException` → `400`                |
| TOTP kodu geçersiz          | `VerifyTotpCommandHandler`       | `ApplicationException` → `400`                |
| TOTP kodu tekrar kullanıldı | `User.VerifyTotp`                | `DomainException` → `400`                     |
| Resend cooldown ihlali      | `UserOtpChallenge.Refresh`       | `DomainException` ("lütfen bekleyin") → `400` |
| 3 resend aşıldı             | `UserOtpChallenge.Refresh`       | `DomainException` → `400`                     |

## İlgili

<CardGroup cols={2}>
  <Card title="Vatandaş Kaydı" icon="user-plus" href="/playbooks/citizen-registration">
    SignUp → ilk OTP doğrulama akışı.
  </Card>

  <Card title="Authenticator (TOTP)" icon="mobile-screen" href="/services/authenticator">
    Secret üretimi, QR, doğrulama penceresi.
  </Card>

  <Card title="JWT BuildingBlock" icon="key" href="/building-blocks/jwt">
    Access / refresh / challenge token şemaları.
  </Card>

  <Card title="SMS Servisi" icon="comment-sms" href="/services/sms">
    NetGSM OTP gönderimi.
  </Card>
</CardGroup>
