@agdsoftware/sso-sdk
v2.7.2
Published
Keycloak/OIDC tabanlı merkezi yetkilendirme SDK'sı — evrensel izin istemcisi, sunucuda JWT doğrulama, web ve mobil için PKCE, NestJS guard zinciri ve React hook'ları; tek paket, ayrı girişler.
Downloads
1,144
Maintainers
Readme
@agdsoftware/sso-sdk
Kimliği Keycloak/OIDC'den, yetkiyi merkezi bir IAM servisinden alan uygulamalar için tek SDK — Node, tarayıcı, React Native.
Tek cümle: token "bu kişi kim" der, IAM "ne yapabilir" der. İkisi ayrıdır ve ayrı kalmalıdır.
Girişler
Paket tektir, girişleri ayrıdır. Her alt yol yalnız kendi ortamının
ağırlığını taşır: bir React uygulaması @nestjs/* çözümlemez, bir NestJS
API'si oidc-client-ts indirmez, mobil hiçbirini görmez.
| Giriş | Ortam | İçerik |
|---|---|---|
| @agdsoftware/sso-sdk | evrensel | tipler, hatalar, IamClient, AuthzStore, SsoSession, kapsam yardımcıları |
| @agdsoftware/sso-sdk/server | yalnız sunucu | createTokenVerifier (jose + JWKS) |
| @agdsoftware/sso-sdk/oidc | tarayıcı + mobil | PKCE, keşif, çıkış adresi — kütüphanesiz |
| @agdsoftware/sso-sdk/nestjs | NestJS API'ler | guard zinciri, dekoratörler, /sso/me, sağlık ucu |
| @agdsoftware/sso-sdk/nestjs/typeorm | NestJS + TypeORM | satır kapsamı için query-builder yardımcısı |
| @agdsoftware/sso-sdk/nestjs/testing | testler | sahte SsoContext |
| @agdsoftware/sso-sdk/react | React arayüzler | SsoProvider, izin hook'ları, HTTP köprüleri |
Kök giriş fetch dışında hiçbir şeye bağlı değildir. Bölünmenin sebebi tek
cümle: token'ı mobil taşır, sunucu doğrular.
Kurulum
npm i @agdsoftware/sso-sdkÇerçeve bağımlılıkları opsiyonel peer'dır — kullandığınız girişin istediğini siz kurarsınız, ötekiler hiç inmez:
# NestJS API
npm i @agdsoftware/sso-sdk @nestjs/common @nestjs/core reflect-metadata rxjs
npm i typeorm # yalnız /nestjs/typeorm kullanacaksanız
# React web
npm i @agdsoftware/sso-sdk react oidc-client-ts
# Mobil / saf Node — hiçbir ek paket gerekmez
npm i @agdsoftware/sso-sdkRegistry'siz ortamda: pnpm sdk:pack → npm i ./vendor/agdsoftware-sso-sdk-*.tgz
Sunucu (Express, Fastify, script)
import { IamClient, bearerToken } from '@agdsoftware/sso-sdk';
// Token doğrulama SUNUCUYA özgüdür (jose): kök giriş evrensel kalsın,
// mobil/tarayıcı onu bundle'a almasın diye `/server` altındadır.
import { createTokenVerifier } from '@agdsoftware/sso-sdk/server';
const verify = createTokenVerifier({
issuer: process.env.SSO_ISSUER!, // http://localhost:8080/realms/agd
audience: process.env.SSO_AUDIENCE!, // SSO ekibinin verdiği audience
});
const iam = new IamClient({ baseUrl: process.env.IAM_URL! });
// istek başına
const token = bearerToken(req.headers.authorization);
const claims = await verify(token!); // 401 riski burada
await iam.assert(token!, groupId, 'ornek:kayit:onayla'); // 403 riski buradagroupId kullanıcının aktif birimidir; iam.me(token) ile listelenir ve
kullanıcıya seçtirilir (birden fazla birimde görevli olabilir).
import { bearerToken, IamClient, ForbiddenError, SsoTokenError } from '@agdsoftware/sso-sdk';
import { createTokenVerifier } from '@agdsoftware/sso-sdk/server';
const authenticate = async (req, res, next) => {
try {
const token = bearerToken(req.headers.authorization);
if (!token) throw new SsoTokenError('Token yok');
req.token = token;
req.user = await verify(token);
next();
} catch (error) {
next(error);
}
};
const requirePermission = (permission: string) => async (req, res, next) => {
try {
// Bağlam birimi istemciden gelir ama YETKİ IAM'de doğrulanır:
// uydurulmuş bir groupId yalnız "yetkin yok" cevabı üretir.
await iam.assert(req.token, req.header('X-Organization-Group-Id'), permission);
next();
} catch (error) {
next(error);
}
};
app.get('/kayitlar', authenticate, requirePermission('ornek:kayit:goruntule'), handler);
app.use((error, req, res, next) => {
if (error instanceof SsoTokenError) return res.status(401).json({ code: 'UNAUTHENTICATED' });
if (error instanceof ForbiddenError) return res.status(403).json({ code: 'FORBIDDEN' });
next(error);
});NestJS
Kendi guard'ınızı yazmayın; zincir hazır: kimlik → bağlam → karar.
1. Modül
import { SsoModule } from '@agdsoftware/sso-sdk/nestjs';
@Module({
imports: [
// SSO_ISSUER · SSO_AUDIENCE · IAM_BASE_URL okunur ve DOĞRULANIR.
// Eksik varsa süreç AÇILIŞTA durur — eksik yapılandırmayla ayağa kalkıp
// ilk isteğe kadar sağlıklı görünmek, sorunu üretimde bulmaktır.
SsoModule.forRootFromEnv({
publicMetadataKeys: ['isPublic'], // var olan @Public() dekoratörünüz
}),
],
})
export class AppModule {}forRoot({ issuer, audience, iamBaseUrl, … }) ve forRootAsync({ useFactory })
de durur.
2. Controller
@Controller('toplantilar')
export class MeetingsController {
@Get()
@RequirePermission('mgkys:toplanti:goruntule')
list(@CurrentContext() ctx: SsoContext) { ... }
@Get('profilim')
@SelfScoped() // izin yok, kimlik yeter
me(@CurrentSsoUser() user: SsoIdentity) { ... }
}Dekoratör taşımayan handler 403 alır (fail-closed). Unutulan uç açık kalmaz.
3. Satır kapsamı
Her kaydınıza sahibi birimin Keycloak yolunu yazın (GroupPath = /İzmir/Konak):
import { applyGroupScope } from '@agdsoftware/sso-sdk/nestjs/typeorm';
import { ownerGroupPath, assertInScope } from '@agdsoftware/sso-sdk/nestjs';
applyGroupScope(qb, ctx); // listeleme: /İzmir → /İzmir ve altı
meeting.GroupPath = ownerGroupPath(ctx); // oluşturma: istemciden gelen birim kabul edilmez
assertInScope(ctx, meeting.GroupPath); // güncelleme/silme: okuma filtresi tek başına yetmezDekoratörler
| Dekoratör | Anlamı |
|---|---|
| @RequirePermission(code) | Bu izin yoksa 403 |
| @RequireAnyPermission(a, b) | Herhangi biri yeter (VEYA) |
| @PermissionResource('mgkys:toplanti') | Sınıf seviyesi; devralınan add/get/getAll/update/delete için izni metot adından türetir |
| @SelfScoped() | Kimlik yeter; handler yalnız @CurrentSsoUser() kimliğini kullanmalı |
| @AnyAuthenticated() | Kimlik yeter; yanıt kişiye göre değişmez (referans veriler) |
| @SsoPublic() | Token bile gerekmez |
| @CurrentContext() / @CurrentSsoUser() / @CurrentGroup() | Parametre dekoratörleri |
@RequirePermission sınıf seviyesinde yasaktır (TypeError): sınıfa yazılan
izin sonradan eklenen her handler'a sessizce miras kalır.
Genişletme noktaları
@Global()
@Module({
providers: [
{ provide: SSO_USER_RESOLVER, useClass: UsersLinkService }, // sub → Users.Id
{ provide: SSO_CACHE, useClass: RedisSsoCache }, // yoksa süreç içi bellek
],
exports: [SSO_USER_RESOLVER, SSO_CACHE],
})
export class SsoBridgeModule {}SsoUserResolver— SSO kullanıcısını kendi tablonuza bağlar.nulldönerse istek403 SSO_USER_NOT_LINKEDalır (sessizce yeni kullanıcı sayılmaz).SsoCacheAdapter— çok instance'lı kurulumda Redis verin.
Katalog doğrulaması
// permissions/catalog.ts (dosyanın sonunda)
registerPermissionCatalog((code) => CODE_SET.has(code));Katalogda olmayan bir kod açılışta TypeError verir. Yakalanmazsa o uç sessizce herkese kapanır ve 403'ün nedeni aylarca aranır.
Testler
Guard'ları mock'lamayın — mock'lanmış guard her zaman "geçti" der ve testler yetki hatalarını göremez. Bağlamı kurgulayın:
import { createTestSsoContext } from '@agdsoftware/sso-sdk/nestjs/testing';
const ctx = createTestSsoContext({
permissions: ['mgkys:toplanti:goruntule'],
group: { path: '/İzmir/Konak', unitType: 'ILCE' },
});React
Arayüzde izin kontrolü görgü kuralıdır, güvenlik sınırı değil: gizlenen buton isteği elle atmayı engellemez. Gerçek kontrol her zaman API'dedir.
import { SsoProvider, RequireSso, ssoConfigFromEnv } from '@agdsoftware/sso-sdk/react';
// VITE_SSO_ISSUER · VITE_SSO_CLIENT_ID · VITE_SSO_REDIRECT_URI · VITE_API_BASE_URL
// (Next.js: ssoConfigFromEnv(process.env, 'NEXT_PUBLIC_'))
<SsoProvider config={ssoConfigFromEnv(import.meta.env)} fallback={<Splash />}>
<RequireSso loading={<Splash />}>
<App />
</RequireSso>
</SsoProvider>redirectUri tam adres olmalı — joker (http://localhost:5173/*) SSO
tarafında IAM_CLIENT_WILDCARD_FORBIDDEN ile reddedilir (açık yönlendirme riski).
İzinler
const can = usePermission('mgkys:toplanti:guncelle');
if (can === 'loading') return <Skeleton />; // ÖNCE bunu yazın
return can ? <EditButton /> : null;'loading' truthy bir string'tir: {can && <EditButton/>} yazmak butonu
yükleme sırasında gösterir. Üç durum bilerek var — iki duruma indirmek ilk
boyamada menüyü zıplatır.
<Can permission="mgkys:toplanti:olustur" loading={<Skeleton/>}>
<NewMeetingButton />
</Can>
// kaynak-kısıtlı yetki
const canEdit = useResourcePermission('mgkys:toplanti:guncelle', 'toplanti', meeting.id);Aktif birim
Kullanıcı birden fazla birimde görevli olabilir; izinler birime bağlıdır.
const { groups, activeGroup, setActiveGroup } = useGroups();
<select value={activeGroup?.id} onChange={(e) => setActiveGroup(e.target.value)}>
{groups.map((g) => <option key={g.id} value={g.id}>{g.path}</option>)}
</select>Birim değişince izinler otomatik yeniden çekilir.
Aktif birimin tipli künyesi
Yol (/İzmir/Konak/Medya Komisyonu) hangi halkanın ne olduğunu söylemez; sunucu
her grubu tipli döker, hook hazır okur:
const units = useActiveUnits();
// { IL: 'İzmir', ILCE: 'Konak', KOMISYON: 'Medya Komisyonu' } | null
<p>{units?.IL} · {units?.ILCE ?? 'tüm il'}</p>Çekirdek yardımcıları her ortamda çalışır: unitOf(grup, 'MAHALLE'),
unitsOf(grup), describeUnits(grup), ORG_UNIT_LABELS.
İstekler ve çıkış
const ssoFetch = useSsoFetch();
await ssoFetch('/api/toplantilar');
const { getAuthHeaders, logout } = useSso();
useEffect(() => attachSsoToAxios(api, getAuthHeaders), [getAuthHeaders]);İkisi de Authorization: Bearer … ve X-Organization-Group-Id ekler. logout
OIDC end-session ucuna id_token_hint ile gider: Keycloak oturumu da kapanır.
Yalnız yerel state'i temizlemek, kullanıcının bir sonraki girişte şifresiz
geri dönmesi demektir — SSO'da "çıktım" yanılsamasının klasik kaynağı.
| Hook / Bileşen | İş |
|---|---|
| useSso() | Durum, kullanıcı, birimler, izinler, login/logout/refresh |
| usePermission(code) | 'loading' \| true \| false |
| useAnyPermission(...codes) | Herhangi biri |
| useResourcePermission(code, type, id) | Kayıt bazlı yetki |
| useGroups() | Birim listesi + aktif birim + değiştirme |
| useActiveUnits() | Aktif birimin tipli künyesi ({IL, ILCE, MAHALLE, …}) |
| useSsoFetch() | Başlıkları ekleyen fetch |
| attachSsoToAxios(instance, getAuthHeaders) | axios interceptor (axios'a bağımlılık yok) |
| <RequireSso> | Oturum kurulana kadar bekletir, yoksa girişe yollar |
| <Can permission> | İzin varsa çocukları gösterir |
Mobil (React Native / Expo)
Mobilde iframe yoktur, yönlendirme özel şemayla döner ve tarayıcı OIDC
kütüphaneleri ya çalışmaz ya ağır gelir. /oidc girişi bunun için var —
kütüphanesiz, tek ihtiyacı sistem tarayıcısını açacak bir fonksiyon:
import { discover, createPkce, buildAuthorizeUrl, parseCallback, exchangeCode }
from '@agdsoftware/sso-sdk/oidc';
const endpoints = await discover(ISSUER);
const pkce = await createPkce();
const url = buildAuthorizeUrl({ endpoints, clientId, redirectUri, pkce });
const returned = await WebBrowser.openAuthSessionAsync(url, redirectUri); // Expo
const tokens = await exchangeCode({
endpoints, clientId, redirectUri, code: parseCallback(returned.url).code!, pkce,
});Parola akışı (direct access grant) kullanılmaz: uygulamanın kullanıcı
parolasını görmesi, MFA'yı ve giriş ekranındaki bot korumasını birlikte devre
dışı bırakır. Ortam gereksinimi: crypto.getRandomValues + crypto.subtle
(react-native-get-random-values ve expo-crypto).
Ayrıntı: docs/MOBIL.md
Ortak kavramlar
Birden fazla izin bakacaksanız
Tek tek check yerine bir kez effectivePermissions alın — tek gidiş-dönüş:
const effective = await iam.effectivePermissions(token, groupId);
const canApprove = effective.permissions.includes('ornek:kayit:onayla');İstemci 30 sn yerel cache tutar (cacheTtlMs ile değiştirilir, 0 kapatır).
İl / ilçe filtrelemesi
iam.me(token) her birim için türü ve türetilmiş il/ilçe yolunu verir:
{ "path": "/İzmir/Konak/Eğitim", "unitType": "KOMISYON",
"il": "/İzmir", "ilce": "/İzmir/Konak" }ilce boşsa kullanıcı il seviyesindedir — "tüm il" demektir.
Kaynak (kayıt) bazlı yetki
Bir yetki tek tek kayıtlara kısıtlanabilir. Kısıtlı izin genel sete girmez:
const allowed = canAccessResource({
grants: effective.resourceGrants,
permission: 'ornek:kayit:guncelle',
resourceType: 'kayit',
resourceId: record.id,
recordGroupPath: record.groupPath, // ZORUNLU
contextGroupPath: activeGroup.path,
});recordGroupPath kontrolü zorunludur: kaynaklar IAM için opaktır, kaydın
gerçekten o birime ait olduğunu IAM doğrulayamaz.
Hata sözleşmesi
| Durum | Kod | Anlamı |
|---|---|---|
| 401 | SSO_TOKEN_MISSING / SSO_TOKEN_INVALID | Kimlik yok/geçersiz |
| 403 | SSO_USER_NOT_LINKED | Kimlik var, uygulamada karşılığı yok |
| 403 | SSO_GROUP_NOT_MEMBER | İstenen birimin üyesi değil |
| 403 | IAM_PERMISSION_DENIED | Yetki yok (hangi izin olduğu söylenmez) |
| 403 | IAM_SCOPE_DENIED / IAM_RESOURCE_DENIED | Kayıt yetki alanı dışında |
| 503 | IAM_UNAVAILABLE | IAM'e ulaşılamadı — yetkisizlik değildir |
503'ü 403'e katlamayın: bir kesintiyi yetki hatası gibi göstermek ekibi günlerce yanlış yerde arattırır.
Sık yapılan hatalar
| Hata | Belirti | Çözüm |
|---|---|---|
| issuer tam eşleşmiyor | unexpected "iss" claim value | Token'daki iss ne ise birebir onu verin — localhost yerine 127.0.0.1 bile yeter |
| audience doğrulanmıyor | Sessiz güvenlik açığı | Başka uygulamanın token'ı kabul edilir; SSO ekibinden audience isteyin |
| Yetkiyi token'dan okumak | Süresi dolan yetki 5 dk yaşar | Karar daima iam.check() / effectivePermissions() |
| Yerel izin cache'i 60 sn'yi aşıyor | Geri alınan yetki o kadar yaşar | TTL'i kısa tutun |
| X-Organization-Group-Id'ye güvenmek | Yetki yükseltme | Paket her istekte kullanıcının IAM'deki birim listesiyle doğrular |
1.x'ten geçiş
SDK 2.0.0'da üç paket tek pakette birleşti. Kod değişmedi; yalnız import yolları değişti:
| 1.x | 2.x |
|---|---|
| @agdsoftware/sso-client | @agdsoftware/sso-sdk |
| @agdsoftware/sso-client/server | @agdsoftware/sso-sdk/server |
| @agdsoftware/sso-client/oidc | @agdsoftware/sso-sdk/oidc |
| @agdsoftware/nestjs-sso | @agdsoftware/sso-sdk/nestjs |
| @agdsoftware/nestjs-sso/typeorm | @agdsoftware/sso-sdk/nestjs/typeorm |
| @agdsoftware/nestjs-sso/testing | @agdsoftware/sso-sdk/nestjs/testing |
| @agdsoftware/react-sso | @agdsoftware/sso-sdk/react |
Tek davranış farkı: oidc-client-ts artık opsiyonel peer'dır. React web
kullanıyorsanız onu kendiniz kurarsınız; NestJS ve mobil tarafında artık hiç
inmez.
Arka plan yetki yenilemesi — 2.7.0
React refreshIntervalMs zamanlayıcısı aynı hazır bağlamı arka planda yeniler;
başarılı aynı bağlam yanıtı form ağacını yükleme ekranıyla değiştirmez.
Çekirdekte açık kullanım store.refresh({ background: true }) biçimindedir.
Parametresiz refresh() ve birim değişimi eski yükleme davranışını korur.
Token kaybı veya son hata, eski izinleri, grant'leri ve aktif kapsamı temizler.
Tüketici; aktör, yetki sürümü, izinler veya kayıt kapsamı değiştiğinde kendi sorgu cache'ini ve korumalı ekran durumunu geçersizleştirmelidir. Aynı hazır bağlamın bekleme sırasında korunması API yetkilendirmesinin yerine geçmez. Yoklama aralığı, sunucu cache süresi ve ağ gecikmesi birlikte değerlendirilir; anlık iptal veya belli bir kapasite garantisi verilmez.
İlgili
- SDK rehberi — kurulum, dayanıklılık ve terminal yayın yolu
- Entegrasyon sözleşmesi
- Token sözleşmesi ve client kaydı
- MGSoft — API/web/native tüketici
- AGD Destek — API/React tüketici
2.7.1 düzeltmesi: yetki yenilemesinin token alma, HTTP/gövde okuma ve tekrarlar dahil toplam süresi 10 saniyeyle sınırlıdır. Süre dolunca eski yetki temizlenir; abort'u dikkate almayan adaptörün geç yanıtı bağlamı geri getiremez.
