gti

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 18 Imported by: 0

README

go-gti

GlobalTrustID'nin resmî Go istemcisi. Üç işi yapar:

  1. "GlobalTrustID ile giriş" — OAuth 2.0 / OpenID Connect, PKCE (S256) ile.
  2. Kimlik doğrulama — QR oluşturur, kullanıcı telefonuyla onaylar.
  3. Webhook doğrulama — gelen bildirimin gerçekten bizden geldiğini kanıtlar.
client, _ := gti.New(gti.Config{
    ClientID:    "gtid_...",
    RedirectURI: "https://siteniz.com/gti/callback",
})

login, _ := client.BeginLogin(gti.ScopeOpenID)
// login.State ve login.Verifier'ı oturumunuzda saklayın
http.Redirect(w, r, login.URL, http.StatusFound)
  • Dış bağımlılık yok — yalnızca standart kütüphane.
  • context.Context her ağ çağrısında.
  • Tipli hatalarerrors.Is / errors.As ile ayırt edilebilir.

İçindekiler


Kurulum

go get github.com/GlobalTrustID/go-gti
import gti "github.com/GlobalTrustID/go-gti"

Go 1.21+.


İki ayrı kimlik bilgisi

Çalışmayan entegrasyonların en sık sebebi bu. APIKey ile ClientSecret aynı şey değildir:

Nereden alınır Ne için
ClientID + ClientSecret Panel → Entegrasyonlar → OAuth Uygulamaları Giriş
APIKey (sk_live_...) Panel → API Keys Kimlik doğrulama oturumları

Yalnızca giriş yapacaksanız APIKey'e, yalnızca kimlik doğrulaması yapacaksanız ClientID/ClientSecret'a ihtiyacınız yok.

Public istemcide ClientSecret yazmayın. Tarayıcı ya da mobil uygulama sır saklayamaz; akışı PKCE korur. Boş bir dize göndermek sunucuya "sırrım var ama boş" demektir.


1. GlobalTrustID ile giriş

client, err := gti.New(gti.Config{
    ClientID:     "gtid_...",
    RedirectURI:  "https://siteniz.com/gti/callback",
    ClientSecret: "...", // yalnızca confidential istemcide
})

Yönlendirme:

login, err := client.BeginLogin(gti.ScopeOpenID)
if err != nil { /* ... */ }

// State ve Verifier SAKLANMAK ZORUNDA — ikisi de callback'te gerekiyor.
session.Put("gti_state", login.State)
session.Put("gti_verifier", login.Verifier)

http.Redirect(w, r, login.URL, http.StatusFound)

Callback:

tokens, err := client.CompleteLogin(r.Context(), r.URL.Query(),
    session.Get("gti_state"), session.Get("gti_verifier"))

if errors.Is(err, gti.ErrDenied) {
    // Kullanıcı vazgeçti. Hata değil, meşru bir sonuç.
    return
}
if err != nil { /* ... */ }

user, err := client.FetchUserInfo(r.Context(), tokens.AccessToken)
// user.Subject kullanıcının DEĞİŞMEYEN kimliği — kendi tablonuzda bunu
// saklayın, e-postayı değil.
Oturum saklama neden sizin işiniz

Bu paket State ve Verifier'ı sizin adınıza saklamıyor. Oturum yönetimi çerçevenizin işi: çerez, Redis, veritabanı ya da başka bir şey olabilir. Bir oturum soyutlaması uydurmak her kurulumda yanlış olurdu.

CompleteLogin State'i sabit zamanlı karşılaştırıyor ve boş bir beklenen değeri reddediyor — oturumdan okunamamış bir state'i sessizce geçirmek, CSRF korumasını tamamen kaldırmak olurdu.


2. Kimlik doğrulama (QR)

client, _ := gti.New(gti.Config{APIKey: "sk_live_..."})

session, err := client.CreateSession(ctx, []string{
    "identity.first_name",
    "identity.last_name",
    "identity.national_id",
}, "") // webhook adresi isterseniz üçüncü argümana

fmt.Fprintf(w, `<img src="%s">`, client.QRURL(session.SessionID))

Kullanıcı onayladıktan sonra:

result, err := client.FetchSession(ctx, session.SessionID)

if result.Approved() && result.IdentityVerified() {
    ad := result.Identity["identity.first_name"]
}

IdentityVerified() yalnızca çipten okunabilen alanlara bakar. Ad ve e-posta kullanıcının kendi yazdığı profilden de gelebilir; kimlik numarası ve belge numarası gelemez — onlar yalnızca NFC ile okunan çipten çıkar.

Sonuçlar kalıcı saklanmaz, kısa süre sonra silinir. Onay gelir gelmez okuyun ya da webhook kullanın.


3. Webhook

client, _ := gti.New(gti.Config{WebhookSecret: "whsec_..."})

func handler(w http.ResponseWriter, r *http.Request) {
    // HAM gövde. JSON'a çevirip yeniden kodlarsanız imza tutmaz.
    body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))

    if err := client.VerifyWebhookRequest(r, body, gti.DefaultWebhookTolerance); err != nil {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    // ... gövdeyi işleyin ...
    w.WriteHeader(http.StatusOK) // 2xx dönmezseniz yeniden denenir
}

Bu adımı atlamayın. Webhook adresiniz herkese açıktır; imzayı doğrulamadan gövdeye güvenirseniz, size sahte bir approved gönderen herkes kimlik doğrulamasını atlatmış olur.

Zaman damgası imzanın içinde: yalnızca gövdeyi imzalamak, saldırganın eski bir teslimatı taze bir zaman başlığıyla yeniden göndermesine izin verirdi. Varsayılan tolerans 5 dakika; kuyruğa alınmış webhook'ları sonradan işliyorsanız 0 geçip zaman kontrolünü kapatabilirsiniz.


Alanlar (scope)

Kullanıcı onay ekranında tam olarak ne istediğinizi görür. İstemediğiniz hiçbir alan size gelmez.

Scope Ne döner
openid Yalnızca giriş — hiçbir kişisel veri paylaşılmaz
identity.first_name / identity.last_name Ad / soyad
identity.birth_date Doğum tarihi
identity.gender Cinsiyet
identity.national_id TC kimlik numarası — yalnızca çipten
identity.document_number Belge numarası — yalnızca çipten
identity.nationality Uyruk — yalnızca çipten
age.over_18 Doğum tarihi olmadan yalnızca "true" / "false"
email E-posta
phone Telefon
avatar Profil görselinin adresi

"Yalnızca çipten" işaretli alanlar profilden doldurulamaz; istemek, NFC ile belge okumasının yapılmış olmasını şart koşar. Listeyi kodda gti.ChipOnlyScopes() ile alabilirsiniz.

age.over_18, yaş sınırı denetlemek için doğum tarihi istemenin yerine geçer: kullanıcı doğum gününü paylaşmadan reşit olduğunu kanıtlar.

address ve identity.passport_number HENÜZ İSTEMEYİN. Sunucunun keşif belgesinde görünüyorlar ama mobil uygulama bugün ikisini de dolduramıyor. Onay için istenen alanların TAMAMININ dolu olması gerektiğinden, bunları istemek oturumu onaylanamaz hâle getirir: kullanıcı "Onayla"ya basamaz ve sebebini anlamaz.

tokens.Scopes()'a bakın, istediğinize değil. Kullanıcı istediğinizden azını onaylamış olabilir; yetkiyi buna göre kurun.


Hata yönetimi

var apiErr *gti.APIError
var oauthErr *gti.OAuthError

switch {
case errors.Is(err, gti.ErrDenied):        // kullanıcı vazgeçti
case errors.Is(err, gti.ErrStateMismatch): // CSRF şüphesi — akışı durdurun
case errors.Is(err, gti.ErrConfig):        // eksik yapılandırma
case errors.Is(err, gti.ErrTransport):     // ağ — yeniden denemek mantıklı
case errors.As(err, &apiErr):              // apiErr.Code, apiErr.Message
case errors.As(err, &oauthErr):            // oauthErr.Code ("invalid_grant"…)
}

ErrTransport ile API hatası bilerek ayrı: birincisi bizim ulaşamamamız (yeniden denemek mantıklı), ikincisi sunucunun "hayır" demesi (yeniden denemek aynı cevabı verir).


