# Sosyal Giriş Sağlayıcısı Yazma

https://dev.wisecp.com/tr/sosyal-giris-saglayicisi-yazma

Giriş, kayıt ve admin ekranlarına "şununla giriş yap" butonu ekleyin. Tek bir sınıf sağlayıcının uçlarını bildirir ve taleplerini bir hesaba eşler.

## Genel Bakış

SocialAuth gerçek bir taban sınıfa sahiptir. `SocialAuthProvider` yetkilendirme adresini, CSRF durumunu, kod takasını ve token doğrulamasını üstlenir. Modülünüz beş uç, bir şema ve bir eşleme metodu bildirir.

Üç sağlayıcı gelir. Keşif kancaya değil kayıt defterine dayanır: `Auth::activeProviders()` etkin modülleri tutar, yeni bir klasör her ekranda belirir.

Akış: buton, popup, onay, tek geri çağrı, kod takası, doğrulama, hesap çözümü, giriş.

## Ön Koşullar

- OAuth 2.0 kod akışı ve OpenID Connect konuşan, RS256 imzalı `id_token` veren bir sağlayıcı.
- Bir client id ve secret, kayıtlı bir yönlendirme adresi.
- Token ve JWKS uçlarına HTTPS erişimi.
- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi), [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi).

## Yapı

```bash
coremio/modules/SocialAuth/Acme/
├── Acme.php          class Acme extends \SocialAuthProvider
├── config.php        künye + durum + boş ayar anahtarları
├── acme.svg          marka logosu, isteğe bağlı (bi-* simgesine geri düşülür)
└── lang/
    ├── en.php
    └── tr.php
```

- **SocialAuthProvider**: Ortak taban.
- **Auth::activeProviders()**: Oturum açma ekranlarının okuduğu kayıt defteri.
- **Auth::handleProviderCallback()**: Geri çağrı giriş noktası; durumu tüketir, `feedback()` çağırır.
- **Auth::connectProvider()**: Hesabı çözer ve oturumu açar.
- **coremio/modules/SocialAuth**: Klasörünüz buraya girer.

## Adım Adım

### 1. Modül İskeletini Kurun

1. `coremio/modules/SocialAuth/Acme/` dizinini açın.
2. `config.php` yazın: `meta`, `status => false` ve boş bir `settings` haritası.
3. `lang/en.php` ve `lang/tr.php` yazın: `button-label`, `field-*` çiftleri, `error-*`, kurulum adımları.
4. Renkli logoyu `acme.svg` olarak bırakın ya da `bi-*` ikon adı döndürün.

### 2. Uçlar ve Kimlik Bilgileri

1. Tabanı genişletin ve beş uç metodunu uygulayın.
2. `configFields()` uygulayın; `'type' => 'password'` alanı şifrelenir ve maskelenir.
3. `testConnection()` metodunu sağlayıcıya gerçek bir soru soracak şekilde yazın.
4. Kurulum adımlarını `setup-step-1`, `setup-step-2` diye kesintisiz yazın.

### 3. Kimliği Eşleyin

1. `feedback()` uygulayın: önce `exchange_code()`, sonra `verify_id_token()`.
2. Doğrulanmamış e-postayı hata fırlatarak reddedin; çözücü e-postayla eşler.
3. Kimliği hesap başına seçin: Google ve Apple `sub`, Microsoft `oid` kullanır.
4. Aşağıdaki iki parçalı diziyi döndürün.

### 4. Etkinleştirin

1. Ayarlar'daki Social Auth listesinde sağlayıcınızı bulun.
2. Panelin gösterdiği Yönlendirme Adresini sağlayıcı konsoluna kaydedin.
3. Kimlik bilgilerini yapıştırın, Bağlantıyı Test Et'e tıklayın, anahtarı açıp kaydedin.
4. Giriş sayfasını yenileyin.

## Referans

### Soyut Sözleşme

Dokuz soyut metot.

