Entegrasyon örnekleri

RESMI_AKIS_V2: Pending Login → challenge → Agent WS (Mode A Browser cert+PIN · Mode B Agent native) → agent-verify → Login Result acceptedLOGIN_SUCCESS. SDK JWT, imza veya sertifika döndürmez.

Tüm örnekleri indir (.zip) Swagger UI CDN demo

Akış

1. Browser → Müşteri backend: Pending Login → correlation_id
2. Browser → api.ksign.tr: POST challenge (Publishable Key + Origin + correlation_id)
   ← agent_ticket + challenge (verify_token YOK, JWT YOK)
3. Browser → Agent: GET /ping → WS LOGIN_START
   Mode A: CERTIFICATE_LIST → SELECT_CERTIFICATE → PIN_SUBMIT
   Mode B: pin_mode=agent — Agent native cert + PIN (Browser cert/PIN görmez)
4. Agent → api: POST …/agent-verify (signature + certificate — Browser taşımaz)
5. api → Müşteri: Login Result (KSign JWT + doğrulanmış kimlik)
6. Müşteri → api: { success, status: "accepted", correlation_id }
7. api → Agent: Login Completed → WS LOGIN_SUCCESS (JWT yok)
8. Müşteri: kendi uygulama oturumu (cookie / session — KSign dışı)

Kategori

RESMI_AKIS_V2 §9 — Mode A (Browser cert+PIN) ve Mode B (Agent native). Müşteri Allowed Origin üzerinde host edin.

Araç

Mode A/B seçici + cdn.ksign.tr — ekstra build yok.

