# Kimlik Doğrulama Modülü Yazma

https://dev.wisecp.com/tr/kimlik-dogrulama-modulu-yazma

Kimlik Doğrulama modülü bir ikinci faktör yöntemidir. Giriş çekirdeğinin kullanıcıyı kaydetmek, soru sormak ve cevabı doğrulamak için çağırdığı metotları uygular.

## Genel Bakış

Ürünle üç modül gelir: Email, Sms ve Totp. Taban sınıf yoktur. Çekirdek yöntem adından bir sınıf çözer ve her metodu `method_exists` ile yoklar.

İki aile vardır. Kayıt, kullanıcıya gösterilen bir sır üretiyor mu? Evet ise `setup()` uygularsınız. Hayır ise kodu iletmek için `verification()` uygularsınız.

Yönetici girişi ile müşteri girişi aynı çekirdekten geçer, yani tek modül ikisine de hizmet eder.

## Ön Koşullar

- `coremio/modules/Authentication/` altına yazma erişimi.
- Bir iletim kanalı ya da paylaşılan sır şeması.
- Kod yöntemi için çalışan bir mail ya da SMS sürücüsü.
- [Bildirim Şablonları Nasıl Çalışır](https://dev.wisecp.com/tr/bildirim-sablonlari-nasil-calisir).

## Yapı

Üç dosya yeter. Ayar sayfası ve controller yoktur: operatör yalnızca yöntemi açar.

```bash
coremio/modules/Authentication/Acme/
├── Acme.php        namespace WISECP\modules\Authentication; class Acme
├── config.php      künye + ayarlar (setup, method, attempts, attempts_penalty_minute)
└── lang/
    ├── en.php      name + description, yöntem kartında görünür
    └── tr.php
```

Sınıf ad alanlıdır. Hazır modüller `namespace WISECP\modules\Authentication;` yazar, çekirdek ise `WISECP\Modules\Authentication\{Yöntem}` kurar. İkisi de çözülür.

## Adım Adım

### Modül Sınıfını Kurma

1. `coremio/modules/Authentication/Acme/Acme.php` dosyasını dizin adını taşıyan bir sınıfla oluşturun.
2. Yapıcıda `config.php` dosyasını yükleyin. Yapıcı argüman almaz.
3. Global sınıfların başına ters bölü koyun: `\Session`, `\Filter`, `\Notification`.
4. Ailenizin kümesini uygulayın: `setup()` ile `install()`, ya da `verification()`.
5. İki durumda da `verify()` uygulayın.

### Kayıt

1. `setup(array $user_data)` sırrı ve sihirbazın göstereceği her şeyi döndürür. Düz kullanıcı dizisini alır.
2. Çekirdek kurulum verisini bir kez üretip oturumda saklar; sihirbaz ve etkinleştirme aynı kopyayı kullanır.
3. `install(array $data)` yazılan kodu saklanan veriye karşı denetler. Hata durumu kaydı iptal eder.
4. `setup()` dönüşü tercihin `data` anahtarında şifreli saklanır, sonraki çağrılarda geri verilir.
5. Kapatma yolu için `uninstall(array $data)` uygulayın.

### Giriş Sorusu

1. `verification(array $params)` soru ekranında çağrılır. Kodu üretin, iletin, saklayın ve ekran yükünü döndürün.
2. İletim hız sınırını kendiniz koyun ve kalan saniyeyi `retry_delay` ile bildirin.
3. `verify(array $params, $code)` gönderilen kodu alır. Başarı ya da hata durumunu döndürün; kilidi çekirdek uygular.
4. Kurtarma anahtarı için `verify_recovery(array $params, $key)` ekleyin ve yükte `recovery` değerini true yapın.
5. İki ekranı da test edin: ayrı oturum ve ayrı sayaç tutarlar.

## Referans

### Metot Sözleşmesi

Her çağrı korunduğu için metotların hepsi isteğe bağlıdır. Bileşim değildir: ne `setup()` ne `verification()` taşıyan bir yöntem kaydedilemez.

| Metot | Ne zaman çağrılır | Aile |
| --- | --- | --- |
| `setup(array $user_data): array` | Hesap paneli, etkin yöntem yokken | sır tabanlı |
| `install(array $data = []): array` | Kayıt kodu gönderildiğinde | sır tabanlı |
| `uninstall(array $data = []): array` | Yöntem kapatıldığında | ikisi de |
| `verification(array $params = []): array` | Soru ekranı, ya da hassas değişiklikten önce | kod tabanlı |
| `verify(array $params = [], $code = ''): array` | Kod gönderildiğinde (giriş ya da yükseltme) | ikisi de |
| `verify_recovery(array $params = [], $key = ''): array` | Kurtarma anahtarı gönderildiğinde | isteğe bağlı |

`setup()` varlığı dört ayrı yerde aile işareti olarak okunur. Panelin sihirbaz göstermesine ve hassas bir değişiklikten önce kod iletilmesine o karar verir. `settings.setup` config anahtarı aynı şeyi söyler ve önce o okunur.

### İmzalar ve Yük Biçimleri

```php
public function __construct();

// BİÇİM TUZAĞI: $user_data düz kullanıcı satırıdır (id, email, full_name),
// verify metotlarının aldığı ['user' => ..., 'data' => ...] sarmalayıcısı DEĞİL.
public function setup(array $user_data): array;

// $data tam olarak setup() metodunun döndürdüğüdür, oturum deposundan yeniden verilir.
public function install(array $data = []): array;

// $data saklanan tercih verisidir, yani setup() metodunun döndürdüğü.
public function uninstall(array $data = []): array;

// SIRA: önce params, sonra gönderilen değer.
// $params = ['user' => ['id' => 5, 'email' => '...'], 'data' => [ /* setup() ne döndürdüyse */ ]]
public function verification(array $params = []): array;
public function verify(array $params = [], string|int|null $code = ''): array;
public function verify_recovery(array $params = [], string|int|null $key = ''): array;
```

```php
// setup(): kayıt sihirbazının bastığı her şey, artı saklanacak sır.
// _preview anahtarları, şablonun biçimi bilmek zorunda kalmadan ekranın
// karakterleri gruplayabilmesi için vardır.
return [
    'secret_key'            => 'JBSWY3DPEHPK3PXP',
    'recovery_key'          => 'K7Q2M9XR4TZB6WVA',
    'secret_key_preview'    => 'JBSW Y3DP EHPK 3PXP',
    'recovery_key_preview'  => 'K7Q2 M9XR 4TZB 6WVA',
    'qr_code'               => 'data:image/png;base64,iVBORw0KG',   // bir data URI'si, yol değil
    'content'               => null,                                 // sihirbaz için ek HTML, ya da null
];

// verification(): soru ekranının nasıl davranacağı.
return [
    'digit'       => 6,        // kaç girdi kutusu çizileceği
    'retry_delay' => 118,      // yeniden göndermeye izin verilene dek saniye; 0 hemen demektir
    'recovery'    => true,     // kurtarma anahtarı alanını sun (gizlemek için yazmayın ya da false)
    'content'     => null,     // girdinin üstünde ek HTML, ya da null
];

// İletim hatası 'error' üzerinden bildirilir, çekirdek onu birebir yüzeye çıkarır.
return ['error' => 'Email could not be sent'];

// install(), uninstall(), verify(), verify_recovery(): bir durum, isteğe bağlı bir mesaj.
return ['status' => 'successful'];
return ['status' => 'error', 'message' => 'That code did not match.'];
```

Başarı sayılan tek şey birebir `successful` dizesidir. Dizi olmayan bir dönüş dahil diğer her şey bir deneme yakar.

### Config Anahtarları ve Kayıt

`config.php` aile işaretini ve kilit politikasını taşır. Çekirdek bu bloğu modül üstverisinden okur.

- **settings.setup**: Sır tabanlı yöntemde true, iletilen kodda false. Önce bu okunur, yedeği `setup()` varlığıdır.
- **settings.method**: Yöntem kartının metnini ve simgesini seçen küçük harfli kanal etiketi (`email`, `sms`, `totp`).
- **settings.attempts**: Hesap engellenmeden önce tolere edilen yanlış kod sayısı. Sıfır saymayı kapatır.
- **settings.attempts_penalty_minute**: Engelin dakika cinsinden süresi. Varsayılan beştir.
- **modules/authentications**: Operatörün açtığı yöntem adları listesi, çoğul olduğuna dikkat. Bu listede olmayan yöntem hiç sunulmaz.
- **Modules::Load()**: Burada örnek değil **üstveri** döndürür: `['lang' => [...], 'config' => [...]]`. Örnek, çözülen sınıf adı üzerinde `new` ile kurulur.

### Kayıt Nerede Saklanır

Kullanıcı başına tek yöntem, kullanıcının bilgi kaydında şifreli bir blok olarak durur. Siz çözülmüş `data` yarısını alırsınız.

```php
// install() kodu onayladıktan sonra enableTwoFactor tarafından yazılır.
$store = ['method' => $method];
if (method_exists($module, 'setup')) $store['data'] = $setupData;

User::setInfo($userId, ['authentication' => Crypt::encode(Utility::jencode($store), Config::get('crypt/user'))]);

// Her soruda geri okunur. Operatörün sonradan kapattığı bir yöntem burada
// false döner, böylece giriş ikinci faktör olmadan sürer.
$raw  = User::getInfo($userId, ['authentication'])['authentication'] ?? '';
$pref = Utility::jdecode(Crypt::decode($raw, Config::get('crypt/user')), true);

// Ve verify() metodunuzun aldığı sarmalayıcı budur.
$params = ['user' => $userRow, 'data' => $pref['data'] ?? []];
```

Bunun etrafında iki kanca çalışır. `gate:user.two_factor_disable` bir mesaj döndürerek kapatmayı veto eder; `action:user.two_factor_changed` her açma ve kapamadan haberdar edilir.

## Örnek

Bildirim sistemiyle ileten kod tabanlı bir yöntem.

```php
<?php
namespace WISECP\modules\Authentication;

class Acme
{
    public array $config;

    private const DELAY = 120;

    public function __construct()
    {
        $this->config = include __DIR__ . DS . 'config.php';
    }

    public function verification(array $params = []): array
    {
        $userId    = (int) ($params['user']['id'] ?? 0);
        $stash     = $this->stash();
        $remaining = self::DELAY;
        $mayResend = true;

        // Engellenen kullanıcılar ceza süresini beklerken iletim hakkı yakamamalıdır.
        if (\User::CheckBlocked("member-login-authentication-attempt", $userId)) $mayResend = false;

        if ($stash) {
            $remaining = (int) $stash['expire'] - time();
            if ($remaining > 0) $mayResend = false;
        }

        if ($mayResend) {
            $expire = \DateManager::next_date(['second' => self::DELAY]);
            $code   = random_int(100000, 999999);

            $sent = \Notification::dispatch('user', 'two-factor-verification', [
                'user_id' => $userId,
                'code'    => $code,
                '_sync'   => true,
            ]);

            // 'error' iletim hatası kanalıdır; çekirdek bu mesajı olduğu gibi basar.
            if (!$sent) return ['error' => 'Verification code could not be delivered.'];

            $this->stash(['code' => $code, 'expire' => \DateManager::strtotime($expire)]);
            $remaining = self::DELAY;
        }

        return [
            'digit'       => (int) ($this->config['settings']['digits'] ?? 6),
            'retry_delay' => max(0, $remaining),
            'content'     => null,
        ];
    }

    public function verify(array $params = [], string|int|null $code = ''): array
    {
        $stash = $this->stash();

        if (empty($code) || !$stash || (string) $stash['code'] !== (string) $code)
            return ['status' => 'error'];

        // Tek kullanım: aynı kodun tekrarı geçemesin diye temizle.
        \Session::delete('AcmeAuthData');

        return ['status' => 'successful'];
    }

    public function uninstall(array $data = []): array
    {
        \Session::delete('AcmeAuthData');

        return ['status' => 'successful'];
    }

    private function stash(?array $write = null): array
    {
        if ($write !== null) {
            \Session::set('AcmeAuthData', \Utility::jencode($write), true);
            return $write;
        }

        $raw  = \Utility::jdecode((string) \Session::get('AcmeAuthData', true), true);
        if (!$raw) return [];

        // Süresi dolmuş depo yok demektir, yoksa bayat bir kod sonsuza dek geçerli kalır.
        if ((int) ($raw['expire'] ?? 0) < time()) {
            \Session::delete('AcmeAuthData');
            return [];
        }

        return $raw;
    }
}
```

```php
// Örnek: önce Load, sonra çözülen sınıf adı üzerinde çıplak bir new.
$class  = 'WISECP\\Modules\\Authentication\\' . $method;
$module = Modules::Load('Authentication', $method) && class_exists($class) ? new $class() : false;

$params      = ['user' => $state['user'] ?? [], 'data' => $state['authentication']['data'] ?? []];
$recoveryKey = trim(str_replace(' ', '', $recoveryKey));

if ($recoveryKey !== '')
    $verify = method_exists($module, 'verify_recovery') ? $module->verify_recovery($params, $recoveryKey) : ['status' => 'error'];
else
    $verify = method_exists($module, 'verify') ? $module->verify($params, $code) : ['status' => 'error'];

if (!is_array($verify) || ($verify['status'] ?? 'error') !== 'successful') {
    // Load burada ÜSTVERİ döndürür, deneme politikası oradan gelir.
    $meta  = Modules::Load('Authentication', $method) ?: [];
    $total = (int) ($meta['config']['settings']['attempts'] ?? 0);

    if ($total) {
        $penalty  = (int) ($meta['config']['settings']['attempts_penalty_minute'] ?? 5);
        $attempts = (int) ($state['attempts'] ?? 0) + 1;

        if ($total - $attempts < 1) User::addBlocked($reason, $userId, [], DateManager::next_date(['minute' => $penalty]));
        else $state['attempts'] = $attempts;
    }

    return ['status' => 'error', 'message' => $message];
}
```

## Tuzaklar

> **setup() kullanıcı satırını alır, diğerleri sarmalayıcı**
> 
> `setup()` düz kullanıcı dizisi alır ve `$user_data['email']` okur. Diğerlerine `['user' => [...], 'data' => [...]]` verilir. Yanlış biçim hata değil boş değer üretir.

> **Kurtarma anahtarı bir kez çalışır, sonra 2FA kapanır**
> 
> Başarılı bir `verify_recovery()` çağrısında çekirdek kayıtlı tercihi siler, kullanıcı yeniden kaydolur. Anahtar bir kez kullanılır; kalıcı bir alternatif parola değildir.

> **Hız sınırını kendi modülünüzde koyun**
> 
> Çekirdek yanlış cevapları sayar, yeniden göndermeleri değil. Yeniden yüklenen bir soru ekranı `verification()` metodunu tekrar çağırır, yani koşulsuz gönderim gelen kutusu doldurur. Bekleyen kodu saklayın ve `retry_delay` döndürün.

> **Durum dizisi döndürün, fırlatmayın**
> 
> Çekirdek yakalamak yerine bir durum okur. Yanlış kodu `['status' => 'error']`, iletim hatasını `['error' => '...']` olarak bildirin. Yakalanmamış bir fırlatma giriş ekranını bozar.

> **Yöntem kapatılınca kayıtlar korunur**
> 
> Kayıtlı tercih her okumada etkin listeye karşı denetlenir. Yönteminizi kapatmak kayıtlı kullanıcıları dışarıda bırakmaz; artık sorguya çekilmezler. Yeniden açınca soru geri gelir.

## İlgili Makaleler

- [Sosyal Giriş Sağlayıcısı Yazma](https://dev.wisecp.com/tr/sosyal-giris-saglayicisi-yazma)
- [Bildirim Şablonları Nasıl Çalışır](https://dev.wisecp.com/tr/bildirim-sablonlari-nasil-calisir)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
- [Güvenlik Pratikleri](https://dev.wisecp.com/tr/guvenlik-pratikleri)
- [Yapılandırma Okuma ve Yazma](https://dev.wisecp.com/tr/yapilandirma-okuma-ve-yazma)