```php
// Sunum. ['label' => string, 'icon' => 'bi-*', 'logo' => mutlak url (isteğe bağlı)]
abstract public function provider_meta(): array;

// Ayarlar akordiyonu için kimlik bilgisi şeması. Aşağıdaki anahtar tablosuna bakın.
abstract public function configFields(): array;

// [Bağlantıyı Test Et] butonu. true döndürün, ya da operatörün işine yarayacak bir mesajla fırlatın.
abstract public function testConnection(): bool;

abstract protected function auth_endpoint(): string;    // popup'ın kullanıcıyı gönderdiği yer
abstract protected function token_endpoint(): string;   // kodun takas edildiği yer
abstract protected function scopes(): string;           // örn. 'openid email profile'
abstract protected function jwks_url(): string;         // imza doğrulama anahtarları
abstract protected function issuers(): array;           // kabul edilen `iss` değerleri

// $ctx = ['code' => string, 'redirect_uri' => string, 'nonce' => string]
abstract public function feedback(array $ctx): array;
```

### feedback()

Bağlam dizisi üç anahtar taşır.

- **code**: Yetkilendirme kodu; sorgu dizesinde ya da gövdede gelir.
- **redirect_uri**: Yetkilendirme adımının kullandığı adres.
- **nonce**: Yetkilendirme adresine konan değer; `verify_id_token()` metoduna geçirin.

```php
return [
    // Hesap bağı. `name` bir kullanıcı bilgi anahtarı olur, `value` şifreli kalıcı kimliktir.
    'field_info' => [
        'name'  => "acme_uid",
        'value' => Crypt::encode($sub, Config::get("crypt/user")),
    ],
    // Hesabın neyden kurulduğu / neyle eşleştirildiği.
    'data' => [
        'name'         => "Ada",              // ad
        'surname'      => "Lovelace",         // soyad
        'email'        => "ada@example.com",  // sağlayıcı tarafından doğrulanmış OLMALI
        'picture'      => "",                 // avatar url'si, ya da boş dize
        'provider_uid' => $sub,               // ham kimlik, şifresiz
    ],
];
```

### Alan Tanımı

Dönen harita `alan adı => tanım` biçimindedir; ad hem POST adı hem ayar anahtarıdır.

- **type**: Yoksa metin girdisi; `password` maskeli ve şifreli, `textarea` çok satırlı.
- **label**: Görünen etiket; dil dosyanızdan okuyun.
- **required**: HTML doğrulamasını değil etkinlik durumunu besler.
- **secret**: `password` olmayan bir alanı şifreleyip maskeler.
- **placeholder**: İpucu.
- **description**: Yardım metni.
- **rows**: `textarea` yüksekliği.

### Geçersiz Kılma Noktaları

Varsayılanlar düz bir OpenID Connect sağlayıcısına uyar.

| Metot | Varsayılan | Ne zaman geçersiz kılınır |
| --- | --- | --- |
| `client_id()` | saklı `client_id` | başka bir anahtardaysa; beklenen izleyici de budur. |
| `client_secret()` | çözülmüş `client_secret` | sabit sır yoksa ve her istekte üretiliyorsa. |
| `extra_auth_params()` | `['prompt' => 'select_account']` | ek parametre gerekiyorsa. Değiştirmeyin, birleştirin. |
| `accept_issuer($iss, $issuers, $payload)` | `issuers()` ile eşleşme | ihraççı sabit değilse. |

> **Erişimciyi çağırın**
> 
> Yetkilendirme adresi, kod takası ve izleyici denetimi `client_id()` üzerinden geçer. Ayarı doğrudan okumak geçersiz kılmayı etkisiz bırakır.

### Taban Yardımcıları

- **exchange_code()**: Kodu client secret ile gönderir; `id_token` yoksa fırlatır.
- **verify_id_token()**: İmzayı, ihraççıyı, izleyiciyi, süreyi ve nonce'ı lokal doğrular; talepleri döndürür.
- **setting()**: Tek bir saklı ayar, ham hâliyle; sır şifreli döner.
- **callback_url()**: Tek yönlendirme adresi; başlatan taraf durumda taşınır.
- **enabled()**: Anahtar açıkken ve zorunlu her kimlik bilgisi doluyken true.
- **authorize_url($context, $mode)**: Yetkilendirme adresini kurar ve durumu kaydeder. `$context` admin ya da client, `$mode` login ya da register.
- **setup_guide()**: `setup-step-N` anahtarlarını ilk boşluğa kadar toplar.
- **save_settings($fields)**: Akordiyonu kaydeder; maske gönderilirse saklı sırrı korur.