cdn-login.html ham dosya
<!DOCTYPE html>
<html lang="tr">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>KSign — Browser Login Örneği (Mode A / B)</title>
  <style>
    :root { color-scheme: light dark; font-family: "Segoe UI", system-ui, sans-serif; line-height: 1.5; }
    body { max-width: 42rem; margin: 2rem auto; padding: 0 1rem; }
    h1 { font-size: 1.35rem; margin-bottom: 0.25rem; }
    p.sub { color: #666; margin-top: 0; }
    label { display: block; font-weight: 600; margin-top: 1rem; }
    input, button {
      width: 100%; box-sizing: border-box; margin-top: 0.35rem;
      padding: 0.55rem 0.65rem; font: inherit;
    }
    .modes { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin: 1rem 0; }
    .mode {
      text-align: left; padding: 0.65rem 0.75rem; border-radius: 8px;
      border: 1px solid #8884; background: transparent; cursor: pointer;
    }
    .mode.active { border-color: #3d8bfd; background: color-mix(in srgb, #3d8bfd 14%, transparent); }
    .mode strong { display: block; font-size: 0.95rem; }
    .mode small { display: block; color: #666; font-weight: 400; margin-top: 0.2rem; }
    .actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; }
    button { width: auto; min-width: 9rem; cursor: pointer; }
    button:disabled { opacity: 0.55; cursor: not-allowed; }
    .panel {
      display: none; margin-top: 1rem; padding: 0.85rem 1rem;
      border: 1px solid #8884; border-radius: 8px;
    }
    .panel.visible { display: block; }
    .cert-list { display: grid; gap: 0.45rem; margin-top: 0.5rem; }
    .cert {
      text-align: left; padding: 0.55rem 0.65rem; border-radius: 6px;
      border: 1px solid #8884; background: transparent; cursor: pointer; width: 100%;
    }
    .cert .subj { font-weight: 600; }
    .cert .meta { font-size: 0.82rem; color: #666; margin-top: 0.15rem; }
    pre {
      background: #111; color: #eee; padding: 1rem; border-radius: 8px;
      overflow: auto; min-height: 7rem; white-space: pre-wrap; word-break: break-word;
    }
    .ok { color: #0a7; }
    .err { color: #c33; }
    a { color: #3d8bfd; }
    .note {
      margin-top: 1rem; padding: 0.75rem 0.9rem; border-radius: 8px;
      background: color-mix(in srgb, #3d8bfd 12%, transparent);
      font-size: 0.9rem;
    }
    .hint { font-size: 0.88rem; color: #666; margin: 0.35rem 0 0; }
  </style>
</head>
<body>
  <p><a href="/examples/">← Örnekler</a></p>
  <h1>KSign Browser Login</h1>
  <p class="sub">RESMI_AKIS_V2 §9 — Mode A (Browser) veya Mode B (Agent native)</p>

  <div class="note">
    Müşteri <strong>Allowed Origin</strong> üzerinde host edin. SDK JWT döndürmez;
    oturum Login Result <code>accepted</code> sonrası müşteri backend’de oluşur.
  </div>

  <div class="modes" role="group" aria-label="PIN mode">
    <button type="button" class="mode active" id="modeBrowser" aria-pressed="true">
      <strong>Mode A — Browser</strong>
      <small>CERTIFICATE_LIST → seçim → PIN_SUBMIT</small>
    </button>
    <button type="button" class="mode" id="modeAgent" aria-pressed="false">
      <strong>Mode B — Agent</strong>
      <small>Native sertifika + PIN — tarayıcıda PIN yok</small>
    </button>
  </div>

  <label for="backendUrl">KSign API</label>
  <input id="backendUrl" type="url" value="https://api.ksign.tr" />

  <label for="publishableKey">Publishable Key</label>
  <input id="publishableKey" type="text" value="ksign_pk_test_019f49100001700080000000000100000000000000000001" autocomplete="off" />

  <label for="pendingUrl">Pending Login URL (müşteri backend, opsiyonel)</label>
  <input id="pendingUrl" type="url" placeholder="https://app.example/api/esign/pending" />

  <label for="correlationId">correlation_id</label>
  <input id="correlationId" type="text" placeholder="Pending’den veya elle UUID" autocomplete="off" />

  <div id="certPanel" class="panel">
    <strong>Sertifika seçin</strong>
    <p class="hint">Yalnız özet bilgi — DER / private key tarayıcıya gelmez.</p>
    <div id="certList" class="cert-list"></div>
  </div>

  <div id="pinPanel" class="panel">
    <strong id="pinTitle">Token PIN</strong>
    <p class="hint" id="pinHint">PIN yalnızca Agent WebSocket PIN_SUBMIT ile gider.</p>
    <label for="pin">PIN</label>
    <input id="pin" type="password" autocomplete="off" />
    <div class="actions">
      <button id="btnPinSubmit" type="button">PIN gönder</button>
    </div>
  </div>

  <div id="agentPanel" class="panel">
    <strong>Agent penceresi</strong>
    <p class="hint" id="agentHint">
      KSign Agent’ta sertifika ve PIN’i onaylayın. Bu sayfada sertifika listesi ve PIN alanı kullanılmaz.
    </p>
  </div>

  <div class="actions">
    <button id="btnPending" type="button">Pending Login</button>
    <button id="btnDetect" type="button">Detect Agent</button>
    <button id="btnLogin" type="button">Login with e-Sign</button>
  </div>

  <label for="output">Sonuç</label>
  <pre id="output">Hazır.</pre>

  <script src="https://cdn.ksign.tr/browser.min.js"></script>
  <script>
    (function () {
      const $ = (id) => document.getElementById(id);
      const out = $('output');
      let pinMode = 'browser';
      let certResolve = null;
      let pinResolve = null;
      let pinReject = null;

      function log(msg, kind) {
        out.textContent = (kind === 'err' ? '[HATA] ' : kind === 'ok' ? '[OK] ' : '') + msg;
        out.className = kind === 'err' ? 'err' : kind === 'ok' ? 'ok' : '';
      }

      function escapeHtml(s) {
        return String(s)
          .replace(/&/g, '&amp;')
          .replace(/</g, '&lt;')
          .replace(/>/g, '&gt;')
          .replace(/"/g, '&quot;');
      }

      function hidePanels() {
        $('certPanel').classList.remove('visible');
        $('pinPanel').classList.remove('visible');
        $('agentPanel').classList.remove('visible');
        $('certList').innerHTML = '';
        $('pin').value = '';
        certResolve = null;
        if (pinReject) {
          const reject = pinReject;
          pinResolve = null;
          pinReject = null;
          reject(new Error('login cancelled'));
        }
      }

      function setPinMode(mode) {
        pinMode = mode === 'agent' ? 'agent' : 'browser';
        $('modeBrowser').classList.toggle('active', pinMode === 'browser');
        $('modeAgent').classList.toggle('active', pinMode === 'agent');
        $('modeBrowser').setAttribute('aria-pressed', pinMode === 'browser' ? 'true' : 'false');
        $('modeAgent').setAttribute('aria-pressed', pinMode === 'agent' ? 'true' : 'false');
        hidePanels();
      }

      function showCertificates(certificates) {
        return new Promise((resolve, reject) => {
          certResolve = resolve;
          const list = $('certList');
          list.innerHTML = '';
          if (!certificates.length) {
            reject(new Error('Uygun imza sertifikası yok'));
            return;
          }
          certificates.forEach((c) => {
            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'cert';
            btn.innerHTML =
              '<div class="subj">' + escapeHtml(c.subject || c.certificateId) + '</div>' +
              '<div class="meta"><code>' + escapeHtml(c.certificateId) + '</code></div>';
            btn.addEventListener('click', () => {
              list.querySelectorAll('button').forEach((b) => { b.disabled = true; });
              if (certResolve) {
                const r = certResolve;
                certResolve = null;
                r(c.certificateId);
              }
            });
            list.appendChild(btn);
          });
          $('certPanel').classList.add('visible');
          log('CERTIFICATE_LIST — sertifika seçin');
        });
      }

      function requestPin(reason) {
        return new Promise((resolve, reject) => {
          pinResolve = resolve;
          pinReject = reject;
          $('pinPanel').classList.add('visible');
          $('pin').value = '';
          if (reason && reason !== 'pin_required') {
            $('pinTitle').textContent = 'PIN yeniden';
            $('pinHint').textContent = 'PIN reddedildi (' + reason + '). Challenge tüketilmez.';
            log('PIN_RETRY: ' + reason, 'err');
          } else {
            $('pinTitle').textContent = 'Token PIN';
            $('pinHint').textContent = 'PIN yalnızca Agent WS PIN_SUBMIT ile gider.';
          }
          $('pin').focus();
        });
      }

      $('modeBrowser').addEventListener('click', () => setPinMode('browser'));
      $('modeAgent').addEventListener('click', () => setPinMode('agent'));

      $('btnPending').addEventListener('click', async () => {
        try {
          const url = $('pendingUrl').value.trim();
          if (!url) {
            $('correlationId').value = crypto.randomUUID();
            log('Yerel correlation_id üretildi.\n' + $('correlationId').value, 'ok');
            return;
          }
          const res = await fetch(url, {
            method: 'POST',
            headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
            credentials: 'include',
            body: '{}',
          });
          const data = await res.json().catch(() => ({}));
          if (!res.ok) throw new Error(data.message || ('HTTP ' + res.status));
          const id = data.correlation_id || data.correlationId;
          if (!id) throw new Error('correlation_id yok');
          $('correlationId').value = id;
          log('Pending OK\n' + JSON.stringify(data, null, 2), 'ok');
        } catch (e) {
          log(e instanceof Error ? e.message : String(e), 'err');
        }
      });

      $('btnDetect').addEventListener('click', async () => {
        try {
          if (!window.KSignSDK) throw new Error('KSignSDK yüklenemedi');
          const result = await window.KSignSDK.discoverAgent();
          log(
            'Agent bulundu\npingUrl: ' + result.pingUrl +
            '\nagentBaseUrl: ' + result.agentBaseUrl +
            '\nwsLoginUrl: ' + result.wsLoginUrl,
            'ok'
          );
        } catch (e) {
          log(e instanceof Error ? e.message : String(e), 'err');
        }
      });

      $('btnPinSubmit').addEventListener('click', () => {
        const pin = $('pin').value;
        if (!pin) {
          log('PIN boş', 'err');
          return;
        }
        if (pinResolve) {
          const r = pinResolve;
          pinResolve = null;
          pinReject = null;
          $('pinPanel').classList.remove('visible');
          r(pin);
        }
      });

      $('btnLogin').addEventListener('click', async () => {
        $('btnLogin').disabled = true;
        hidePanels();
        try {
          if (!window.KSignSDK) throw new Error('KSignSDK yüklenemedi');
          const publishableKey = $('publishableKey').value.trim();
          const backendUrl = $('backendUrl').value.trim().replace(/\/+$/, '');
          const correlationId = $('correlationId').value.trim() || crypto.randomUUID();
          if (!publishableKey) throw new Error('publishableKey gerekli');
          if (!correlationId) throw new Error('correlation_id gerekli');
          if (!$('correlationId').value.trim()) $('correlationId').value = correlationId;

          const client = new window.KSignSDK.KSign({ publishableKey, backendUrl });
          const loginOptions = {
            correlationId,
            pinMode,
            onEvent: (ev) => {
              console.log('[ksign]', ev.type, ev);
              if (ev.type === 'READY' && pinMode === 'agent') {
                $('agentPanel').classList.add('visible');
                log('READY — Agent native UI bekleniyor');
              } else if (ev.type === 'PIN_ACCEPTED' || ev.type === 'SIGNING' || ev.type === 'QUEUE_WAIT') {
                hidePanels();
              } else if (ev.type === 'PIN_RETRY' && pinMode === 'agent') {
                $('agentPanel').classList.add('visible');
                $('agentHint').textContent =
                  'Yanlış PIN — Agent penceresinde tekrar deneyin (challenge tüketilmez).';
                log('PIN_RETRY (Mode B): ' + (ev.reason || 'wrong_pin'), 'err');
              }
            },
          };

          if (pinMode === 'browser') {
            loginOptions.onSelectCertificate = (certs) => showCertificates(certs);
            loginOptions.onRequestPin = () => requestPin();
            loginOptions.onPinRetry = (reason) => requestPin(reason);
          }

          const result = await client.login(loginOptions);
          hidePanels();
          log(
            'Login tamamlandı (JWT yok)\n' + JSON.stringify(result, null, 2) +
            '\n\nAkış: challenge → WS → agent-verify → Login Result → accepted → LOGIN_SUCCESS' +
            '\nUygulama oturumu: müşteri backend.',
            'ok'
          );
        } catch (e) {
          hidePanels();
          const body = e && e.code
            ? { code: e.code, message: e.message, challengeId: e.challengeId, backendCode: e.backendCode }
            : String(e);
          log('Login başarısız\n' + JSON.stringify(body, null, 2), 'err');
        } finally {
          $('btnLogin').disabled = false;
        }
      });

      log(
        'Origin: ' + location.origin + '\n' +
        'Mode A: CERTIFICATE_LIST → SELECT_CERTIFICATE → PIN_SUBMIT\n' +
        'Mode B: pinMode=agent — Agent native cert + PIN\n' +
        'Kaynak: /examples/browser/login.js'
      );
    })();
  </script>
</body>
</html>

Zip: ./scripts/pack-docs-examples.sh · Deploy: cd update && make update-docs · CDN: cdn.ksign.tr