Örnekler

Klasör Ne gösteriyor
examples/login OAuth ile giriş, net/http ile
examples/verify QR ile kimlik doğrulama
examples/webhook Webhook alıcısı ve imza doğrulama
GTI_CLIENT_ID=gtid_... go run ./examples/login
GTI_API_KEY=sk_live_... go run ./examples/verify
GTI_WEBHOOK_SECRET=whsec_... go run ./examples/webhook

Destek

Lisans

MIT — bkz. LICENSE.

Documentation

Overview

Package gti, GlobalTrustID'nin resmî Go istemcisidir.

Üç işi yapar:

  1. "GlobalTrustID ile giriş" — OAuth 2.0 / OpenID Connect, PKCE (S256) ile.
  2. Kimlik doğrulama — QR oluşturur, kullanıcı telefonuyla onaylar.
  3. Webhook doğrulama — gelen bildirimin gerçekten bizden geldiğini kanıtlar.

DIŞ BAĞIMLILIK YOK: yalnızca standart kütüphane. Kimlik doğrulama altyapısına bağlanan bir paketin kendi bağımlılık ağacını getirmemesi, denetlenebilirliği doğrudan etkiliyor.

İki ayrı kimlik bilgisi

APIKey ile ClientSecret AYNI ŞEY DEĞİLDİR ve karıştırılmaları çalışmayan entegrasyonların en sık sebebi:

  • ClientID + ClientSecret: OAuth uygulamanız. Panel → Entegrasyonlar → OAuth Uygulamaları. Giriş akışında kullanılır.
  • APIKey (sk_live_...): organizasyonunuzun API anahtarı. Panel → API Keys. Kimlik doğrulama oturumlarında kullanılır.

Yalnızca giriş yapacaksanız APIKey'e, yalnızca kimlik doğrulaması yapacaksanız ClientID/ClientSecret'a ihtiyacınız yok.

Örnek

client, err := gti.New(gti.Config{
    ClientID:    "gtid_...",
    RedirectURI: "https://siteniz.com/gti/callback",
})

// Yönlendirme
login, _ := client.BeginLogin(gti.ScopeOpenID)
// login.State ve login.Verifier'ı OTURUMUNUZDA saklayın
http.Redirect(w, r, login.URL, http.StatusFound)

// Callback
tokens, err := client.CompleteLogin(ctx, r.URL.Query(), state, verifier)

Index

Constants

View Source
const (
	DefaultAPIBase = "https://api.globaltrust.id"
	DefaultIssuer  = "https://globaltrust.id"
)

Varsayılan adresler. Kurulumunuz farklıysa Config'ten geçin.

View Source
const (
	SignatureHeader = "X-GTI-Signature"
	TimestampHeader = "X-GTI-Timestamp"
	EventHeader     = "X-GTI-Event"
)

Webhook başlıkları.

View Source
const DefaultWebhookTolerance = 5 * time.Minute

DefaultWebhookTolerance, kabul edilen azami zaman sapması.

View Source
const ScopeOpenID = "openid"

ScopeOpenID, yalnızca kimlik doğrulama: hiçbir kişisel veri paylaşılmaz.

Kullanıcının onay ekranında "hiçbir bilgin paylaşılmıyor" yazar. Ad, e-posta gibi alanlara gerçekten ihtiyacınız varsa açıkça isteyin — istemediğiniz veri size gelmez.

View Source
const Version = "0.1.0"

Version, User-Agent'ta gider; destek talebinde işe yarar. Git etiketiyle aynı tutulmalı.

Variables