### Çekirdek Çağrı Noktaları

| Çağrı noktası | Çağırdığı | Neden |
| --- | --- | --- |
| `Auth::activeProviders()` | `provider_meta()`, `enabled()`, `connection_button()` | Kayıt defteri. |
| `Auth::handleProviderCallback()` | `enabled()`, `consume_state()`, `feedback()` | Geri çağrı; mesajınız popup'a döner. |
| `controllers/admin/settings.php` | `configFields()`, `callback_url()`, `setup_guide()` | Ayarlar akordiyonu. |
| `save_social_provider()` | `save_settings()` | Kayıt; her alan tipine göre filtrelenir. |
| `test_social_provider()` | `testConnection()` | Test; yazılan değerler önce bellekte uygulanır. |

## Örnek

Eksiksiz bir sağlayıcı, sonra çekirdek kodu.

```php
<?php
namespace WISECP\Modules\SocialAuth;

use Config;
use Crypt;
use Exception;
use Filter;
use Utility;

class Acme extends \SocialAuthProvider
{
    public function provider_meta(): array
    {
        return [
            'label' => $this->lang["button-label"] ?? "Continue with Acme",
            'icon'  => 'bi-box-arrow-in-right',
            'logo'  => $this->url . 'acme.svg',
        ];
    }

    public function configFields(): array
    {
        return [
            'client_id' => [
                'type'        => 'text',
                'label'       => $this->lang["field-client-id"] ?? "Client ID",
                'required'    => true,
                'placeholder' => "acme-0000-0000",
                'description' => $this->lang["field-client-id-desc"] ?? '',
            ],
            'client_secret' => [
                'type'        => 'password',
                'label'       => $this->lang["field-client-secret"] ?? "Client Secret",
                'required'    => true,
                'description' => $this->lang["field-client-secret-desc"] ?? '',
            ],
        ];
    }

    protected function auth_endpoint(): string  { return "https://id.acme.example/oauth2/authorize"; }
    protected function token_endpoint(): string { return "https://id.acme.example/oauth2/token"; }
    protected function scopes(): string         { return "openid email profile"; }
    protected function jwks_url(): string       { return "https://id.acme.example/.well-known/jwks.json"; }
    protected function issuers(): array         { return ["https://id.acme.example"]; }

    public function testConnection(): bool
    {
        $clientId = $this->client_id();
        if ($clientId === '')
            throw new Exception($this->lang["error-invalid-client-id"] ?? "Please enter a Client ID.");

        // Sağlayıcıya bu kimliğin var olup olmadığını sorun: bilinmeyen biri invalid_client,
        // gerçek olan redirect_uri_mismatch döndürür. Biçim denetimi ikisini de geçirirdi.
        $probe = $this->auth_endpoint() . '?' . http_build_query([
            'client_id'     => $clientId,
            'response_type' => 'code',
            'scope'         => $this->scopes(),
            'redirect_uri'  => $this->callback_url(),
            'state'         => 'wisecp-connectivity-check',
        ]);

        $resp = (string) Utility::HttpRequest($probe, ['timeout' => 8]);
        if ($resp === '')
            throw new Exception($this->lang["error-unreachable"] ?? "Could not reach Acme.");

        if (str_contains($resp, 'invalid_client'))
            throw new Exception($this->lang["error-client-not-found"] ?? "Acme does not recognize this Client ID.");

        return true;
    }

    public function feedback(array $ctx): array
    {
        $code = (string) ($ctx['code'] ?? '');
        if ($code === '') throw new Exception("Missing authorization code.");

        $token   = $this->exchange_code($code, (string) ($ctx['redirect_uri'] ?? ''));
        $payload = $this->verify_id_token((string) $token['id_token'], (string) ($ctx['nonce'] ?? ''));

        $email = (string) ($payload["email"] ?? '');
        if (!$email)
            throw new Exception($this->lang["error-no-email"] ?? "Could not read your email address.");

        // Şüphede reddet: çözücü mevcut bir hesabı e-postayla eşler, yani doğrulanmamış
        // bir adres herkesin başkasının hesabını sahiplenmesine izin verirdi.
        if (($payload["email_verified"] ?? false) !== true)
            throw new Exception($this->lang["error-email-unverified"] ?? "Acme has not verified this email address.");

        $fullName = trim((string) ($payload["given_name"] ?? '') . ' ' . (string) ($payload["family_name"] ?? ''));
        $smash    = Filter::name_smash(Utility::ucfirst_space(Utility::substr($fullName, 0, 255)));

        $sub = (string) ($payload["sub"] ?? '');
        if ($sub === '') throw new Exception($this->lang["error-no-account"] ?? "Could not identify your Acme account.");

        return [
            'field_info' => [
                'name'  => "acme_uid",
                'value' => Crypt::encode($sub, Config::get("crypt/user")),
            ],
            'data' => [
                'name'         => $smash["first"] ?? '',
                'surname'      => $smash["last"] ?? '',
                'email'        => $email,
                'picture'      => (string) ($payload["picture"] ?? ''),
                'provider_uid' => $sub,
            ],
        ];
    }
}
```

