Tema
Kimlik Doğrulama
Widget dört kimlik moduyla çalışır. Giriş hiçbir zaman zorunlu değildir: ziyaretçi modu her zaman açıktır; diğer modlar ziyaretçiyi isteğe bağlı olarak üye kimliğine yükseltir. Hangi mod kullanılırsa kullanılsın sohbet ve arama aynı şekilde çalışır — üye kimliği yalnızca konuşmaların kime ait olduğunu netleştirir.
| Mod | Kimlik kaynağı | Ne gerekir |
|---|---|---|
| 1. Ziyaretçi (varsayılan) | Otomatik, anonim | Hiçbir şey |
2. Özel giriş (getToken) | Sizin backend'iniz | secret_key ile imzalı soket token'ı |
| 3. Sosyal giriş — Webfon anahtarları | Webfon'un OAuth uygulamaları | Admin panelinde sağlayıcıyı açmak |
| 4. Sosyal giriş — kendi anahtarlarınız | Sizin OAuth uygulamalarınız | Admin panelinde client_id / client_secret |
1. Ziyaretçi modu (varsayılan)
Hiçbir yapılandırma gerektirmez. Widget ilk açılışta sessizce anonim bir ziyaretçi kimliği üretir ve bununla bağlanır. Ziyaretçiden isim ya da giriş asla istenmez; sohbet ve arama doğrudan başlar. Aşağıdaki modların tümü bu tabanın üzerine eklenir — üye çıkış yaptığında widget yine ziyaretçi moduna döner.
Ziyaretçi kimliği (wf_visitor_uid), tarayıcıda üretilen dışa çıkarılamayan bir anahtardan türetilir (bkz. Bağlantı güvenliği) — böylece kimlik, o anahtara sahip olmakla eşdeğerdir. Anahtar IndexedDB'de tutulur; anahtar/veri temizlenirse yeni bir ziyaretçi kimliği üretilir (bkz. Depolama Anahtarları).
2. Özel giriş — getToken
Sitenizde giriş yapmış kullanıcıyı widget'a kendiniz tanıtırsınız. Backend'iniz, widget'ın bağlanacağı soket token'ını doğrudan imzalar — sağlayıcının secret_key'i ile (HS256), ~1 saat geçerli bir JWT. Widget bu token'ı olduğu gibi kullanır; araya bir Webfon ucu girmez.
json
{
"pid": "PROVIDER_ID",
"uid": "<user-id>",
"type": "user",
"cnf": { "jkt": "<widget-jkt>" },
"name": "...",
"email": "...",
"avatar": "https://.../avatar.jpg",
"iat": 1735689600,
"exp": 1735693200
}pid(zorunlu) — sağlayıcı kimliği.uid(zorunlu) — sitenizin kendi kullanıcı kimliği.type(zorunlu) —"user"olmalı.cnf.jkt(zorunlu) — widget bağlantı anahtarının parmak izi;getToken'a argüman olarak gelir (aşağıya bakın). Konmazsa soket bağlantıyı reddeder (bkz. Bağlantı güvenliği).name/email/avatar(opsiyonel) — widget'ta üye adı ve avatarı olarak gösterilir (avatarbir URL'dir).exp— ~1 saat; widget süre dolmadangetTokenile otomatik yeniler.
secret_key tarayıcıya gönderilmez
İmza sırrını (secret_key) asla tarayıcıya göndermeyin. Token'ı kendi backend'iniz imzalar; widget yalnızca imzalı token'ı taşır. secret_key sunucuda kalır.
Token'ı backend'inizde imzalama
Widget, getToken callback'ini çağırırken bağlantı anahtarının parmak izini (jkt) argüman olarak verir. Backend'iniz bunu cnf.jkt'ye gömerek soket token'ını herhangi bir JWT kütüphanesiyle imzalar:
php
<?php
// routes/api.php — widget getToken({ jkt }) çağırır; gövde: { "jkt": "..." }.
use Firebase\JWT\JWT; // firebase/php-jwt
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/webfon-token', function (Request $request) {
$user = $request->user(); // oturumdaki üye; yoksa null → ziyaretçi
if (!$user) {
return response()->json(['token' => null]);
}
$now = time();
$payload = [
'pid' => 'PROVIDER_ID',
'uid' => (string) $user->id,
'type' => 'user',
'cnf' => ['jkt' => (string) $request->input('jkt')], // anahtara bağlar (zorunlu)
'name' => $user->name, // opsiyonel
'email' => $user->email, // opsiyonel
'avatar' => $user->avatar_url, // opsiyonel (URL)
'iat' => $now,
'exp' => $now + 3600, // ~1 saat; widget getToken ile yeniler
];
// secret_key SADECE sunucuda: config/services.php → 'webfon' => ['secret' => env('WEBFON_SECRET')]
return response()->json([
'token' => JWT::encode($payload, config('services.webfon.secret'), 'HS256'),
]);
})->middleware('auth');php
<?php
namespace App\Controller;
use Firebase\JWT\JWT; // firebase/php-jwt
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
class WebfonTokenController extends AbstractController
{
// widget getToken({ jkt }) çağırır; gövde: { "jkt": "..." }.
#[Route('/api/webfon-token', methods: ['POST'])]
public function __invoke(Request $request): JsonResponse
{
$user = $this->getUser(); // oturumdaki üye; yoksa null → ziyaretçi
if (!$user) {
return $this->json(['token' => null]);
}
$jkt = (string) ($request->toArray()['jkt'] ?? '');
$now = time();
$payload = [
'pid' => 'PROVIDER_ID',
'uid' => (string) $user->getUserIdentifier(),
'type' => 'user',
'cnf' => ['jkt' => $jkt], // anahtara bağlar (zorunlu)
'name' => $user->getName(), // opsiyonel
'email' => $user->getEmail(), // opsiyonel
'avatar' => $user->getAvatarUrl(), // opsiyonel (URL)
'iat' => $now,
'exp' => $now + 3600, // ~1 saat; widget getToken ile yeniler
];
// secret_key SADECE sunucuda: %env(WEBFON_SECRET)%
return $this->json([
'token' => JWT::encode($payload, $_ENV['WEBFON_SECRET'], 'HS256'),
]);
}
}js
// app/api/webfon-token/route.js (App Router)
import jwt from 'jsonwebtoken';
import { auth } from '@/auth'; // sizin oturum çözümünüz (ör. NextAuth v5)
// widget getToken({ jkt }) çağırır; gövde: { jkt }.
export async function POST(req) {
const session = await auth();
if (!session?.user) return Response.json({ token: null }); // → ziyaretçi
const { jkt } = await req.json();
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
pid: 'PROVIDER_ID',
uid: String(session.user.id),
type: 'user',
cnf: { jkt }, // anahtara bağlar (zorunlu)
name: session.user.name, // opsiyonel
email: session.user.email, // opsiyonel
avatar: session.user.image, // opsiyonel (URL)
iat: now,
exp: now + 3600, // ~1 saat; widget getToken ile yeniler
},
process.env.WEBFON_SECRET, // sunucuda kalır; tarayıcıya gönderilmez
{ algorithm: 'HS256' },
);
return Response.json({ token });
}js
import jwt from 'jsonwebtoken';
// POST /api/webfon-token — widget getToken({ jkt }) çağırır; gövde: { jkt }.
app.post('/api/webfon-token', (req, res) => {
const user = req.user; // sizin oturumunuz
if (!user) return res.json({ token: null }); // → widget ziyaretçi kalır
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
pid: 'PROVIDER_ID',
uid: String(user.id),
type: 'user',
cnf: { jkt: req.body.jkt }, // anahtara bağlar (zorunlu)
name: user.name, // opsiyonel
email: user.email, // opsiyonel
avatar: user.avatarUrl, // opsiyonel (URL)
iat: now,
exp: now + 3600, // ~1 saat; widget getToken ile yeniler
},
process.env.WEBFON_SECRET, // sunucuda kalır; tarayıcıya gönderilmez
{ algorithm: 'HS256' }, // exp payload'da; expiresIn KULLANMAYIN
);
res.json({ token });
});php
<?php
use Firebase\JWT\JWT; // firebase/php-jwt
// POST /api/webfon-token — widget getToken({ jkt }) çağırır; gövde: { "jkt": "..." }.
// secret_key SADECE sunucuda kalır; tarayıcıya asla gönderilmez.
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$jkt = (string) ($body['jkt'] ?? '');
$user = current_member(); // sizin oturumunuz; yoksa null → ziyaretçi
if (!$user) { echo json_encode(['token' => null]); exit; }
$now = time();
$payload = [
'pid' => 'PROVIDER_ID',
'uid' => (string) $user->id,
'type' => 'user',
'cnf' => ['jkt' => $jkt], // anahtara bağlar (zorunlu)
'name' => $user->name, // opsiyonel
'email' => $user->email, // opsiyonel
'avatar' => $user->avatarUrl, // opsiyonel (URL)
'iat' => $now,
'exp' => $now + 3600, // ~1 saat; widget getToken ile yeniler
];
echo json_encode(['token' => JWT::encode($payload, SECRET_KEY, 'HS256')]);getToken ile bağlama (önerilen)
Bir callback tanımlarsınız; widget token'ı gerektiğinde kendisi çeker — ilk açılışta ve süre dolduğunda otomatik yeniler. Callback, bağlantı anahtarının jkt'sini alıp yukarıdaki uca iletir; null döndürürse widget ziyaretçi olarak çalışır. İstek widget ile aynı origin'e gittiğinden CORS gerekmez.
html
<script>
window.WebfonConfig = {
providerId: 'PROVIDER_ID',
getToken: async ({ jkt }) => {
const res = await fetch('/api/webfon-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ jkt }),
});
return res.ok ? (await res.json()).token : null; // null → ziyaretçi
},
};
</script>
<script src="https://assets.webfon.io/sdk.js" async></script>js
initWebfon({
providerId: 'PROVIDER_ID',
getToken: async ({ jkt }) => {
const res = await fetch('/api/webfon-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ jkt }),
});
return res.ok ? (await res.json()).token : null;
},
});Alınan soket token'ı wf_member_token_<providerId> anahtarıyla localStorage'da (exp kontrolüyle) cache'lenir — sayfa geçişlerinde getToken gereksiz çağrılmaz. Token bağlantı anahtarına bağlı olduğundan (cnf.jkt), çalınsa bile başka bir tarayıcıda kullanılamaz. logout() cache'i siler.
Neden getToken?
Soket token'ı tarayıcının origin-başına anahtarına bağlıdır (cnf.jkt); backend onu anahtarı görmeden önceden imzalayamaz. getToken bu jkt'yi callback argümanı olarak sağladığından özel girişin tek yoludur. Oturumu sonlandırmak için logout() kullanılır.
3. Sosyal giriş — Webfon anahtarları
Kod yazmadan üye girişi: ziyaretçi, widget'ın giriş sayfasındaki sosyal butonlarla (Google, GitHub…) oturum açar. OAuth uygulaması olarak Webfon'un kendi uygulamaları kullanılır — tek yapmanız gereken, admin panelindeki Üye Girişi (Member Login) bölümünde anahtar kaynağını Webfon Application Keys bırakıp istediğiniz sağlayıcıları açmak (bkz. Admin Paneli).
Yalnızca açtığınız sağlayıcılar widget'ın giriş sayfasında listelenir; liste, yapılandırma yanıtının auth.oauth_providers alanından gelir.
Desteklenen sağlayıcılar
İlk fazda: Google, Facebook, GitHub, GitLab, Discord. Apple, X, TikTok ve Amazon daha sonra eklenecektir.
4. Sosyal giriş — kendi OAuth uygulamalarınız
Onay ekranında Webfon yerine kendi markanızın görünmesini isterseniz, sağlayıcılarda kendi OAuth uygulamalarınızı oluşturup anahtarlarını admin paneline girersiniz. Üye Girişi (Member Login) bölümünde:
- Key Source →
Own Application Keysseçin. - Sağlayıcı başına etkinleştirme kutusu +
client_id/client_secretgirin. - Apple için ek olarak
team_id,key_idve p8 private key gerekir.
OAuth uygulamanızda redirect URI olarak, sağlayıcı adını ({social}) yerine koyarak şunu kaydedin — URL işletmeden bağımsızdır (sağlayıcı kimliği imzalı state içinde taşınır), bu yüzden her sosyal sağlayıcı için tek bir URI yeterlidir:
https://api.webfon.io/v1/public/oauth/{social}/callbackÖrnekler: …/oauth/google/callback, …/oauth/github/callback, …/oauth/discord/callback.
Akış mod 3 ile birebir aynıdır; yalnızca kullanılan uygulama anahtarları değişir.
OAuth akışı (popup + postMessage)
Sosyal giriş her iki anahtar kaynağında da aynı akışı izler:
- Ziyaretçi giriş sayfasında bir sosyal butona tıklar; widget bir popup açar:
GET /v1/public/provider/{id}/oauth/{social}?origin=<sayfa-origin'i>. APIorigin'i sağlayıcının kayıtlı domain'iyle doğrular ve sosyal sağlayıcının onay sayfasına yönlendirir. - Kullanıcı onaylar; sosyal sağlayıcı
…/oauth/{social}/callbackucuna döner. API kimliği alır, üye kaydını oluşturur/günceller ve bir kimlik token'ı üretir. - Callback sayfası token'ı
window.opener.postMessage(...)ile yalnızca doğrulanmış origin'e iletir ve popup kendini kapatır. Widget token'ı alır; normal üye oturumu başlar.
Üretilen kimlik token'ı sağlayıcının secret_key'i ile HS256 imzalı, 24 saat geçerli bir JWT'dir; uid alanı <social>:<sub> biçimindedir (örn. github:12345). API bunu /member/token?auth= ucunda anahtar bağlamalı soket token'ına çevirir (özel girişten farklı olarak — orada token'ı backend'iniz doğrudan imzalar). Uç ayrıntıları için bkz. Sunucu Uçları.
SDK yüzeyi
logout() ve wf_member_token_<providerId> cache anahtarı sosyal girişte de geçerlidir. OAuth asla otomatik tetiklenmez — yalnızca ziyaretçi giriş sayfasındaki butona tıklayınca başlar.
Bağlantı güvenliği
Widget yüklenirken, çalıştığı origin'de dışa çıkarılamayan bir ECDSA (P-256) anahtar çifti üretir ve IndexedDB'de saklar (private key hiçbir zaman ham byte olarak dışarı çıkmaz). WebSocket bağlantısı açılırken, bu anahtarla imzalanmış tek seferlik bir kanıt (DPoP, RFC 9449 ruhu) gönderilir. Sunucu bağlantıyı yalnızca kanıt anahtarın kimliğiyle eşleşiyorsa kabul eder:
- Üye modlarında soket token'ına anahtarın parmak izi (
cnf.jkt) gömülür; token başka bir yere taşınıp tekrar kullanılamaz. - Ziyaretçi modunda kimlik (
uid) doğrudan anahtardan türetildiği için, çalınan bir token/uid başka anahtarla doğrulanamaz.
Bu, token çalınıp başka bir makineden tekrar kullanılmasını engeller. (Sayfada canlı çalışan bir XSS'e karşı koruma değildir — o, CSP'nin işidir.)
Güvenli bağlam gerekir
Anahtar üretimi crypto.subtle + IndexedDB kullanır; bu API'ler yalnızca güvenli bağlamda (HTTPS veya localhost) çalışır. Zaten üretimde WSS zorunludur — widget güvenli olmayan bir origin'de bağlanamaz.
Anahtar ve kanıt şeffaftır
Bağlantı anahtarı ve DPoP kanıtı tümüyle widget içinde, şeffaf çalışır. Özel girişte backend'inizin tek yapması, getToken'a gelen jkt'yi soket token'ının cnf.jkt'sine gömmektir. logout() yüzeyi ve wf_member_token_<providerId> cache mekanizması aynen kalır.
Üye kaydı
Sosyal girişle (mod 3/4) gelen üyeler sunucuda kalıcı üye kaydı olarak tutulur (chat_customer kaydına auth_provider alanıyla yazılır: google, github…); temsilci panelinde üyenin adı/e-postası ve giriş yöntemi görünür, sonraki girişlerde aynı kayıt güncellenir. Özel girişte (mod 2) üye kimliği token'ınızdaki alanlardan gelir; kayıt, üye ilk kez mesaj/arama yapınca soket tarafında oluşturulur (ad, widget'ın gönderdiği başlıktan yazılır). Her iki durumda da anonim ziyaretçi geçmişi, girişten sonra üye kaydına bağlanmaz — ayrı kayıtlardır.