Sessiz giriş
Bir girişte yeni hiçbir şey yokken telefona tekrar tekrar sormayın. Tarayıcı cihaza bağlı bir anahtar tutar ve kararı Touchque risk motoru verir: telefona yalnızca yeni bir şey olduğunda sorulur (yeni tarayıcı, telefon, ülke ya da ağ, kritik işlem, herhangi bir risk sinyali). Sunucunuz push'u yalnızca Touchque öyle dediğinde atlar.
| Adım | Ne olur |
|---|---|
| Tarayıcı | createTouchqueWeb({ browserKey: true }) (@touchque/web 2.2.0) siteniz için WebCrypto ile dışa aktarılamayan bir ECDSA P-256 anahtarı üretip IndexedDB'de tutar ve her istekte taze, imzalı kanıtını X-Touchque-Browser başlığında gönderir. Kod anahtarı kullanabilir, okuyamaz. |
| İlk giriş | Sunucunuz push'u kanıtla birlikte gönderir (login.request({ …, browserProof }); guard bunu sizin için yapar). Kullanıcı telefonda onaylayınca Touchque tarayıcıyı tanır. |
| Sonraki girişler | Taze kanıtla POST /risk/check (tq.risk.check). prompt: false: yeni bir şey yok, kullanıcıyı push olmadan içeri alın. Diğer her durumda: her zamanki push. |
/login/request ile aynı API anahtarı ve kota. Telefona hiçbir şey göndermez. Yanıt: 200 { prompt, reasons: [{ code, text }], assurance: "high" | "normal" | "low", silentMode: "enforce" | "shadow", caseCode: "K7P-2QX" }. caseCode bu yanıtın "Neden?" vaka kodudur: telefona sorulduğunda gösterin; ekibiniz kararı bu panelde Logs → Look up a case altında açar.
| Alan | Tip | Açıklama |
|---|---|---|
externalUsername | string | Kullanıcı ID Zorunlu |
type | string | İşlem (varsayılan "LOGIN"). Kritik bir işlem her zaman sorar. |
clientIp | string | Son kullanıcının genel IP adresi; önceki onaylı girişlerinin ağlarıyla karşılaştırılır. Gönderilmezse telefona sorulur. |
userAgent | string | Son kullanıcının tarayıcısı |
browserProof | string | X-Touchque-Browser değeri, olduğu gibi iletilir. Eksik ya da geçersiz olması hata değildir: tanınmayan tarayıcı sayılır. |
maxAgeSeconds | integer ≥ 0 | Bu tarayıcıdaki son telefon onayının yaşı için kendi sınırınız. 0 = her zaman sor. Touchque hiçbir zaman 7 günü aşmaz. |
prompt: false (boolean) olduğunda atlayın. Hata, zaman aşımı ya da okunamayan yanıt: her zamanki gibi push gönderin. SDK'lar yanıtı bu şekilde okur ve guard'ları hata durumunda asla onay vermez.- Bir tarayıcıdaki ilk giriş ve geçerli anahtar kanıtı olmayan her istek (gizli pencere, başka tarayıcı, browserKey kullanmayan sayfa)
- Kritik işlemler ve önemli bir şeyi değiştiren işlemler
- Yeni bir ülke ya da ağ, barındırma / VPN / Tor ağı ya da karşılaştırılacak clientIp olmaması
- Tarayıcıyı onaylayandan farklı bir telefon ya da maxAgeSeconds veya 7 günden eski son telefon onayı
- Herhangi bir risk sinyali: yakın zamanda yanıtsız ya da reddedilmiş push, sahtekârlık ya da "Bu ben değildim" bildirimi, son 24 saatte değişen giriş yöntemi ya da parola
- Vishing Shield: arama sırasında verilip yeniden onaylanmamış bir onay (12 saat) ya da geri alınan veya yönlendirildiği için reddedilen bir onay (7 gün)
- Dondurulmuş hesap, anahtarlarını kaybeden telefon, yalnız passkey ya da her zaman sayı eşleştirme politikası, bekleyen ek doğrulama, erişim ya da ülke kuralı
- Çalışamayan herhangi bir kontrol ve ölçüm (shadow) modu
shadow modunda (silentMode) prompt her zaman true olur ve reasons ne olacağını söyler.| code | Anlam |
|---|---|
known_browser | Bu tarayıcı cihaza bağlı anahtarını kanıtladı |
recent_approval | Bu tarayıcıda süre sınırı içinde telefonla onay verildi |
same_phone | Şu an bağlı olan telefon, bu tarayıcıyı onaylayan telefon |
familiar_network | Ülke ve ağ daha önceki onaylı girişlerden tanıdık |
no_risk_signal | Risk sinyali yok |
| code | Anlam |
|---|---|
no_browser_key | Geçerli tarayıcı anahtarı kanıtı yok: gizli pencere, başka tarayıcı, browserKey kapalı ya da eski veya tekrar kullanılmış kanıt |
new_browser | Bu tarayıcı bu hesap için daha önce telefonla onaylanmadı |
approval_too_old | Bu tarayıcıdaki son telefon onayı sınırdan eski (maxAgeSeconds, en fazla 7 gün) |
policy_every_sign_in | maxAgeSeconds 0: her girişte sorulur |
other_phone | Onaylayacak telefon, bu tarayıcıyı onaylayan telefon değil |
new_country | Bu ülkeden ilk onaylı giriş |
new_network | Bu ağdan ilk onaylı giriş |
network_unknown | Ülke ya da ağ önceki girişlerle karşılaştırılamadı (clientIp gönderin) |
hosting_network | Barındırma, VPN ya da Tor ağından giriş |
critical_action | İşlem kritik |
sensitive_action | İşlem önemli bir şeyi değiştiriyor |
risk_signal | Bir risk sinyali (text içinde yazar) |
push_fatigue | Son zamanlarda yanıtsız ya da reddedilmiş onaylar var |
fraud_report | Yakın zamanda "Bu ben değildim" ya da sahtekârlık bildirimi |
new_factor | Son 24 saatte bir giriş yöntemi ya da parola değişti |
vishing_shield | Yakın bir onay sırasında arama ya da yönlendirme algılandı |
checks_incomplete | Kontrollerin hepsi çalışamadı |
not_a_sign_in | Yalnızca bir girişin ilk değerlendirmesi sessiz olabilir |
silent_mode_shadow | Sessiz giriş henüz ölçüm modunda (shadow) |
phone_not_linked | Bu hesaba bağlı telefon yok |
frozen | Bu hesabın onayları dondurulmuş |
phone_keys_lost | Bağlı telefon onay anahtarlarını kaybetti |
passkey_required_by_policy | Politikanız bu işlem için passkey istiyor |
policy_always_challenge | Politikanız her seferinde sayı eşleştirmeli onay istiyor |
step_up_pending | Önceki bir onay ek doğrulama istedi |
access_policy | Erişim ya da ülke kurallarınızdan biri uygulanıyor |
engine_unavailable | Risk motoruna sorulamadı |
text İngilizcedir; gösterin ya da kodu çevirin. Küçük sürümlerde yeni kodlar eklenebilir: bilinmeyen bir kodu "soruldu" sayın.
| Endpoint | Sonuç |
|---|---|
GET /risk/browsers?externalUsername= | { browsers: [{ thumbprint, label, createdAt, lastMfaAt, lastMfaMethod, lastSeenAt, lastSilentAt }] } |
POST /risk/browsers/forget | { externalUsername, thumbprint? } → { forgotten: n }. thumbprint verilmezse hepsi unutulur: parola sıfırlamadan ya da "her yerden çıkış"tan sonra yapın. |
| SDK | Seçenek | Nerede |
|---|---|---|
| Node | silentSignIn: true | { maxAgeSeconds } | requireTouchque, withTouchque, touchqueRouter, runGuard |
| Python | silent_sign_in=True | {'max_age_seconds': n} | require_touchque (Flask, Django), RequireTouchque (FastAPI), relay'ler, run_guard |
| PHP | 'silentSignIn' => true | ['maxAgeSeconds' => n] | TouchqueMiddleware, TouchqueRelay, RelayHandler, Guard::run |
| Go | SilentSignIn: true, SilentSignInMaxAgeSeconds | RequireOptions, RelayOptions, GuardInput |
| .NET | SilentSignIn = new() | new() { MaxAgeSeconds = n } | RequireTouchque, RelayOptions, GuardInput |
| Java | silentSignIn(Guard.SilentSignIn.on() | new Guard.SilentSignIn(n)) | TouchqueGuardOptions, RelayOptions, Guard.Input |
| Ruby | silent_sign_in: true | { max_age_seconds: n } | require_touchque (Rails), Touchque::Rack::Relay |
Açıkken X-Touchque-Browser taşıyan yeni bir deneme önce risk.check çağırır. prompt: false ise push olmadan hemen onaylanır: assurance.method "silent", confirmedVia "SILENT", requestId "silent", reasons; relay'ler onAuthenticated'ı via "silent" ile çağırır. Aksi halde, ve kontroldeki her hatada, kanıt eklenmiş her zamanki push başlar; onayı tarayıcıyı tanıtır. Token ile yoklama tekrar kontrol etmez. Başka bir origin'deki relay, Access-Control-Allow-Headers içinde X-Touchque-Browser'a izin vermelidir.
import { createTouchqueWeb, browserKey } from '@touchque/web';
// Korumalı istekler ve klasik 2FA girişi X-Touchque-Browser taşır
const tq = createTouchqueWeb({ baseUrl: '/touchque', browserKey: true });
const result = await tq.classic2fa.login({ onStep });
if (result.assurance?.method === 'silent') {
// bilinen tarayıcı: push gönderilmedi
}
// Ortak bilgisayar / her yerden çıkış: bu tarayıcının anahtarını sil
await tq.browserKey.forget();
// createTouchqueWeb olmadan: başlık değeri, kullanılamıyorsa null
const proof = await browserKey.proof();app.post('/login/2fa',
requireTouchque('LOGIN', {
user: (req) => req.session.passwordVerifiedUser,
silentSignIn: true, // ya da kendi sınırınız: { maxAgeSeconds: 8 * 3600 }
}),
(req, res) => {
// req.touchque.assurance.method === 'silent' → push gönderilmedi
// req.touchque.reasons → [{ code: 'known_browser', text: '…' }, …]
res.json({ ok: true });
});
// touchqueRouter(tq, { …, silentSignIn: true }) → onAuthenticated: via === 'silent'# Flask (Django: require_touchque, FastAPI: RequireTouchque take the same option)
@app.post('/login/2fa')
@require_touchque('LOGIN', user=lambda: session.get('password_ok'),
silent_sign_in=True) # ya da {'max_age_seconds': 8 * 3600}
def login_2fa():
# g.touchque['assurance']['method'] == 'silent' → push gönderilmedi
return jsonify(ok=True)
# touchque_relay_blueprint(tq, ..., silent_sign_in=True) → on_authenticated: via == 'silent'// Laravel — a service provider
$this->app->bind(\Touchque\Laravel\TouchqueMiddleware::class, fn ($app) => new \Touchque\Laravel\TouchqueMiddleware(
$app->make(\Touchque\Touchque::class),
['silentSignIn' => true] // ya da ['maxAgeSeconds' => 8 * 3600]
));
// $request->attributes->get('touchque')['assurance']['method'] === 'silent'
// TouchqueRelay: ['silentSignIn' => true] → onAuthenticated: $info['via'] === 'silent'mux.Handle("/login/2fa", touchque.Require(tq, "LOGIN", http.HandlerFunc(signIn), touchque.RequireOptions{
User: func(r *http.Request) string { return passwordVerifiedUser(r) },
SilentSignIn: true, // kendi sınırınız: SilentSignInMaxAgeSeconds (*int)
}))
func signIn(w http.ResponseWriter, r *http.Request) {
approval, _ := touchque.ApprovalFromContext(r.Context())
if approval.Assurance != nil && approval.Assurance.Method == "silent" {
// push gönderilmedi; approval.Reasons nedenini söyler
}
}
// RelayOptions{..., SilentSignIn: true} → OnAuthenticated: info.Via == touchque.ViaSilentapp.MapPost("/login/2fa", SignIn)
.RequireTouchque("LOGIN", o =>
{
o.User = c => c.HttpContext.Session.GetString("password_verified_user");
o.SilentSignIn = new(); // ya da kendi sınırınız: new() { MaxAgeSeconds = 8 * 3600 }
});
// http.GetTouchqueApproval()!.Assurance?.Method == "silent" → push gönderilmedi
// MapTouchqueRelay(..., new RelayOptions<HttpContext> { ..., SilentSignIn = new() }) → OnAuthenticated: info.Via == "silent"@PostMapping("/login/2fa")
@RequireTouchque("LOGIN")
ResponseEntity<Void> signIn(Approval approval) {
// approval.assurance().method().equals("silent") → push gönderilmedi
return ResponseEntity.ok().build();
}
@Bean
TouchqueGuardOptions login() {
return TouchqueGuardOptions.forAction("LOGIN")
.user(c -> (String) c.request().getSession().getAttribute("password_verified_user"))
.silentSignIn(Guard.SilentSignIn.on()); // ya da kendi sınırınız: new Guard.SilentSignIn(8 * 3600)
}
// RelayOptions<HttpServletRequest>(...).silentSignIn(Guard.SilentSignIn.on()) → onAuthenticated: info.via().equals("silent")class TwoFactorController < ApplicationController
include Touchque::Rails::Guard
require_touchque "LOGIN", only: :create, user: -> { session[:password_verified_user] },
silent_sign_in: true # ya da { max_age_seconds: 8 * 3600 }
def create
# touchque_approval["assurance"]["method"] == "silent" → push gönderilmedi
head :ok
end
end
# Touchque::Rack::Relay.new(..., silent_sign_in: true) → on_authenticated: info["via"] == "silent"const browserProof = req.get('x-touchque-browser');
const risk = await tq.risk.check({
externalUsername: user, type: 'LOGIN', clientIp: req.ip, userAgent: req.get('user-agent'), browserProof,
});
if (risk.prompt === false) return openSession(user); // risk.reasons nedenini söyler
// Her şeyin dışında: her zamanki push, kanıt ekli (onay tarayıcıyı tanıtır)
await tq.login.request({ externalUsername: user, type: 'LOGIN', clientIp: req.ip, browserProof });
const { browsers } = await tq.risk.browsers({ externalUsername: user });
await tq.risk.forgetBrowsers({ externalUsername: user }); // hepsi; tek biri için { thumbprint }browser_proof = request.headers.get('X-Touchque-Browser')
risk = tq.risk.check(user, type='LOGIN', client_ip=ip, browser_proof=browser_proof)
if risk['prompt'] is False:
return open_session(user) # risk['reasons'] nedenini söyler
step = tq.start('LOGIN', user, ip=ip, browser_proof=browser_proof)
browsers = tq.risk.browsers(user)['browsers']
tq.risk.forget_browsers(user) # hepsi; tek biri için thumbprint=...$browserProof = $request->header('X-Touchque-Browser');
$risk = $tq->risk->check([
'externalUsername' => $user, 'type' => 'LOGIN', 'clientIp' => $request->ip(), 'browserProof' => $browserProof,
]);
if ($risk['prompt'] === false) {
return openSession($user); // $risk['reasons'] nedenini söyler
}
$step = $tq->start('LOGIN', $user, null, null, $request->ip(), $request->userAgent(), $browserProof);
$browsers = $tq->risk->browsers(['externalUsername' => $user])['browsers'];
$tq->risk->forgetBrowsers(['externalUsername' => $user]); // hepsi; tek biri için 'thumbprint'proof := r.Header.Get(touchque.GuardBrowserHeader)
risk, err := tq.Risk.Check(r.Context(), touchque.RiskCheckOptions{
ExternalUsername: user, Type: "LOGIN", ClientIP: ip, UserAgent: r.UserAgent(), BrowserProof: proof,
})
if err == nil && !risk.Prompt {
return openSession(user) // risk.Reasons nedenini söyler
}
step, err := tq.Start("LOGIN", user, touchque.StartOptions{IP: ip, BrowserProof: proof})
list, err := tq.Risk.Browsers(ctx, touchque.RiskBrowsersOptions{ExternalUsername: user})
_, err = tq.Risk.ForgetBrowsers(ctx, touchque.ForgetBrowsersOptions{ExternalUsername: user}) // hepsi; tek biri için Thumbprintvar browserProof = http.Request.Headers["X-Touchque-Browser"].ToString();
var risk = await tq.Risk.CheckAsync(new RiskCheckOptions
{
ExternalUsername = user, Type = "LOGIN", ClientIp = http.TouchqueClientIp(),
UserAgent = http.Request.Headers.UserAgent.ToString(), BrowserProof = browserProof,
});
if (!risk.Prompt) return OpenSession(user); // risk.Reasons nedenini söyler
var step = await tq.StartAsync("LOGIN", new StartOptions { User = user, Ip = http.TouchqueClientIp(), BrowserProof = browserProof });
var browsers = await tq.Risk.BrowsersAsync(user);
await tq.Risk.ForgetBrowsersAsync(user); // hepsi; tek biri için thumbprintString browserProof = request.getHeader("X-Touchque-Browser");
Risk.CheckResult risk = tq.risk().check(new Risk.CheckOptions(user).type("LOGIN")
.clientIp(TouchqueSpring.clientIp(request)).userAgent(request.getHeader("User-Agent")).browserProof(browserProof));
if (!risk.prompt()) return openSession(user); // risk.reasons() nedenini söyler
Step step = tq.start("LOGIN", new StartOptions(user).ip(TouchqueSpring.clientIp(request)).browserProof(browserProof));
JsonNode browsers = tq.risk().browsers(user);
tq.risk().forgetBrowsers(user, null); // hepsi; tek biri için thumbprintbrowser_proof = request.headers["X-Touchque-Browser"]
risk = tq.risk.check(user, type: "LOGIN", client_ip: request.remote_ip, user_agent: request.user_agent, browser_proof: browser_proof)
return open_session(user) if risk["prompt"] == false # risk["reasons"] nedenini söyler
step = tq.start("LOGIN", user, ip: request.remote_ip, browser_proof: browser_proof)
browsers = tq.risk.browsers(user)["browsers"]
tq.risk.forget_browsers(user) # hepsi; tek biri için thumbprint:curl -X POST https://api.touchque.com/risk/check \
-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@company.com","type":"LOGIN","clientIp":"203.0.113.7","browserProof":"'"$PROOF"'"}'{
"prompt": false,
"reasons": [
{ "code": "known_browser", "text": "This browser proved its device-bound key" },
{ "code": "recent_approval", "text": "Approved on the phone in this browser 3 hours ago" },
{ "code": "same_phone", "text": "The same phone approved this browser" },
{ "code": "familiar_network", "text": "Country and network seen in earlier approved sign-ins" },
{ "code": "no_risk_signal", "text": "No risk signal" }
],
"assurance": "high",
"silentMode": "enforce"
}