View Source
var (
	// ErrConfig, istemci eksik ya da tutarsız yapılandırıldı. İstek hiç
	// gönderilmedi.
	ErrConfig = errors.New("gti: yapılandırma eksik")

	// ErrStateMismatch, dönüş adresindeki state beklenenle uyuşmadı.
	// Giriş DURDURULMALI: bu, akışı başlatanın kullanıcı olmadığı anlamına
	// gelebilir (CSRF).
	ErrStateMismatch = errors.New("gti: state eşleşmedi")

	// ErrDenied, kullanıcı onay vermedi ya da sunucu isteği reddetti.
	ErrDenied = errors.New("gti: giriş reddedildi")

	// ErrTransport, sunucuya ulaşılamadı. API hatasından AYRI: birincisi
	// bizim ulaşamamamız, ikincisi sunucunun "hayır" demesi ve ikisi farklı
	// davranmayı gerektirir (yeniden dene / deneme).
	ErrTransport = errors.New("gti: sunucuya ulaşılamadı")
)

Sentinel hatalar. errors.Is ile karşılaştırılmak üzere.

Hata METNİNE göre dallanmak zorunda kalınmasın diye ayrı değerler: metin değişebilir ve çevrilebilir, sentinel sözleşmenin parçası.

Functions

func ChipOnlyScopes

func ChipOnlyScopes() []string

ChipOnlyScopes, çipten okunmadan doldurulamayan scope listesinin kopyasını verir.

Types

type APIError

type APIError struct {
	Code    int
	Message string
	// HTTPStatus, yanıtın HTTP durum kodu.
	HTTPStatus int
}

APIError, sunucunun zarflı yanıtındaki hata (/api/v1/... uçları).

Zarf biçimi: {status, data, error_code, error_message}

func (*APIError) Error

func (e *APIError) Error() string

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client, GlobalTrustID istemcisi. Eşzamanlı kullanıma uygundur.

func New

func New(cfg Config) (*Client, error)

New, istemciyi kurar.

Yapılandırma burada DOĞRULANMIYOR: hangi alanların gerekli olduğu yapılacak işe göre değişiyor ve yalnızca webhook doğrulaması yapan biri ClientID vermek zorunda değil. Eksiklik, o alana ihtiyaç duyan çağrıda ErrConfig olarak bildiriliyor — istek gönderilmeden.

func (*Client) BeginLogin

func (c *Client) BeginLogin(scope ...string) (*LoginRequest, error)

BeginLogin, giriş adresini ve saklanması gereken değerleri üretir.

login, err := client.BeginLogin(gti.ScopeOpenID)
session.Put("gti_state", login.State)
session.Put("gti_verifier", login.Verifier)
http.Redirect(w, r, login.URL, http.StatusFound)

func (*Client) CompleteLogin

func (c *Client) CompleteLogin(ctx context.Context, query url.Values,
	expectedState, verifier string) (*TokenSet, error)

CompleteLogin, callback'teki sorgu dizesini token'a çevirir.

expectedState ve verifier, BeginLogin'den alıp sakladığınız değerler.

tokens, err := client.CompleteLogin(ctx, r.URL.Query(), state, verifier)
if errors.Is(err, gti.ErrDenied) { /* kullanıcı vazgeçti */ }

func (*Client) CreateSession

func (c *Client) CreateSession(ctx context.Context, scope []string, webhookURL string) (*Session, error)

CreateSession, kimlik doğrulama oturumu açar.

webhookURL boş bırakılabilir; verilirse sonuç hazır olduğunda oraya POST edilir ve kullanıcı tarayıcıyı kapatsa bile size ulaşır.

func (*Client) Exchange

func (c *Client) Exchange(ctx context.Context, code, verifier string) (*TokenSet, error)

Exchange, yetkilendirme kodunu token ile değiştirir.

func (*Client) FetchSession

func (c *Client) FetchSession(ctx context.Context, sessionID string) (*SessionResult, error)

FetchSession, bir oturumun güncel durumunu ve varsa sonucunu okur.

func (*Client) FetchUserInfo

func (c *Client) FetchUserInfo(ctx context.Context, accessToken string) (*UserInfo, error)

FetchUserInfo, erişim token'ıyla onaylanan alanları okur.

func (*Client) QRURL

func (c *Client) QRURL(sessionID string) string

QRURL, oturumun hazır QR görselinin adresi (SVG).

Kendi QR kütüphanenizi kullanmak isterseniz Session.QR alanındaki metni kodlayın; bu adres sunucunun ürettiği hazır görsel.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)

Refresh, süresi dolan erişim token'ını yeniler.

func (*Client) VerifyWebhook

