Entegrasyon örnekleri
v2.3 login: Browser SDK Mode A + müşteri backend Pending Login / Login Result
(accepted). SDK JWT, imza veya sertifika döndürmez.
Akış
Browser → Müşteri: Pending Login → correlation_id
Browser → api.ksign.tr: challenge (Publishable Key + Origin + correlation_id)
Browser ↔ Agent: WebSocket LOGIN_START … LOGIN_SUCCESS
api → Müşteri: Login Result (KSign JWT + user)
Müşteri → api: { success, status: "accepted", correlation_id }
Müşteri: kendi uygulama oturumu (cookie / session JWT)
Kategori
Tarayıcıda Agent keşfi + WebSocket Mode A login. Müşteri origin üzerinde host edin.
Araç
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</title>
<style>
:root { color-scheme: light dark; font-family: "Segoe UI", system-ui, sans-serif; line-height: 1.5; }
body { max-width: 40rem; 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;
}
.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; }
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;
}
</style>
</head>
<body>
<p><a href="/examples/">← Örnekler</a></p>
<h1>KSign Browser Login</h1>
<p class="sub">v2.3 WebSocket Mode A — CDN: <code>cdn.ksign.tr</code></p>
<div class="note">
Bu sayfa referans içindir. Tam login için sayfayı <strong>müşteri origin</strong>
(Allowed Origin) üzerinde host edin. PIN yalnızca yerel Agent’a gider; SDK JWT döndürmez.
</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" />
<label for="pin">Token PIN</label>
<input id="pin" type="password" autocomplete="off" placeholder="USB token PIN" />
<label for="certificateId">Certificate thumbprint (opsiyonel)</label>
<input id="certificateId" type="text" placeholder="SHA-256 — boşsa tek sertifika seçilir" />
<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');
function log(msg, kind) {
out.textContent = (kind === 'err' ? '[HATA] ' : kind === 'ok' ? '[OK] ' : '') + msg;
out.className = kind === 'err' ? 'err' : kind === 'ok' ? 'ok' : '';
}
$('btnPending').addEventListener('click', async () => {
try {
const url = $('pendingUrl').value.trim();
if (!url) {
$('correlationId').value = crypto.randomUUID();
log('Yerel correlation_id üretildi (Pending URL yok).\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');
}
});
$('btnLogin').addEventListener('click', async () => {
$('btnLogin').disabled = true;
try {
if (!window.KSignSDK) throw new Error('KSignSDK yüklenemedi');
const publishableKey = $('publishableKey').value.trim();
const backendUrl = $('backendUrl').value.trim().replace(/\/+$/, '');
const pin = $('pin').value;
const correlationId = $('correlationId').value.trim() || crypto.randomUUID();
if (!publishableKey) throw new Error('publishableKey gerekli');
if (!pin) throw new Error('PIN gerekli');
const client = new window.KSignSDK.KSign({ publishableKey, backendUrl });
const options = {
pin,
correlationId,
onEvent: (ev) => console.log('[ksign]', ev.type, ev),
};
const cert = $('certificateId').value.trim();
if (cert) options.certificateId = cert;
const result = await client.login(options);
log(
'Login tamamlandı (JWT yok)\n' + JSON.stringify(result, null, 2) +
'\n\nUygulama oturumu: müşteri backend Login Result accepted sonrası.',
'ok'
);
} catch (e) {
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(
'Sayfa origin: ' + location.origin + '\n' +
'1) Pending Login → correlation_id\n' +
'2) Detect Agent → /ping\n' +
'3) Login → challenge + WS … LOGIN_SUCCESS\n' +
'Kaynak: /examples/browser/login.js'
);
})();
</script>
</body>
</html>
SDK orchestration helper.
login.js ham dosya
/**
* KSign Browser SDK v2.3 — Mode A login orchestration
*
* Prerequisites:
* <script src="https://cdn.ksign.tr/browser.min.js"></script>
*
* Does NOT return JWT, signature, or certificate.
* Application session is owned by the customer backend after Login Result `accepted`.
*/
/**
* @param {object} opts
* @param {string} opts.publishableKey
* @param {string} [opts.backendUrl=https://api.ksign.tr]
* @param {string} opts.pin — USB token PIN (never sent to KSign api)
* @param {string} opts.correlationId — from customer Pending Login
* @param {string} [opts.certificateId] — SHA-256 thumbprint
* @param {(event: { type: string }) => void} [opts.onEvent]
*/
export async function loginWithESign(opts) {
if (!globalThis.KSignSDK?.KSign) {
throw new Error('KSignSDK yüklenemedi — cdn.ksign.tr/browser.min.js');
}
if (!opts?.publishableKey) throw new Error('publishableKey gerekli');
if (!opts?.correlationId) throw new Error('correlationId gerekli (Pending Login)');
if (!opts?.pin) throw new Error('PIN gerekli');
const client = new globalThis.KSignSDK.KSign({
publishableKey: opts.publishableKey,
backendUrl: (opts.backendUrl || 'https://api.ksign.tr').replace(/\/+$/, ''),
});
const result = await client.login({
pin: opts.pin,
correlationId: opts.correlationId,
certificateId: opts.certificateId || undefined,
onEvent: opts.onEvent,
});
// { success: true, challengeId: '...', status: 'login_completed' }
return result;
}
/**
* Optional: ask customer backend for a fresh correlation_id.
* @param {string} pendingUrl e.g. https://app.example/api/esign/pending
*/
export async function startPendingLogin(pendingUrl) {
const res = await fetch(pendingUrl, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
credentials: 'include',
body: '{}',
});
if (!res.ok) {
throw new Error('Pending Login başarısız: HTTP ' + res.status);
}
const data = await res.json();
const correlationId = data.correlation_id || data.correlationId;
if (!correlationId) {
throw new Error('Pending Login yanıtında correlation_id yok');
}
return { correlationId, ...data };
}
Svelte 5 runes bileşen + SvelteKit Pending / Login Result endpoint’leri. npm install @ksign/browser
Araç
ESignLogin.svelte ham dosya
<script lang="ts">
/**
* KSign Browser SDK v2.3 — Svelte 5 (runes) login örneği
*
* npm: npm install @ksign/browser
* Host: müşteri Allowed Origin üzerinde çalıştırın.
*
* SDK JWT / signature / certificate döndürmez.
* Uygulama oturumu: müşteri backend Login Result `accepted` sonrası.
*/
import { KSign, discoverAgent } from '@ksign/browser';
import type { LoginResult, LoginSessionEvent } from '@ksign/browser';
interface Props {
publishableKey: string;
backendUrl?: string;
/** Müşteri Pending Login endpoint (opsiyonel) */
pendingUrl?: string;
}
let {
publishableKey,
backendUrl = 'https://api.ksign.tr',
pendingUrl = ''
}: Props = $props();
let pin = $state('');
let correlationId = $state('');
let certificateId = $state('');
let busy = $state(false);
let logText = $state('Hazır.');
let logKind = $state<'info' | 'ok' | 'err'>('info');
let lastEvent = $state('');
let loginResult = $state<LoginResult | null>(null);
function setLog(message: string, kind: 'info' | 'ok' | 'err' = 'info') {
logText = message;
logKind = kind;
}
async function startPending() {
busy = true;
loginResult = null;
try {
if (!pendingUrl.trim()) {
correlationId = crypto.randomUUID();
setLog('Yerel correlation_id üretildi.\n' + correlationId, 'ok');
return;
}
const res = await fetch(pendingUrl.trim(), {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
credentials: 'include',
body: '{}'
});
const data = (await res.json().catch(() => ({}))) as {
correlation_id?: string;
correlationId?: string;
message?: string;
};
if (!res.ok) throw new Error(data.message || `HTTP ${res.status}`);
const id = data.correlation_id || data.correlationId;
if (!id) throw new Error('Pending yanıtında correlation_id yok');
correlationId = id;
setLog('Pending OK\n' + JSON.stringify(data, null, 2), 'ok');
} catch (e) {
setLog(e instanceof Error ? e.message : String(e), 'err');
} finally {
busy = false;
}
}
async function detectAgent() {
busy = true;
try {
const result = await discoverAgent();
setLog(
'Agent bulundu\n' +
`pingUrl: ${result.pingUrl}\n` +
`agentBaseUrl: ${result.agentBaseUrl}\n` +
`wsLoginUrl: ${result.wsLoginUrl}`,
'ok'
);
} catch (e) {
setLog(e instanceof Error ? e.message : String(e), 'err');
} finally {
busy = false;
}
}
async function login() {
busy = true;
loginResult = null;
lastEvent = '';
try {
if (!publishableKey.trim()) throw new Error('publishableKey gerekli');
if (!pin) throw new Error('PIN gerekli');
const corr = correlationId.trim() || crypto.randomUUID();
if (!correlationId.trim()) correlationId = corr;
const client = new KSign({
publishableKey: publishableKey.trim(),
backendUrl: backendUrl.replace(/\/+$/, '')
});
const result = await client.login({
pin,
correlationId: corr,
certificateId: certificateId.trim() || undefined,
onEvent: (event: LoginSessionEvent) => {
lastEvent = event.type;
console.log('[ksign]', event.type, event);
}
});
loginResult = result;
pin = '';
setLog(
'Login tamamlandı (JWT yok)\n' +
JSON.stringify(result, null, 2) +
'\n\nOturum: müşteri backend Login Result accepted sonrası.',
'ok'
);
} catch (e) {
const err = e as { code?: string; message?: string; challengeId?: string; backendCode?: number };
const body =
err && err.code
? {
code: err.code,
message: err.message,
challengeId: err.challengeId,
backendCode: err.backendCode
}
: e instanceof Error
? e.message
: String(e);
setLog('Login başarısız\n' + JSON.stringify(body, null, 2), 'err');
} finally {
busy = false;
}
}
</script>
<section class="esign">
<label>
correlation_id
<input bind:value={correlationId} placeholder="Pending’den veya UUID" autocomplete="off" />
</label>
<label>
Token PIN
<input bind:value={pin} type="password" autocomplete="off" placeholder="USB token PIN" />
</label>
<label>
Certificate thumbprint (opsiyonel)
<input bind:value={certificateId} placeholder="SHA-256" autocomplete="off" />
</label>
<div class="actions">
<button type="button" disabled={busy} onclick={startPending}>Pending Login</button>
<button type="button" disabled={busy} onclick={detectAgent}>Detect Agent</button>
<button type="button" disabled={busy} onclick={login}>Login with e-Sign</button>
</div>
{#if lastEvent}
<p class="event">Son WS olayı: <code>{lastEvent}</code></p>
{/if}
{#if loginResult}
<p class="ok">status: <code>{loginResult.status}</code> · challenge: <code>{loginResult.challengeId}</code></p>
{/if}
<pre class:ok={logKind === 'ok'} class:err={logKind === 'err'}>{logText}</pre>
</section>
<style>
.esign {
display: grid;
gap: 0.85rem;
max-width: 28rem;
}
label {
display: grid;
gap: 0.35rem;
font-weight: 600;
font-size: 0.9rem;
}
input {
font: inherit;
padding: 0.5rem 0.65rem;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
button {
font: inherit;
cursor: pointer;
padding: 0.5rem 0.85rem;
}
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
pre {
margin: 0;
padding: 0.85rem;
border-radius: 8px;
background: #111;
color: #eee;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.85rem;
}
pre.ok {
color: #6dcfb0;
}
pre.err {
color: #f07178;
}
.event,
.ok {
margin: 0;
font-size: 0.9rem;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.88em;
}
</style>
+page.svelte ham dosya
<script lang="ts">
/**
* SvelteKit sayfa örneği — müşteri uygulamasında:
*
* src/routes/login/+page.svelte
*
* PUBLIC_KSIGN_PUBLISHABLE_KEY ve PUBLIC_KSIGN_API_URL .env içinden gelir.
* Secret Key asla $env/static/public veya istemci koduna konmaz.
*/
import { env } from '$env/dynamic/public';
import ESignLogin from '$lib/ksign/ESignLogin.svelte';
const publishableKey = env.PUBLIC_KSIGN_PUBLISHABLE_KEY ?? '';
const backendUrl = env.PUBLIC_KSIGN_API_URL ?? 'https://api.ksign.tr';
const pendingUrl = '/api/esign/pending';
</script>
<svelte:head>
<title>e-İmza ile giriş</title>
</svelte:head>
<main>
<h1>e-İmza ile giriş</h1>
<p>
PIN yalnızca yerel KSign Agent’a gider. KSign JWT tarayıcıya gelmez; oturum sunucunuzda
Login Result sonrası oluşur.
</p>
{#if !publishableKey}
<p class="warn">PUBLIC_KSIGN_PUBLISHABLE_KEY tanımlı değil.</p>
{:else}
<ESignLogin {publishableKey} {backendUrl} {pendingUrl} />
{/if}
</main>
<style>
main {
max-width: 36rem;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
}
.warn {
color: #b45309;
}
</style>
README.md ham dosya
# Svelte / SvelteKit örnekleri (v2.3)
| Dosya | Yerleşim (müşteri projesi) |
|-------|----------------------------|
| `ESignLogin.svelte` | `src/lib/ksign/ESignLogin.svelte` |
| `+page.svelte` | `src/routes/login/+page.svelte` |
| `server/pending/+server.ts` | `src/routes/api/esign/pending/+server.ts` |
| `server/login-result/+server.ts` | `src/routes/api/ksign/login-result/+server.ts` |
```bash
npm install @ksign/browser
```
Env (örnek):
```env
PUBLIC_KSIGN_PUBLISHABLE_KEY=ksign_pk_...
PUBLIC_KSIGN_API_URL=https://api.ksign.tr
KSIGN_CALLBACK_SECRET=... # yalnız sunucu; X-KSign-Secret-Key
```
Secret Key / callback secret asla `PUBLIC_*` altına konmaz.
Pending Login + Login Result (müşteri sunucusu).
pending/+server.ts ham dosya
/**
* SvelteKit — Pending Login
*
* Yerleşim: src/routes/api/esign/pending/+server.ts
*
* Browser POST → correlation_id; challenge create body’de api.ksign.tr’ye gider.
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
export const POST: RequestHandler = async () => {
const correlationId = crypto.randomUUID();
const expiresAt = Math.floor(Date.now() / 1000) + 300;
// TODO: Redis / DB — pending kaydı (session ipucu ile bağlayın)
return json({
success: true,
correlation_id: correlationId,
expires_at: expiresAt
});
};
login-result/+server.ts ham dosya
/**
* SvelteKit — Login Result callback (müşteri backend)
*
* Yerleşim: src/routes/api/ksign/login-result/+server.ts
* Portal’da login_result_url = https://app.example/api/ksign/login-result
*
* Auth: X-KSign-Secret-Key
* Zorunlu yanıt: { success: true, status: "accepted", correlation_id }
*
* access_token yalnızca burada — Browser’a KSign protokolü ile verilmez.
*/
import { createHash, timingSafeEqual } from 'node:crypto';
import { env } from '$env/dynamic/private';
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
type LoginResultBody = {
correlation_id?: string;
challenge_id?: string;
access_token?: string;
expires_at?: number;
user?: {
user_uuid?: string;
customer_uuid?: string;
email?: string;
full_name?: string;
roles?: string[];
};
verification?: { valid?: boolean; certificate_sha256?: string };
};
function secretMatches(expected: string, got: string | null): boolean {
if (!expected || !got) return false;
const a = createHash('sha256').update(expected).digest();
const b = createHash('sha256').update(got).digest();
return a.length === b.length && timingSafeEqual(a, b);
}
export const POST: RequestHandler = async ({ request }) => {
const expected = env.KSIGN_CALLBACK_SECRET ?? '';
if (!expected) {
throw error(500, 'callback secret not configured');
}
const got = request.headers.get('X-KSign-Secret-Key');
if (!secretMatches(expected, got)) {
throw error(401, 'unauthorized');
}
let body: LoginResultBody;
try {
body = (await request.json()) as LoginResultBody;
} catch {
throw error(400, 'invalid json');
}
const correlationId = String(body.correlation_id ?? '');
const challengeId = String(body.challenge_id ?? '');
const accessToken = String(body.access_token ?? '');
if (!correlationId || !challengeId || !accessToken) {
throw error(400, 'missing required fields');
}
if (body.verification?.valid !== true) {
throw error(400, 'verification not valid');
}
// TODO: pending kaydını correlation_id ile doğrula
// TODO: KSign JWT’yi JWKS ile doğrula; uygulama oturumu oluştur (cookie / Redis)
return json({
success: true,
status: 'accepted',
correlation_id: correlationId
});
};
Login Result callback: X-KSign-Secret-Key → { success, status: "accepted", correlation_id }. JWT yalnız burada.
Araç
pending-login.php ham dosya
<?php
/**
* KSign müşteri backend — Pending Login örneği (PHP)
*
* Browser bu endpoint’e POST atar; correlation_id alır.
* correlation_id challenge create body’de api.ksign.tr’ye gider.
*
* Route örneği: POST /api/esign/pending
*
* Güvenlik: oturum/CSRF kendi uygulamanıza göre ekleyin.
* Bu dosya production-ready iskelettir; store’u kendi altyapınıza bağlayın.
*/
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method not allowed'], JSON_UNESCAPED_UNICODE);
exit;
}
/**
* @return array{correlation_id: string, expires_at: int}
*/
function createPendingLogin(): array
{
$correlationId = sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0x0fff) | 0x4000,
random_int(0, 0x3fff) | 0x8000,
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff)
);
$expiresAt = time() + 300; // 5 dk — kendi TTL politikanız
// TODO: Redis / DB — pending kaydı
// pendingStore()->put($correlationId, [
// 'status' => 'pending',
// 'created_at' => time(),
// 'expires_at' => $expiresAt,
// 'session_hint' => session_id(),
// ]);
return [
'correlation_id' => $correlationId,
'expires_at' => $expiresAt,
];
}
$pending = createPendingLogin();
http_response_code(200);
echo json_encode([
'success' => true,
'correlation_id' => $pending['correlation_id'],
'expires_at' => $pending['expires_at'],
], JSON_UNESCAPED_UNICODE);
login-result.php ham dosya
<?php
/**
* KSign müşteri backend — Login Result callback (PHP)
*
* KSign api → bu URL (customer_integrations.login_result_url)
* Header: X-KSign-Secret-Key: <callback secret / Secret Key>
*
* Zorunlu yanıt (HTTP 2xx + body):
* { "success": true, "status": "accepted", "correlation_id": "<aynı>" }
*
* access_token yalnızca burada gelir — Browser/Agent’a verilmez.
* Uygulama oturumunu (cookie / kendi JWT) burada oluşturursunuz.
*
* Route örneği: POST /api/ksign/login-result
*/
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method not allowed'], JSON_UNESCAPED_UNICODE);
exit;
}
/** Ortam değişkeninden okuyun — asla repoya yazmayın. */
$expectedSecret = getenv('KSIGN_CALLBACK_SECRET') ?: '';
if ($expectedSecret === '') {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'callback secret not configured'], JSON_UNESCAPED_UNICODE);
exit;
}
$gotSecret = $_SERVER['HTTP_X_KSIGN_SECRET_KEY'] ?? '';
if ($gotSecret === '' || !hash_equals($expectedSecret, $gotSecret)) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'unauthorized'], JSON_UNESCAPED_UNICODE);
exit;
}
$raw = file_get_contents('php://input');
$payload = json_decode($raw ?: 'null', true);
if (!is_array($payload)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid json'], JSON_UNESCAPED_UNICODE);
exit;
}
$correlationId = isset($payload['correlation_id']) ? (string) $payload['correlation_id'] : '';
$challengeId = isset($payload['challenge_id']) ? (string) $payload['challenge_id'] : '';
$accessToken = isset($payload['access_token']) ? (string) $payload['access_token'] : '';
$user = isset($payload['user']) && is_array($payload['user']) ? $payload['user'] : [];
$verification = isset($payload['verification']) && is_array($payload['verification']) ? $payload['verification'] : [];
if ($correlationId === '' || $challengeId === '' || $accessToken === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing required fields'], JSON_UNESCAPED_UNICODE);
exit;
}
if (($verification['valid'] ?? false) !== true) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'verification not valid'], JSON_UNESCAPED_UNICODE);
exit;
}
// TODO: pending kaydını correlation_id ile bulun; yoksa / süresi dolmuşsa 404/410
// $pending = pendingStore()->take($correlationId);
// if ($pending === null) { http_response_code(404); ... }
// TODO: KSign JWT’yi doğrulayın (JWKS: https://api.ksign.tr/.well-known/jwks.json)
// ve kendi uygulama oturumunuzu oluşturun (cookie / Redis / …).
// KSign JWT’yi Browser’a aynen vermek zorunlu değildir — oturum size aittir.
// applicationSession()->createFromEsign([
// 'correlation_id' => $correlationId,
// 'challenge_id' => $challengeId,
// 'user_uuid' => $user['user_uuid'] ?? null,
// 'customer_uuid' => $user['customer_uuid'] ?? null,
// 'email' => $user['email'] ?? null,
// 'ksign_access_token' => $accessToken,
// 'expires_at' => $payload['expires_at'] ?? null,
// ]);
http_response_code(200);
echo json_encode([
'success' => true,
'status' => 'accepted',
'correlation_id' => $correlationId,
], JSON_UNESCAPED_UNICODE);
pending_login.go ham dosya
// Package example — KSign müşteri backend Pending Login (Go)
//
// Route: POST /api/esign/pending
// Browser correlation_id alır; challenge create body'de api'ye gönderir.
package example
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"time"
)
type pendingResponse struct {
Success bool `json:"success"`
CorrelationID string `json:"correlation_id"`
ExpiresAt int64 `json:"expires_at"`
}
// PendingLoginHandler starts a pending e-sign login for the browser.
func PendingLoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"success":false,"message":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
http.Error(w, `{"success":false,"message":"internal error"}`, http.StatusInternalServerError)
return
}
// UUID-like opaque id (v4-ish); production: use uuid.NewString()
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
correlationID := hex.EncodeToString(b[0:4]) + "-" +
hex.EncodeToString(b[4:6]) + "-" +
hex.EncodeToString(b[6:8]) + "-" +
hex.EncodeToString(b[8:10]) + "-" +
hex.EncodeToString(b[10:16])
expiresAt := time.Now().UTC().Add(5 * time.Minute).Unix()
// TODO: persist pending { correlation_id, status, expires_at } in Redis/DB
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(pendingResponse{
Success: true,
CorrelationID: correlationID,
ExpiresAt: expiresAt,
})
}
login_result.go ham dosya
// Package example — KSign müşteri backend Login Result callback (Go)
//
// KSign api POSTs here with header X-KSign-Secret-Key.
// Required ack body:
//
// {"success":true,"status":"accepted","correlation_id":"<same>"}
//
// Route: POST /api/ksign/login-result
package example
import (
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"io"
"net/http"
"os"
)
const maxLoginResultBody = 1 << 20 // 1 MiB
type loginResultRequest struct {
CorrelationID string `json:"correlation_id"`
ChallengeID string `json:"challenge_id"`
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresAt int64 `json:"expires_at"`
User struct {
UserUUID string `json:"user_uuid"`
CustomerUUID string `json:"customer_uuid"`
Email string `json:"email"`
FullName string `json:"full_name"`
Roles []string `json:"roles"`
} `json:"user"`
Verification struct {
Valid bool `json:"valid"`
CertificateSHA256 string `json:"certificate_sha256"`
} `json:"verification"`
}
type acceptedResponse struct {
Success bool `json:"success"`
Status string `json:"status"`
CorrelationID string `json:"correlation_id"`
}
// LoginResultHandler acknowledges KSign Login Result and creates the app session.
func LoginResultHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"success":false,"message":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
expected := os.Getenv("KSIGN_CALLBACK_SECRET")
if expected == "" {
http.Error(w, `{"success":false,"message":"callback secret not configured"}`, http.StatusInternalServerError)
return
}
got := r.Header.Get("X-KSign-Secret-Key")
if !secureEqual(expected, got) {
http.Error(w, `{"success":false,"message":"unauthorized"}`, http.StatusUnauthorized)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, maxLoginResultBody))
if err != nil {
http.Error(w, `{"success":false,"message":"read failed"}`, http.StatusBadRequest)
return
}
var req loginResultRequest
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, `{"success":false,"message":"invalid json"}`, http.StatusBadRequest)
return
}
if req.CorrelationID == "" || req.ChallengeID == "" || req.AccessToken == "" {
http.Error(w, `{"success":false,"message":"missing required fields"}`, http.StatusBadRequest)
return
}
if !req.Verification.Valid {
http.Error(w, `{"success":false,"message":"verification not valid"}`, http.StatusBadRequest)
return
}
// TODO: resolve pending by correlation_id; reject unknown/expired
// TODO: verify KSign JWT via JWKS; create application session (cookie / Redis / …)
// Do not rely on Browser receiving this JWT — it is customer-backend only.
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(acceptedResponse{
Success: true,
Status: "accepted",
CorrelationID: req.CorrelationID,
})
}
func secureEqual(a, b string) bool {
ha := sha256.Sum256([]byte(a))
hb := sha256.Sum256([]byte(b))
return subtle.ConstantTimeCompare(ha[:], hb[:]) == 1
}
server.mjs ham dosya
/**
* KSign müşteri backend — Pending Login + Login Result (Node.js)
*
* Env:
* KSIGN_CALLBACK_SECRET — X-KSign-Secret-Key ile karşılaştırılır
* PORT — default 3100
*
* Endpoints:
* POST /api/esign/pending
* POST /api/ksign/login-result
*
* Çalıştırma: node server.mjs
*/
import http from 'node:http';
import { randomUUID, createHash, timingSafeEqual } from 'node:crypto';
const PORT = Number(process.env.PORT || 3100);
const CALLBACK_SECRET = process.env.KSIGN_CALLBACK_SECRET || '';
/** @type {Map<string, { expiresAt: number }>} */
const pending = new Map();
function json(res, status, body) {
const raw = JSON.stringify(body);
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(raw),
});
res.end(raw);
}
function readBody(req, limit = 1 << 20) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
req.on('data', (c) => {
size += c.length;
if (size > limit) {
reject(new Error('body too large'));
req.destroy();
return;
}
chunks.push(c);
});
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
function secretOk(got) {
if (!CALLBACK_SECRET || !got) return false;
const a = createHash('sha256').update(CALLBACK_SECRET).digest();
const b = createHash('sha256').update(String(got)).digest();
return a.length === b.length && timingSafeEqual(a, b);
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
if (req.method === 'POST' && url.pathname === '/api/esign/pending') {
const correlationId = randomUUID();
const expiresAt = Math.floor(Date.now() / 1000) + 300;
pending.set(correlationId, { expiresAt });
return json(res, 200, {
success: true,
correlation_id: correlationId,
expires_at: expiresAt,
});
}
if (req.method === 'POST' && url.pathname === '/api/ksign/login-result') {
if (!CALLBACK_SECRET) {
return json(res, 500, { success: false, message: 'callback secret not configured' });
}
if (!secretOk(req.headers['x-ksign-secret-key'])) {
return json(res, 401, { success: false, message: 'unauthorized' });
}
let payload;
try {
payload = JSON.parse((await readBody(req)).toString('utf8'));
} catch {
return json(res, 400, { success: false, message: 'invalid json' });
}
const correlationId = String(payload.correlation_id || '');
const challengeId = String(payload.challenge_id || '');
const accessToken = String(payload.access_token || '');
const valid = payload.verification?.valid === true;
if (!correlationId || !challengeId || !accessToken) {
return json(res, 400, { success: false, message: 'missing required fields' });
}
if (!valid) {
return json(res, 400, { success: false, message: 'verification not valid' });
}
const entry = pending.get(correlationId);
if (!entry || entry.expiresAt < Math.floor(Date.now() / 1000)) {
// Production: unknown correlation may be 404; demo store is in-memory only.
// Still accept if you correlate via durable store — adjust as needed.
}
pending.delete(correlationId);
// TODO: verify KSign JWT (JWKS); create application session for payload.user
// access_token is customer-backend only — do not expose to Browser as KSign protocol.
return json(res, 200, {
success: true,
status: 'accepted',
correlation_id: correlationId,
});
}
json(res, 404, { success: false, message: 'not found' });
});
server.listen(PORT, () => {
console.log(`KSign customer example listening on :${PORT}`);
console.log(' POST /api/esign/pending');
console.log(' POST /api/ksign/login-result');
});