```php
// classes/Auth.php - handleProviderCallback(), sınıfınızın dokunduğu kısma indirgenmiş.
$st = $provider->consume_state($state);            // mode + context + nonce, tek seferlik
if (!$st) return ['status' => 'error', 'message' => "Sign-in could not be completed."];

$result = $provider->feedback([
    'code'         => $code,
    'redirect_uri' => $provider->callback_url(),
    'nonce'        => (string) ($st['nonce'] ?? ''),
]);

// connectProvider() sonra şunları bu sırayla yapar:
//   1. hesabı field_info ile arar (e-posta değişikliğinden sağ çıkar),
//   2. data.email değerine düşer,
//   3. yalnız mode=register ve üye bağlamında hesabı oluşturur,
//   4. field_info değerini saklar ki sonraki giriş 1. adımla çözülsün.
return Auth::connectProvider('member', 'Acme', $result, (string) ($st['mode'] ?? 'login'));
```

Oturum açma ekranı kayıt defterine sorar:

```php
// controllers/website/sign.php - giriş sayfası, kayıtta ise aynı çağrı "register" ile.
$this->addData("social_providers", \Auth::activeProviders("login", "client"));

// Tema sonra diziyi gezer; her kayıt ['meta' => [...], 'button' => '<html>'] biçimindedir.
```

## Tuzaklar

> **Doğrulanmamış e-posta hesap ele geçirmesidir**
> 
> Çözücü hesabı e-postayla eşler, yani doğrulanmamış bir adres saldırganın başkası olarak girmesine izin verir. Pozitif doğrulama sinyali arayın.

> **Yanlış kalıcı kimlik ikinci girişi bozar**
> 
> Hesap için değişmez ve uygulamalar arasında aynı olan bir talep seçin; uygulama başına kimlik, hesabı bir daha üretilmeyecek değere bağlar.

> **Hatayı fırlatarak bildirin**
> 
> `$this->error = '...'; return false;` eski kuşaktan kalmadır. Geri çağrı işleyicisi ve Ayarlar operation'ları hatayı yakalayıp mesajını gösterir; dönen false bozuk sonuç sayılır.

> **config.php dosyasına elle sır koymayın**
> 
> Sırlar şifreli yazılır ve eşleşen çözücüyle okunur; elle yazılan değer boş dizeye döner.

> **Örneği fabrikayla kurun**
> 
> Her çağrı noktası `Modules::getInstance("SocialAuth", $name)` kullanır; bu config ve dili doldurur. Doğrudan kurmak atlar.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Modül Dil Dosyaları](https://dev.wisecp.com/tr/modul-dil-dosyalari)
- [Kimlik Doğrulama Modülü Yazma](https://dev.wisecp.com/tr/kimlik-dogrulama-modulu-yazma)
- [Giriş ve Kayıt](https://dev.wisecp.com/tr/giris-ve-kayit)
- [Güvenlik Pratikleri](https://dev.wisecp.com/tr/guvenlik-pratikleri)