func (c *Client) VerifyWebhook(body []byte, signature, timestamp string, tolerance time.Duration) error

VerifyWebhook, gelen webhook'un gerçekten bizden geldiğini doğrular.

BU ADIMI ATLAMAYIN. Webhook adresiniz herkese açıktır; imzayı doğrulamadan gövdeye güvenirseniz, size sahte bir "approved" gönderen herkes kimlik doğrulamasını atlatmış olur.

body HAM BAYTLAR olmalı. JSON'u çözüp yeniden kodlarsanız boşluk ve anahtar sırası değişir, imza tutmaz.

tolerance sıfır ya da negatifse zaman kontrolü yapılmaz. Bunu yalnızca kuyruğa alınmış webhook'ları sonradan işliyorsanız kullanın: zaman kontrolü, geçerli bir isteği kaydedip sonra tekrar gönderen birine (replay) karşı koruyor.

func (*Client) VerifyWebhookRequest

func (c *Client) VerifyWebhookRequest(r *http.Request, body []byte, tolerance time.Duration) error

VerifyWebhookRequest, başlıkları istekten okuyup VerifyWebhook'u çağırır.

body'yi SİZ okuyup vermelisiniz: http.Request.Body tek kullanımlık, ve burada okusaydık siz aynı gövdeyi bir daha okuyamazdınız.

body, _ := io.ReadAll(r.Body)
if err := client.VerifyWebhookRequest(r, body, gti.DefaultWebhookTolerance); err != nil {
    w.WriteHeader(http.StatusUnauthorized)
    return
}

type Config

type Config struct {
	// OAuth uygulaması — "GlobalTrustID ile giriş" için.
	ClientID    string
	RedirectURI string
	// ClientSecret YALNIZCA confidential istemcide doldurulur. Public
	// istemcide (SPA, mobil) sır yoktur ve akışı PKCE korur — boş bırakın.
	// Boş bir dize göndermek sunucuya "sırrım var ama boş" demektir.
	ClientSecret string

	// APIKey (sk_live_... / sk_test_...) — kimlik doğrulama oturumları için.
	APIKey string

	// WebhookSecret, gelen webhook'un imzasını doğrulamak için.
	WebhookSecret string

	// APIBase ve Issuer boşsa varsayılanlar kullanılır.
	APIBase string
	Issuer  string

	// HTTPClient verilmezse 15 saniye zaman aşımlı bir istemci kurulur.
	//
	// Kendi istemcinizi vermek isteyebilirsiniz: bağlantı havuzu, proxy ya da
	// izleme (tracing) eklemek için. Yalnızca dikkat: yönlendirmeleri İZLEYEN
	// bir istemci, Authorization başlığı taşıyan bir isteği başka bir hosta
	// göndererek gizli anahtarı oraya sızdırabilir.
	HTTPClient *http.Client
}

Config, istemcinin yapılandırması.

type LoginRequest

type LoginRequest struct {
	// URL, kullanıcıyı yönlendireceğiniz adres.
	URL string
	// State, CSRF koruması. Callback'te dönen değerle karşılaştırılıyor.
	State string
	// Verifier, PKCE doğrulayıcısı. GİZLİ: yalnızca token değişiminde
	// sunucuya gidiyor, yönlendirme adresinde asla görünmüyor.
	Verifier string
}

LoginRequest, BeginLogin'in ürettiği tek seferlik giriş isteği.

STATE VE VERIFIER SAKLANMAK ZORUNDA. İkisi de callback'te gerekiyor ve bu paket onları sizin adınıza saklamıyor — oturum yönetimi sizin çerçevenizin işi (çerez, Redis, veritabanı). Bir oturum soyutlaması uydurmak, her kurulumda yanlış olurdu.

type OAuthError

type OAuthError struct {
	Code        string // "invalid_grant", "invalid_client", ...
	Description string
	HTTPStatus  int
}

OAuthError, OAuth/OIDC uçlarının hatası. Bu uçlar zarf kullanmıyor; hata RFC 6749 §5.2 biçiminde geliyor.

func (*OAuthError) Error

func (e *OAuthError) Error() string

func (*OAuthError) Is

