Çevrimdışı onay
Kullanıcının telefonu internetsiz olsa bile onay verebilir: sayfanızda bir QR gösterirsiniz, telefon bunu çevrimdışı okur, markanızı ve işlem detaylarını gösterir, Face ID ister ve 7 karakterlik bir kod üretir. Kullanıcı kodu sayfanıza yazar, Touchque doğrular. QR’ın içeriği yalnızca kullanıcının telefonu için şifrelenir; başka bir QR okuyucu veya JWT çözücü yalnızca şifreli veri görür. Kod tek bir işleme bağlıdır ve bir kez geçerlidir.
POST/offline/challenge
| Endpoint | Gövde | Sonuç |
|---|---|---|
POST /offline/challenge | externalUsername, type, details?, clientIp?, userAgent?, ttlSeconds? | QR metni (qr), hazır QR görseli (qrDataUrl), challengeId, süre, totpAvailable |
POST /offline/verify | challengeId, code | { approved: true, externalUsername, type } — yanlış kodda 401 + attemptsLeft |
POST /offline/totp/verify | externalUsername, code, type?, clientIp? | Zaman tabanlı kod (düşük güvence). Kritik işlemlerde 403 |
Panel → Developers → App user policy → Offline sekmesinde “Offline sign (QR + code)” anahtarını açın (varsayılan kapalı). Kritik işlemler için ayrıca “Allow offline sign for critical actions” gerekir ve işlem detayı (details) zorunludur. Zaman tabanlı kod aynı sekmede ayrı bir anahtardır, “Offline time-based code (lower assurance)”, ve çevrimdışı onayın açık olmasını ister.
Sınırlar: Kod, telefonun çevrimiçi risk kontrolü olmadan üretilir; sahte bir sayfa gerçek bir QR’ı olduğu gibi gösterebilir (telefon markayı, konumu ve cihazı gösterir). Bu bir MAC tabanlı doğrulamadır: kodu yalnızca Touchque doğrulayabilir. 5 yanlış denemede challenge kilitlenir, kullanıcı başına saatlik başarısız deneme sınırı vardır.
Çevrimdışı zaman tabanlı kod (isteğe bağlı)
Politikada “Offline time-based code” açıksa uygulama, kamera/QR olmadan 30 sn’de bir değişen 7 karakterlik kod gösterir. İşlem detayı taşımaz ve süresi içinde iletilebilir; bu yüzden düşük güvencelidir ve kritik işlemler için asla kabul edilmez. Her kod bir kez kullanılabilir.
Challenge ve doğrulama
const { Touchque } = require('@touchque/node');
const tq = new Touchque({ apiKey: 'tq_abc123', apiSecret: 'YOUR_API_SECRET' });
// 1) Kullanıcı çevrimdışı doğrulamayı seçtiğinde bir meydan okuma oluştur
const challenge = await tq.offline.challenge({
externalUsername: 'user@yourcompany.com',
type: 'WITHDRAW', // Dashboard'daki işlem tipi
details: { Amount: '5.000 TRY', To: 'TR12 0006 …' },
clientIp: req.ip,
userAgent: req.get('user-agent'),
});
// Sayfada göster:
// <img src={challenge.qrDataUrl} /> (veya challenge.qr metnini kendi QR çiziciniz ile çizin)
// 2) Kullanıcı telefondaki 7 karakterlik kodu yazınca doğrula
const result = await tq.offline.verify({ challengeId: challenge.challengeId, code: 'K7M-2QXP' });
if (result.approved) { /* işlemi tamamla */ }
else { /* result.reason, result.attemptsLeft */ }challenge = tq.offline.challenge('user@yourcompany.com', type='WITHDRAW',
details={'Amount': '5.000 TRY'}, client_ip=ip, user_agent=ua)
# challenge['qrDataUrl']'yi gösterin
result = tq.offline.verify(challenge['challengeId'], 'K7M-2QXP')
if result['approved']:
... # işlemi tamamla; değilse result['reason'], result.get('attemptsLeft')$challenge = $tq->offline->challenge('user@yourcompany.com', 'WITHDRAW', ['Amount' => '5.000 TRY'], $request->ip(), $request->userAgent());
// $challenge['qrDataUrl']'yi gösterin
$result = $tq->offline->verify($challenge['challengeId'], 'K7M-2QXP');
if ($result['approved']) { /* işlemi tamamla */ }challenge, err := tq.Offline.Challenge(touchque.OfflineChallengeOptions{
ExternalUsername: "user@yourcompany.com", Type: "WITHDRAW",
Details: []touchque.LoginDetail{{Label: "Amount", Value: "5.000 TRY"}},
ClientIP: ip, UserAgent: r.UserAgent(),
})
// challenge.QRDataURL'yi gösterin
result, err := tq.Offline.Verify(challenge.ChallengeID, "K7M-2QXP")
if err == nil && result.Approved { /* işlemi tamamla */ } // result.Reason, result.AttemptsLeftvar challenge = await tq.Offline.ChallengeAsync(new OfflineChallengeOptions
{
ExternalUsername = "user@yourcompany.com", Type = "WITHDRAW",
Details = [new LoginDetail("Amount", "5.000 TRY")], ClientIp = ip, UserAgent = userAgent,
});
// challenge.QrDataUrl'yi gösterin
var result = await tq.Offline.VerifyAsync(challenge.ChallengeId, "K7M-2QXP");
if (result.Approved) { /* işlemi tamamla */ } // result.Reason, result.AttemptsLeftJsonNode challenge = tq.offline().challenge(new Offline.ChallengeOptions("user@yourcompany.com")
.type("WITHDRAW").details(List.of(LoginDetail.of("Amount", "5.000 TRY"))).clientIp(ip).userAgent(userAgent));
// challenge.path("qrDataUrl")'yi gösterin
JsonNode result = tq.offline().verify(challenge.path("challengeId").asText(), "K7M-2QXP");
if (result.path("approved").asBoolean()) { /* işlemi tamamla */ }challenge = tq.offline.challenge("user@yourcompany.com", type: "WITHDRAW",
details: { "Amount" => "5.000 TRY" }, client_ip: request.remote_ip, user_agent: request.user_agent)
# challenge["qrDataUrl"]'yi gösterin
result = tq.offline.verify(challenge["challengeId"], "K7M-2QXP")
complete_action if result["approved"] # değilse result["reason"], result["attemptsLeft"]# TS, NONCE ve SIG: bkz. Kimlik Doğrulama (her istek kendi nonce'u ve imzasıyla)
curl -X POST https://api.touchque.com/offline/challenge \
-H "x-api-key: tq_abc123" -H "x-timestamp: $TS" -H "x-nonce: $NONCE" -H "x-signature: $SIG" \
-H "Content-Type: application/json" \
-d '{"externalUsername":"user@yourcompany.com","type":"WITHDRAW","details":{"Amount":"5.000 TRY"}}'
curl -X POST https://api.touchque.com/offline/verify \
-H "x-api-key: tq_abc123" -H "x-timestamp: $TS" -H "x-nonce: $NONCE" -H "x-signature: $SIG" \
-H "Content-Type: application/json" \
-d '{"challengeId":"<challengeId>","code":"K7M-2QXP"}'Cevap 200
{
"challengeId": "c0ffee…",
"qr": "TQ2.k1.pKq3…",
"qrDataUrl": "data:image/png;base64,…",
"expiresAt": "2026-09-26T12:02:00.000Z",
"expiresInSeconds": 120,
"totpAvailable": false
}Zaman tabanlı kod
// Düşük güvence: QR/işlem detayı yok. Kritik işlemler için reddedilir.
const result = await tq.offline.verifyTotp({
externalUsername: 'user@yourcompany.com',
code: 'K7M-2QXP',
type: 'LOGIN',
clientIp: req.ip,
});
// result = { approved: true } | { approved: false, reason: 'invalid_code' | 'offline_totp_disabled' | … }result = tq.offline.verify_totp('user@yourcompany.com', 'K7M-2QXP', type='LOGIN', client_ip=ip)$result = $tq->offline->verifyTotp('user@yourcompany.com', 'K7M-2QXP', 'LOGIN', $request->ip());result, err := tq.Offline.VerifyTotp("user@yourcompany.com", "K7M-2QXP", "LOGIN", ip)var result = await tq.Offline.VerifyTotpAsync("user@yourcompany.com", "K7M-2QXP", "LOGIN", ip);JsonNode result = tq.offline().verifyTotp("user@yourcompany.com", "K7M-2QXP", "LOGIN", ip, null);result = tq.offline.verify_totp("user@yourcompany.com", "K7M-2QXP", type: "LOGIN", client_ip: request.remote_ip)