Touchque Login
Kendi müşterileriniz için, kendi markanız ve alan adınızda kayıt ve giriş sayfaları: önce passkey, ikinci adım olarak Touchque uygulaması ya da e-posta/SMS kodu, sosyal ve kurumsal bağlantılar, kurumsal müşteriler için organizasyonlar. Sayfalar sizin logonuzu ve renklerinizi taşır, altta her zaman küçük bir “Touchque ile korunuyor” satırı bulunur. Standart OpenID Connect'tir; her OIDC kütüphanesi çalışır, aşağıdaki SDK'lar ayrıntıları sizin yerinize halleder.
- Dashboard → Touchque Login → Setup: adresinizi seçin (
acme.login.touchque.com). - Apps → uygulamanızı callback adresiyle ekleyin. Sayfası, sizin değerlerinizle doldurulmuş kodu gösterir.
- Projenizde:
npm init @touchque -- --login(Next.js, Express, ASP.NET Core, Rails, Spring Boot; Python, PHP, Go ve Sinatra dosyaları da).--login@touchque/create 2.0.2 ve sonrasında var. .env'i doldurup/auth/login'i açın.
| Yol | Ne |
|---|---|
/.well-known/openid-configuration | Keşif |
/authorize | screen_hint=signup, login_hint, ui_locales, connection, organization, invitation, prompt, max_age, acr_values |
/oauth/token | authorization_code (PKCE), refresh_token (her kullanımda döner), client_credentials, jeton değişimi (RFC 8693), CIBA |
/bc-authorize | CIBA (yoklama): bir uygulama müşteriden telefonunda onay ister |
/userinfo · /oauth/revoke · /oauth/introspect | OpenID Connect ve OAuth 2.0 |
/oidc/logout | id_token_hint, post_logout_redirect_uri |
/account | Müşterilerinizin kendi hesap sayfası |
/me/v1 | My Account API (audience {issuer}/me/, birinci taraf uygulamalar) |
state, birebir callback adresleri, yanıtta issuer (RFC 9207), JWKS'e karşı doğrulanan ID token'lar, her kullanımda dönen refresh token'lar (tekrar kullanılan tüm aileyi bitirir), Argon2id ile saklanan ve sızıntı listelerine karşı denetlenen parolalar.| Koruma | Ne yapar |
|---|---|
| Yanlış parolalar | 15 dakikada 10 yanlış parola o hesapta parolayla girişi durdurur; sahibine kilit açma bağlantısı gider. |
| Şüpheli adresler | Bir adresten birçok hesapta başarısız giriş: bot kontrolü ya da bir saat bekleme. |
| Bot kontrolü | Turnstile, hCaptcha ya da reCAPTCHA v2; sunucuda hostname ile doğrulanır. |
| Sızmış parolalar | Girişte bulunursa: ikinci adımdan sonra yeni parola ya da e-posta. |
| Uyarlanabilir ikinci adım | Yalnızca riskli görünen girişte sorulur (yeni tarayıcı, şüpheli adres, başarısız denemeler, risk motoru). |
Actions'larınız girişte de çalışır: identity.post_login reddedebilir ya da ikinci adım isteyebilir, identity.pre_user_registration bir kaydı reddedebilir. Müşteriler profillerini, passkey'lerini, cihazlarını, uygulama erişimlerini ve verilerini /account'ta ya da sizin uygulamanızda My Account API ve <UserProfile> ile yönetir.
Bir gizli uygulama için açın (Touchque Login → Setup → Apps → uygulama → Acting for your customers; telefonunuz onaylar). Jeton değişimi (RFC 8693) ile uygulama, giriş yapmış bir müşterinin erişim jetonunu API'nizi çağırmak için bir jetonla değiştirir: sub müşteri, act.sub uygulamadır; kapsamlar API'nin uygulamaya verdikleridir (ve müşterinin rollerinin izin verdikleri), müşterinin jetonuyla biter; zincirdeki üçüncü uygulama reddedilir. CIBA ile uygulama müşteriye telefonunda sorar: Touchque authorization_details'i satırlar halinde çizer, önce uygulamanın adını, mesajını doğrulanmamış olarak gösterir, Face ID ve uygulamanın gösterdiği sayıyı ister; jetonlar tam olarak onaylananı bir kez taşır. Telefon görüşmesi sırasında verilen onay yetmez.
Jeton kasası. Bir bağlantı jetonları saklıyorsa (Touchque Login → Setup → Connections → bağlantı → Keep tokens for apps working for people; telefonunuz onaylar), müşteri adına çalışan bir uygulama o müşterinin kendi Google, Microsoft ya da GitHub erişim jetonunu requested_token_type=urn:touchque:params:oauth:token-type:connection-access-token ve connection ile jeton değişiminden alır. Touchque jetonları şifreli saklar, yeniler ve yenileme jetonunu asla vermez; scope yalnızca sağlayıcının verdiğini daraltabilir. Müşterinin jetonu uygulamanın kendisine verilmiş olmalıdır, bağlantı uygulamayı listelemiyorsa. Müşteriler bunu /account'ta (ya da stopAppAccess ile), siz DELETE /identity/users/{id}/connected-accounts/{accountId} ile kapatırsınız; hesabın bağlantısını kesmek ya da müşteriyi silmek jetonları da siler.
// npm init @touchque -- --login (writes these files)
// lib/touchque.ts
import { createTouchqueLogin } from '@touchque/node';
export const touchque = createTouchqueLogin({
domain: ({}).TOUCHQUE_LOGIN_DOMAIN!, // acme.login.touchque.com
clientId: ({}).TOUCHQUE_LOGIN_CLIENT_ID!,
clientSecret: ({}).TOUCHQUE_LOGIN_CLIENT_SECRET,
secret: ({}).TOUCHQUE_SESSION_SECRET!, // 32+ random characters
appUrl: ({}).APP_URL!,
});
// app/auth/[action]/route.ts
export const { GET } = touchque.handlers;
// middleware.ts (Edge)
import { touchqueMiddleware } from '@touchque/node/next/middleware';
export default touchqueMiddleware({ secret: ({}).TOUCHQUE_SESSION_SECRET!, protect: ['/dashboard'] });const { HostedLogin, hostedLoginRoutes, requireSignIn } = require('@touchque/node');
const login = new HostedLogin({ domain: 'acme.login.touchque.com', clientId, clientSecret,
redirectUri: 'https://app.acme.com/callback' });
app.use(hostedLoginRoutes({ login, secret: ({}).SESSION_SECRET })); // /login, /callback, /logout
app.get('/account', requireSignIn(), (req, res) => res.json(req.touchqueSession.user));import { HostedLoginProvider, TouchqueLogin, UserProfile } from '@touchque/react';
<HostedLoginProvider domain="acme.login.touchque.com" clientId={id} redirectUri={`${location.origin}/callback`}>
<header><TouchqueLogin /></header> {/* Sign in · Create account · user menu */}
<UserProfile /> {/* profile, passkeys, devices, apps */}
</HostedLoginProvider>from touchque import HostedLogin
login = HostedLogin(domain="acme.login.touchque.com", client_id=CLIENT_ID, client_secret=CLIENT_SECRET,
redirect_uri="https://app.acme.com/callback")
req = login.authorization_url() # keep state, nonce, code_verifier; redirect to req.url
tokens = login.handle_callback(request.url, **session.pop("tq"))use Touchque\HostedLogin\HostedLogin;
$login = new HostedLogin(['domain' => 'acme.login.touchque.com', 'clientId' => $id, 'clientSecret' => $secret,
'redirectUri' => 'https://app.acme.com/callback']);
$req = $login->authorizationUrl();
$tokens = $login->handleCallback($_GET, $state, $nonce, $codeVerifier);login, _ := touchque.NewHostedLogin(touchque.HostedLoginConfig{
Domain: "acme.login.touchque.com", ClientID: id, ClientSecret: secret, RedirectURI: "https://app.acme.com/callback",
})
req, _ := login.AuthorizationURL(touchque.AuthorizationOptions{})
tokens, err := login.HandleCallback(ctx, r.URL.Query(), touchque.CallbackChecks{State: s, Nonce: n, CodeVerifier: v})// ASP.NET Core: cookie session + /login, /callback, /logout
builder.Services.AddTouchqueLogin(o =>
{
o.Domain = "acme.login.touchque.com"; o.ClientId = id; o.ClientSecret = secret;
o.Scope = "openid profile email offline_access";
});
app.UseAuthentication(); app.UseAuthorization();
app.MapTouchqueLogin();
app.MapGet("/account", (ClaimsPrincipal user) => user.FindFirst("email")?.Value).RequireAuthorization();
// Your API: AddAuthentication().AddTouchqueBearer(o => { o.Domain = "acme.login.touchque.com"; o.Audience = "https://api.acme.com"; })# application.properties (Spring Boot starter + spring-boot-starter-oauth2-client)
touchque.login.domain=acme.login.touchque.com
touchque.login.client-id=…
touchque.login.client-secret=…
touchque.login.scope=openid,profile,email,offline_access
# → every page signs in with Touchque Login (PKCE): /oauth2/authorization/touchque, sign-out POST /logout
# Your own SecurityFilterChain: touchqueLogin.configure(http)
# Your API (resource server): a JwtDecoder for at+jwt access tokens
touchque.login.audience=https://api.acme.com# config/application.rb
config.touchque.login = { domain: "acme.login.touchque.com", client_id: ENV["CLIENT_ID"], client_secret: ENV["CLIENT_SECRET"] }
# config/routes.rb
Touchque::Rails.login_routes(self) # /login, /callback, /logout
class AccountController < ApplicationController
include Touchque::Rails::HostedLogin
before_action :require_touchque_sign_in
def show = render(json: touchque_session["user"])
endconst { users } = await tq.identity.users.list({ q: 'ayse' });
await tq.identity.users.block(users[0].id, { reason: 'chargeback' });
const job = await tq.identity.imports.start({ users: [{ email: 'old@acme.com', passwordHash: '$2b$10$…' }] });
await tq.identity.setDelivery('email', { provider: 'postmark', from: 'login@acme.com', serverToken });
await tq.identity.setAttackProtection({ bot: { provider: 'turnstile', siteKey, secret, mode: 'risky' } });
const data = await tq.identity.users.export(userId); // KVKK / GDPRusers = tq.identity.users.list(q="ayse")["users"]
tq.identity.users.block(users[0]["id"], reason="chargeback")
data = tq.identity.users.export(user_id) # KVKK / GDPR$users = $tq->identity->users->list(['q' => 'ayse'])['users'];
$tq->identity->users->block($users[0]['id'], 'chargeback');
$data = $tq->identity->users->export($userId); // KVKK / GDPRpage, err := tq.Identity.Users.List(map[string]string{"q": "ayse"})
users, _ := page["users"].([]interface{})
first, _ := users[0].(map[string]interface{})
_, err = tq.Identity.Users.Block(first["id"].(string), "chargeback")
data, err := tq.Identity.Users.Export(userID) // KVKK / GDPRvar page = await tq.Identity.Users.ListAsync(new { q = "ayse" });
var userId = (string)page["users"]![0]!["id"]!;
await tq.Identity.Users.BlockAsync(userId, "chargeback");
var data = await tq.Identity.Users.ExportAsync(userId); // KVKK / GDPRJsonNode users = tq.identity().users().list(Map.of("q", "ayse")).get("users");
tq.identity().users().block(users.get(0).get("id").asText(), "chargeback");
JsonNode data = tq.identity().users().export(userId); // KVKK / GDPRusers = tq.identity.users.list(q: "ayse")["users"]
tq.identity.users.block(users[0]["id"], reason: "chargeback")
data = tq.identity.users.export(user_id) # KVKK / GDPRGET /identity/users?q=ayse
POST /identity/users/{id}/block
POST /identity/users-imports
PUT /identity/delivery/email
PUT /identity/attack-protection
GET /identity/users/{id}/export
DELETE /identity/users/{id}// An AI agent of yours (a confidential app with these grants on, Touchque Login → Setup → Apps → the app)
const forAda = await login.exchangeToken(adasAccessToken, { audience: 'https://api.acme.com' }); // sub = Ada, act.sub = the app
const ask = await login.askUser({ loginHint: 'ada@acme.com', scope: 'openid payments:create', audience: 'https://api.acme.com',
bindingMessage: 'Pay invoice 42', authorizationDetails: [payment] });
reply(`Type ${ask.bindingCode} in the Touchque app`);
const approved = await login.waitForUser(ask); // approved.authorizationDetails
// Token vault: Ada's own Google token (Touchque Login → Setup → Connections → Google → Keep tokens…)
const google = await login.getConnectionToken(adasAccessToken, { connection: 'google' });
await fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', { headers: { Authorization: `Bearer ${google.accessToken}` } });for_ada = login.exchange_token(adas_access_token, audience="https://api.acme.com")
ask = login.ask_user("ada@acme.com", "openid payments:create", audience="https://api.acme.com", binding_message="Pay invoice 42")
approved = login.wait_for_user(ask)
google = login.get_connection_token(adas_access_token, "google") # token vault$forAda = $login->exchangeToken($adasAccessToken, 'https://api.acme.com');
$ask = $login->askUser('ada@acme.com', 'openid payments:create', ['audience' => 'https://api.acme.com', 'bindingMessage' => 'Pay invoice 42']);
$approved = $login->waitForUser($ask);
$google = $login->getConnectionToken($adasAccessToken, 'google'); // token vaultforAda, _ := login.ExchangeToken(ctx, adasAccessToken, "https://api.acme.com", "")
ask, _ := login.AskUser(ctx, touchque.AskUserParams{LoginHint: "ada@acme.com", Scope: "openid payments:create", Audience: "https://api.acme.com", BindingMessage: "Pay invoice 42"})
approved, err := login.WaitForUser(ctx, ask)
google, err := login.GetConnectionToken(ctx, adasAccessToken, "google", "") // token vaultvar forAda = await login.ExchangeTokenAsync(adasAccessToken, "https://api.acme.com");
var ask = await login.AskUserAsync("ada@acme.com", "openid payments:create",
new AskUserOptions { Audience = "https://api.acme.com", BindingMessage = "Pay invoice 42" });
var approved = await login.WaitForUserAsync(ask);
var google = await login.GetConnectionTokenAsync(adasAccessToken, "google"); // token vaultvar forAda = login.exchangeToken(adasAccessToken, "https://api.acme.com", null);
var ask = login.askUser("ada@acme.com", "openid payments:create",
new HostedLogin.AskUserOptions().audience("https://api.acme.com").bindingMessage("Pay invoice 42"));
var approved = login.waitForUser(ask);
var google = login.getConnectionToken(adasAccessToken, "google", null); // token vaultfor_ada = login.exchange_token(adas_access_token, "https://api.acme.com")
ask = login.ask_user("ada@acme.com", "openid payments:create", audience: "https://api.acme.com", binding_message: "Pay invoice 42")
approved = login.wait_for_user(ask)
google = login.get_connection_token(adas_access_token, "google") # token vaultPOST /bc-authorize (Basic client_id:client_secret)
login_hint=ada@acme.com&scope=openid payments:create&audience=https://api.acme.com
&binding_message=Pay invoice 42&authorization_details=[…]
# → { "auth_req_id": "…", "expires_in": 300, "interval": 5, "binding_code": "47" }
POST /oauth/token grant_type=urn:openid:params:grant-type:ciba&auth_req_id=…
POST /oauth/token grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<customer's access token>&subject_token_type=urn:ietf:params:oauth:token-type:access_token&audience=…
# Token vault: the provider's own access token instead of audience
&requested_token_type=urn:touchque:params:oauth:token-type:connection-access-token&connection=google