N'oluyo ile giriş
N'oluyo ile giriş (OAuth2), bir kişinin kendi hesabıyla senin web sitene, masaüstü programına ya da mobil uygulamana açık rızasıyla giriş yapmasını sağlar. Şifresini seninle paylaşmaz; N'oluyo ekranında hangi bilgilerin paylaşılacağını görür, onaylar ve senin uygulamana tek kullanımlık bir kodla döner.
Bu nedir ve ne zaman gerekir
Geliştirici portalındaki tek bir uygulama hem bot hem de N'oluyo ile giriş barındırabilir; ama ikisi tamamen farklı işler yapar:
- Bir sunucuya bot eklemek istiyorsan bu sayfaya ihtiyacın yok. Bot ekleme akışı
/bot-ekle/<uygulama_id>ekranından yürür; ayrıntısı Bot uygulamaları rehberinde. - Kendi sitene veya uygulamana kullanıcı girişi koymak istiyorsan doğru yer burasıdır. Bot kullanıcısı oluşturman bile gerekmez; yalnız OAuth2 ayarlarını açman yeterlidir.
Kişinin gördüğü izin ekranı
Kullanıcıyı https://noluyo.net/oauth2/authorize adresine yönlendirdiğinde karşısına N'oluyo'nun izin ekranı çıkar. Bu ekranda:
- Uygulamanın adı, avatarı, doğrulanmış rozeti (doğrulanmamışsa açık uyarı etiketi), gizlilik politikası (
privacy_url) ve kullanım koşulları (terms_url) bağlantıları yer alır. - İstenen her kapsam (
identify,email,guilds) sade cümlelerle listelenir. - Ekranın altında "Uygulama şunları asla göremez" kutusu sabit olarak gösterilir: şifren, iki aşamalı doğrulama kodların, özel mesajların, sunucu mesajların, sesli sohbetlerin ve ödeme bilgilerin paylaşılmaz; uygulama senin adına mesaj atamaz veya sunucuya katılamaz.
Misafir (kayıtsız) hesaplar OAuth2 izni veremez (403 guest_not_allowed). Doğrulanmamış bir uygulamaya en çok 100 farklı kullanıcı giriş yapabilir; 101. kişi 403 oauth_user_cap uyarısı alır. Sınırı kaldırmak için Doğrulama başvurusu yapılır.
Portal ayarları ve yönlendirme adresleri
/gelistirici/uygulamalar sayfasında uygulamanı açıp N'oluyo ile Giriş (OAuth2) bölümünü yapılandır:
- Yönlendirme adresleri (
redirect_uris): En çok 10 adres eklenebilir (her biri en çok 512 karakter). Yetkilendirme isteğindekiredirect_uriparametresi buradaki kayıtlardan biriyle birebir eşleşmelidir. Joker karakter (*),#fragment,kullanici@bilgisi veyanoluyo.net/*.noluyo.netalan adları kabul edilmez. İzin verilen üç biçim vardır:
- Web siteleri:
https://ile başlayan tam adres (https://ornek.site/giris/geri-donus). - Yerel geliştirme ve masaüstü geri dönüşü:
http://localhost,http://127.0.0.1veyahttp://[::1]. Port numarası çalışma anında esnektir: portalahttp://127.0.0.1/geri-donuskaydettiğinde çalışma anındahttp://127.0.0.1:5173/geri-donusveya rastgele seçilmiş yerel bir port kabul edilir. - Mobil ve masaüstü özel şema: Nokta içeren ters alan adı (
com.ornek.uygulama:/oauth/geri-donus;net.noluyoile başlayamaz).
- İstemci türü (
oauth_public):
- Gizli istemci (
oauth_public: false, varsayılan): Kendi arka uç sunucusu olan web siteleri içindir. Portaldan Secret oluştur / yenile düğmesiyle (POST /api/v1/applications/{application_id}/oauth-secret) birclient_secretüret ve sunucundaki.envdosyasında sakla. Secret yalnızca üretildiği anda tek sefer gösterilir. - Açık istemci (
oauth_public: true): Arka ucu olmayan tek sayfa uygulamalar (SPA), masaüstü uygulamaları ve mobil uygulamalar içindir. Kodu kullanıcının cihazında çalıştığı içinclient_secretkullanılmaz; güvenliği PKCE ve tam yönlendirme adresi sağlar.
- Yasal bağlantılar: İzin ekranında görünmesi için
privacy_urlveterms_urlalanlarınahttps://ile başlayan gizlilik politikası ve kullanım koşulları adreslerini ekle.
Yetkilendirme adresi ve parametreler
Kullanıcıyı tarayıcıda şu adrese yönlendirirsin:
https://noluyo.net/oauth2/authorize?client_id=123456789012345678&redirect_uri=https%3A%2F%2Fornek.site%2Fgiris%2Fgeri-donus&response_type=code&scope=identify+email+guilds&state=RastgeleDurumDegeri&code_challenge=PKCEOzetDegeri&code_challenge_method=S256
Her parametrenin tam karşılığı:
| Parametre | Zorunlu | Açıklama |
|---|---|---|
client_id | evet | Uygulamanın snowflake kimliği (application.id) |
redirect_uri | evet | Portalda kayıtlı yönlendirme adreslerinden biri (URL kodlu) |
response_type | evet | Her zaman code olmalıdır |
scope | evet | Boşlukla ayrılmış kapsam listesi; identify zorunludur (identify, identify email, identify email guilds) |
state | evet | Oturumuna bağladığın rastgele tahmin edilemez metin (en az 16 bayt rastgele veri önerilir) |
code_challenge | evet | code_verifier değerinin SHA-256 özeti (base64url, = dolgusuz) |
code_challenge_method | evet | Her zaman S256 olmalıdır; plain reddedilir |
PKCE ve state ne işe yarar
Hem gizli hem de açık istemcilerde PKCE (S256) ve state zorunludur:
code_verifiervecode_challenge(PKCE): Kullanıcıyı yönlendirmeden hemen önce 43–128 karakter uzunluğunda rastgele bir metin (code_verifier) üretip kullanıcının oturumunda saklarsın. Bunun SHA-256 özetinin base64url karşılığını (code_challenge) yetkilendirme adresine koyarsın. Kullanıcı kodla döndüğünde asılcode_verifierdeğerini/api/v1/oauth2/tokenucuna gönderirsin. Yolda kodu ele geçiren biricode_verifierdeğerini bilmediği için token alamaz.state: Başka birinin kendi yetkilendirme kodunu senin kullanıcının tarayıcısına enjekte etmesini (CSRF) engeller. Yönlendirmeden önce ürettiğin rastgelestatedeğerini HTTP-only çerezde ya da sunucu oturumunda sakla; geri dönüşte gelenstatebirebir eşleşmiyorsa işlemi hemen reddet.
Beş adımda giriş akışı
Tam çalışan web sitesi örneklerini depodaki sdks/examples/js/oauth-site ve sdks/examples/python/oauth_site klasörlerinde bulabilirsin.
1. PKCE değerlerini üret ve kullanıcıyı yönlendir
# PKCE ve state üretimi (OpenSSL ile örnek):
VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)
echo "Oturuma kaydet -> verifier=$VERIFIER state=$STATE"
echo "Tarayıcıda aç -> https://noluyo.net/oauth2/authorize?client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcb&response_type=code&scope=identify+email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
import { createHash, randomBytes } from "node:crypto";
const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const state = randomBytes(16).toString("hex");
// codeVerifier ve state değerlerini kullanıcının sunucu oturumuna kaydet.
const url = new URL("https://noluyo.net/oauth2/authorize");
url.searchParams.set("client_id", process.env.CLIENT_ID);
url.searchParams.set("redirect_uri", "http://127.0.0.1:3000/cb");
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "identify email");
url.searchParams.set("state", state);
url.searchParams.set("code_challenge", codeChallenge);
url.searchParams.set("code_challenge_method", "S256");
import base64, hashlib, os, secrets
from urllib.parse import urlencode
code_verifier = secrets.token_urlsafe(32)
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
state = secrets.token_hex(16)
# code_verifier ve state değerlerini kullanıcının sunucu oturumuna kaydet.
params = urlencode({
"client_id": os.environ["CLIENT_ID"],
"redirect_uri": "http://127.0.0.1:3000/cb",
"response_type": "code",
"scope": "identify email",
"state": state,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
})
authorize_url = f"https://noluyo.net/oauth2/authorize?{params}"
2. Geri dönüşü karşıla ve state kontrolü yap
Kullanıcı İzin ver düğmesine bastığında tarayıcı redirect_uri?code=...&state=... adresine, Vazgeç düğmesine bastığında ise redirect_uri?error=access_denied&state=... adresine döner. Yetkilendirme kodu 2 dakika geçerlidir ve tek kullanımlıktır.
# Tarayıcının döndüğü adres sorgusundan gelen değerler:
# ?code=nly_ac_...&state=...
# Önce gelen state değerinin 1. adımdaki $STATE ile birebir aynı olduğunu doğrula.
import { timingSafeEqual } from "node:crypto";
function checkCallback(query, session) {
const got = Buffer.from(String(query.state ?? ""));
const expected = Buffer.from(String(session.state ?? ""));
if (got.length === 0 || got.length !== expected.length || !timingSafeEqual(got, expected)) {
throw new Error("Geçersiz state");
}
if (query.error) {
throw new Error(`Kullanıcı vazgeçti: ${query.error}`);
}
return String(query.code);
}
import hmac
def check_callback(query: dict[str, str], session: dict[str, str]) -> str:
got = query.get("state", "")
expected = session.get("state", "")
if not got or not hmac.compare_digest(got, expected):
raise ValueError("Geçersiz state")
if "error" in query:
raise ValueError(f"Kullanıcı vazgeçti: {query['error']}")
return query["code"]
3. Kodu access token ile değiştir
POST https://noluyo.net/api/v1/oauth2/token ucuna application/x-www-form-urlencoded gövde gönderilir (en çok 8 KiB; JSON gövde gönderirsen 400 invalid_request alırsın). Gizli istemciler client_secret alanını form içinde ya da HTTP Basic başlığında iletir; açık istemciler (oauth_public: true) client_secret göndermez. Hız sınırı client_id ve IP başına dakikada 20 istektir.
curl -X POST "https://noluyo.net/api/v1/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=http://127.0.0.1:3000/cb" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET" \
--data-urlencode "code_verifier=$VERIFIER"
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "http://127.0.0.1:3000/cb",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET, // Açık istemcide bu satırı çıkar
code_verifier: session.codeVerifier,
});
const tokenRes = await fetch("https://noluyo.net/api/v1/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
const tokens = await tokenRes.json();
if (!tokenRes.ok) throw new Error(`${tokens.error}: ${tokens.error_description}`);
// tokens: { access_token, token_type: "Bearer", expires_in: 3600, refresh_token, scope }
import os, aiohttp
async with aiohttp.ClientSession() as http:
async with http.post(
"https://noluyo.net/api/v1/oauth2/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": "http://127.0.0.1:3000/cb",
"client_id": os.environ["CLIENT_ID"],
"client_secret": os.environ["CLIENT_SECRET"], # Açık istemcide çıkar
"code_verifier": session["code_verifier"],
},
) as resp:
tokens = await resp.json()
if resp.status != 200:
raise RuntimeError(f"{tokens['error']}: {tokens.get('error_description')}")
4. Kullanıcı profilini ve sunucu listesini oku
access_token 1 saat (expires_in: 3600) geçerlidir ve Authorization: Bearer <access_token> başlığıyla kullanılır (Bot önekiyle değil!). Okuma uçlarının hız sınırı 5 saniyede 30 istektir (oauth_read).
curl "https://noluyo.net/api/v1/oauth2/@me" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# "guilds" kapsamı istendiyse kullanıcının üye olduğu sunucular:
curl "https://noluyo.net/api/v1/oauth2/@me/guilds" \
-H "Authorization: Bearer $ACCESS_TOKEN"
const meRes = await fetch("https://noluyo.net/api/v1/oauth2/@me", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const me = await meRes.json();
if (!meRes.ok) throw new Error(me.code);
// me.user.id -> kendi veritabanındaki tekil anahtar!
async with aiohttp.ClientSession() as http:
async with http.get(
"https://noluyo.net/api/v1/oauth2/@me",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
) as resp:
me = await resp.json()
if resp.status != 200:
raise RuntimeError(me["code"])
user_id = me["user"]["id"]
5. Token yenile (rotasyon) ve çıkışta iptal et
refresh_token 30 gün geçerlidir ve her yenilemede döner (rotate olur): yanıt sana yeni bir access_token ve yeni bir refresh_token verir; hemen ikisini birden veritabanında güncelle. Eşzamanlı ağ isteklerinin birbirini kırmaması için eski refresh_token 30 saniyelik bir geçiş penceresi boyunca geçerli kalır; 30 saniye geçtikten sonra eski bir refresh_token tekrar gönderilirse çalıntı şüphesiyle o iznin tüm açık token'ları anında iptal edilir ve 400 invalid_grant döner.
Kullanıcı senin sitenden çıkış yaptığında ya da hesabını ayırdığında POST /api/v1/oauth2/revoke çağrısıyla token'ı iptal et (her zaman 200 döner):
# Yenileme:
curl -X POST "https://noluyo.net/api/v1/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET"
# İptal etme:
curl -X POST "https://noluyo.net/api/v1/oauth2/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "token=$REFRESH_TOKEN" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET"
// Yenileme:
const refreshed = await fetch("https://noluyo.net/api/v1/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: storedRefreshToken,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
}).then((r) => r.json());
// Çıkışta iptal:
await fetch("https://noluyo.net/api/v1/oauth2/revoke", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
token: refreshed.refresh_token,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
});
async with aiohttp.ClientSession() as http:
# Yenileme:
async with http.post(
"https://noluyo.net/api/v1/oauth2/token",
data={
"grant_type": "refresh_token",
"refresh_token": stored_refresh_token,
"client_id": os.environ["CLIENT_ID"],
"client_secret": os.environ["CLIENT_SECRET"],
},
) as resp:
refreshed = await resp.json()
# Çıkışta iptal:
await http.post(
"https://noluyo.net/api/v1/oauth2/revoke",
data={
"token": refreshed["refresh_token"],
"client_id": os.environ["CLIENT_ID"],
"client_secret": os.environ["CLIENT_SECRET"],
},
)
Kapsamlar ve dönen alanlar
N'oluyo kasıtlı olarak yalnızca üç salt-okunur kapsam sunar; mesaj okuma, mesaj yazma ya da sunucuya sokma kapsamı yoktur:
| Kapsam | Zorunlu | Döndürdüğü uç ve alanlar |
|---|---|---|
identify | evet | GET /api/v1/oauth2/@me içinde application: {id, name}, scopes, expires_at ve user: {id, username, display_name, avatar} |
email | hayır | GET /api/v1/oauth2/@me içindeki user nesnesine email (string veya null) ve email_verified (boolean) ekler |
guilds | hayır | GET /api/v1/oauth2/@me/guilds ucunu açar; her sunucu için {id, name, icon, owner, permissions} dizisi döndürür (permissions dizgi olarak izin bit maskesidir) |
Açık istemciler ve CORS
Tarayıcıda çalışan tek sayfa uygulamalar (SPA), Electron/Tauri masaüstü programları ve mobil uygulamalar kaynak kodunda gizli anahtar saklayamaz. Bu uygulamalarda:
- Portalda Açık istemci (
oauth_public) seçeneğini aç ve istemci koduna hiçbir zamanclient_secretgömme. /api/v1/oauth2/token,/api/v1/oauth2/revoke,/api/v1/oauth2/@meve/api/v1/oauth2/@me/guildsuçlarıAccess-Control-Allow-Origin: *(çerezsiz CORS) başlığıyla tarayıcıdan doğrudan çağrılabilir.- Tarayıcı uygulamasında
access_tokendeğerinilocalStorageyerine bellek içinde tut; masaüstü ve mobilde işletim sisteminin güvenli anahtar zincirini (Keychain / Keystore / Credential Locker) kullan.
Hata tablosu
/api/v1/oauth2/token ve /revoke uçları RFC 6749 biçiminde {"error": "...", "error_description": "..."} döndürür; diğer tüm uçlar standart {"code": "...", "message": "..."} zarfını kullanır:
| Durum | Kod / error | Neden ve çözüm |
|---|---|---|
| 400 | invalid_request | Form kodlaması (application/x-www-form-urlencoded) kullanılmadı, PKCE S256 eksik ya da zorunlu alan boş. |
| 401 | invalid_client | client_id veya client_secret yanlış, ya da gizli istemcide secret gönderilmedi. |
| 400 | invalid_grant | Yetkilendirme kodunun 2 dakikalık süresi doldu, kod ikinci kez kullanıldı, redirect_uri veya code_verifier eşleşmedi ya da 30 saniyeyi geçmiş eski bir refresh_token tekrar gönderildi. Kullanıcıdan yeniden giriş iste. |
| 400 | unsupported_grant_type | grant_type alanı authorization_code veya refresh_token dışında bir değer. |
| 400 | invalid_scope | İstenen kapsam tanınmıyor veya identify eklenmemiş. |
| 400 | oauth_unknown_client | Yetkilendirme ekranındaki client_id bulunamadı veya uygulama durdurulmuş. |
| 400 | oauth_bad_redirect | redirect_uri portalda kayıtlı adreslerle eşleşmiyor. Portaldaki listeyi kontrol et. |
| 400 | oauth_bad_request | response_type=code, state, code_challenge veya code_challenge_method=S256 eksik/geçersiz. |
| 403 | oauth_user_cap | Doğrulanmamış uygulamanın 100 kullanıcı sınırı doldu. Doğrulama başvurusu yap. |
| 403 | guest_not_allowed | Misafir hesapla giriş denendi; kullanıcının kalıcı hesap açması gerekir. |
| 401 | invalid_oauth_token | access_token süresi doldu, iptal edildi ya da kullanıcı şifresini değiştirdi. refresh_token ile yenile; o da reddedilirse yeniden giriş ekranına yönlendir. |
| 403 | insufficient_scope | /oauth2/@me/guilds çağrıldı ama izin alınırken guilds kapsamı istenmemişti. |
Oturum iptali, şifre değişimi ve hesap silme
Token yaşam döngüsü kullanıcının hesap güvenliği hareketlerini anında izler:
- Kullanıcı erişimi kaldırdığında: Kullanıcı Ayarlar → Bağlı uygulamalar ekranından uygulamanı kaldırdığında tüm
access_tokenverefresh_tokenkayıtları anında silinir; bir sonraki@meçağrısı401 invalid_oauth_tokendöndürür. - Şifre değişimi veya sıfırlama: Kullanıcı şifresini değiştirdiğinde ya da sıfırladığında açık
access_tokenverefresh_tokendeğerleri iptal olur (uygulama Bağlı uygulamalar listesinde kalır; kullanıcı sitene geldiğinde yeniden giriş yapar). "Diğer cihazlardan çık" düğmesi ve iki aşamalı doğrulama değişiklikleri OAuth2 token'larını düşürmez. - Hesap veya uygulama kapanışı: Kullanıcı N'oluyo hesabını sildiğinde/dondurduğunda ya da sen uygulamayı sildiğinde/durdurduğunda tüm token'lar derhal geçersiz kalır.
Güvenlik listesi ve yasal yükümlülüklerin
Yayına çıkmadan önce şu maddeleri doğrula:
client_secrethiçbir zaman tarayıcı koduna, mobil pakete veya Git deposuna girmez; yalnız arka uç.envdosyasında durur.- Her giriş isteğinde yeni bir
statevecode_verifierüretilir ve geri dönüştestatedoğrulanmadan kod takas edilmez. - Portalda yalnızca gerçekten kullandığın tam
redirect_uriadresleri kayıtlıdır. - Kullanıcı kimliği yalnızca
user.idüzerinden tutulur veuser.email === nulldurumu hatasız ele alınır. - Yasal sorumluluk: N'oluyo ile giriş üzerinden kendi sistemine çektiğin veriler için kendi kullanıcılarına karşı bağımsız veri sorumlusun. Portalda doğru bir gizlilik politikası (
privacy_url) yayınlamalı ve sitende hesabını silen kişinin senden tutulmasını istemediği OAuth2 token'larını/oauth2/revokeile iptal edip verilerini silmelisin.