func (e *OAuthError) Is(target error) bool

Is, kullanıcının vazgeçtiği hâlin ErrDenied ile yakalanmasını sağlar: access_denied bir arıza değil, meşru bir sonuç.

type Session

type Session struct {
	SessionID string `json:"session_id"`
	// QR, karekodun içine yazılacak METİN. Kodu kendiniz üretmek isterseniz
	// bunu kullanın; hazır görsel için QRURL.
	QR        string   `json:"qr"`
	Scope     []string `json:"scope"`
	Status    string   `json:"status"`
	ExpiresAt string   `json:"expires_at"`
}

Session, açılan doğrulama oturumu.

type SessionResult

type SessionResult struct {
	SessionID string `json:"session_id"`
	// Status: waiting | scanned | opened | approved | declined | expired
	Status string   `json:"status"`
	Scope  []string `json:"scope"`
	// Identity yalnızca approved durumunda ve sonuç henüz silinmediyse dolu.
	// Sonuçlar kısa süre sonra kendiliğinden yok oluyor — kalıcı saklanmazlar.
	Identity  map[string]string `json:"identity,omitempty"`
	ExpiresAt string            `json:"expires_at"`
}

SessionResult, oturumun okunan sonucu.

func (SessionResult) Approved

func (r SessionResult) Approved() bool

Approved, oturum onaylandı mı.

func (SessionResult) IdentityVerified

func (r SessionResult) IdentityVerified() bool

IdentityVerified, kullanıcının kimlik BELGESİNİ gerçekten okutup okutmadığını söyler.

Ad ve e-posta gibi alanlar kullanıcının kendi yazdığı profilden de gelebilir; belge numarası ve kimlik numarası gelemez — onlar yalnızca NFC ile okunan çipten çıkar. Bu yüzden "bu kişi kimliğini doğruladı mı?" sorusunun cevabı, sonuçta bu alanlardan birinin bulunup bulunmadığıdır.

type TokenSet

type TokenSet struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	// IDToken bir JWT. Doğrulamadan içeriğine güvenmeyin.
	IDToken string `json:"id_token,omitempty"`
	// Scope, kullanıcının FİİLEN onayladığı alanlar — istediğinizden dar
	// olabilir. Yetkiyi buna göre kurun, istediğinize göre değil.
	Scope string `json:"scope"`
}

TokenSet, token ucunun yanıtı.

func (TokenSet) Scopes

func (t TokenSet) Scopes() []string

Scopes, onaylanan alanları liste hâlinde verir.

type UserInfo

type UserInfo struct {
	Subject        string `json:"sub"`
	GivenName      string `json:"given_name,omitempty"`
	FamilyName     string `json:"family_name,omitempty"`
	Name           string `json:"name,omitempty"`
	Birthdate      string `json:"birthdate,omitempty"`
	Gender         string `json:"gender,omitempty"`
	Email          string `json:"email,omitempty"`
	PhoneNumber    string `json:"phone_number,omitempty"`
	Address        string `json:"address,omitempty"`
	Nationality    string `json:"nationality,omitempty"`
	NationalID     string `json:"national_id,omitempty"`
	DocumentNumber string `json:"document_number,omitempty"`
	PassportNumber string `json:"passport_number,omitempty"`
	Picture        string `json:"picture,omitempty"`
	// AgeOver18, doğum tarihi paylaşılmadan "true"/"false".
	AgeOver18 string `json:"age_over_18,omitempty"`
}

UserInfo, /oauth/userinfo yanıtı. Alanlar yalnızca onaylanan scope kadar dolu; istemediğiniz bir alan hiç gelmez.

Directories

Path Synopsis
examples
login command
ÖRNEK 1 — "GlobalTrustID ile giriş" (OAuth 2.0 / OIDC).
ÖRNEK 1 — "GlobalTrustID ile giriş" (OAuth 2.0 / OIDC).
verify command
ÖRNEK 2 — QR ile kimlik doğrulama.
ÖRNEK 2 — QR ile kimlik doğrulama.
webhook command
ÖRNEK 3 — Webhook alıcısı.
ÖRNEK 3 — Webhook alıcısı.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL