
# Express oturumları

`@keenauth/node/bff` bir Express 4 veya 5 uygulamasına kimlik doğrulama ekler. Sunucunuz OAuth istemcisidir ve tüm belirteçleri sunucuda tutar. Tarayıcı yalnızca şifrelenmiş, HttpOnly bir oturum çerezi alır; hiçbir erişim, yenileme veya ID belirtecini görmez. Bu, arka uç-ön uç (BFF) desenidir. Paket Express'i içe aktarmaz, dolayısıyla ek bir bağımlılık eklemez. Henüz npm'de yayımlanmamıştır.

Bearer belirteçleri alan bir API'yi korumak için bunun yerine [API'nizi koruma](/docs/sdk/protect-api) bölümüne bakın. Yönetim API'sini çağırmak için [Node.js yönetim SDK'sına](/docs/sdk/node) bakın.

## Başlamadan önce

1. Panelde, ortamınızda bir web uygulaması oluşturun.
2. İstemci kimlik doğrulamasını seçin: bir istemci sırrı (`client_secret_basic`) veya genel anahtarını uygulamaya kaydettiğiniz bir anahtar çifti ile `private_key_jwt`. Paylaşılan bir sır sunucunuzdan çıkmadığı için `private_key_jwt` önerilir.
3. `https://app.acme.example/auth/callback` yönlendirme URI'sini ve `https://app.acme.example/` çıkış sonrası yönlendirme URI'sini ekleyin.
4. En az 32 karakterlik bir çerez sırrı üretin, örneğin `openssl rand -base64 32` ile. Bunu depoda değil, gizli bilgi deponuzda saklayın.

## Kurulum

```ts
import express from "express";
import { createBff, expressRequireAuth, expressSession } from "@keenauth/node/bff";

const bff = createBff({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "client_secret_basic", clientSecret: process.env.KEENAUTH_CLIENT_SECRET! },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});

const app = express();
app.use(expressSession(bff));

app.get("/orders", expressRequireAuth(bff), (req, res) => {
  const { session } = req.keenauth!;
  res.send(`Signed in as ${session!.user.email}`);
});
```

`private_key_jwt` kullanırken sır yerine özel anahtarı geçirin. SDK, belirteç ve PAR uç noktasına yapılan her istek için yeni bir istemci onay belirteci imzalar (`aud` issuer'dır, `jti` rastgeledir, 120 saniye geçerlidir, `kid` anahtarın RFC 7638 parmak izidir). Algoritma anahtarı izler: RSA için `RS256` (veya `alg: "PS256"`), P-256 için `ES256` ve Ed25519 için `EdDSA`.

```ts
import { readFileSync } from "node:fs";

const bff = createBff({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
```

Anahtar bir KMS veya HSM içinde tutuluyorsa, bunun yerine `clientAuth: { method: "private_key_jwt", assertion }` geçirin; burada `assertion()` her seferinde yeni bir imzalı istemci onay belirteci döndürür. `privateKey` veya `assertion` değerlerinden yalnızca birini geçirin, ikisini birden değil.

`appBaseUrl`, uygulamanızın herkese açık adresidir. SDK yönlendirme URI'sini ve tüm yönlendirmeleri bundan oluşturur; isteğin `Host` veya `X-Forwarded-*` başlıklarından asla oluşturmaz, bu yüzden `trust proxy` ayarı buna etki etmez. Düz `http` yalnızca `localhost` için kabul edilir.

Seçenekler `createBff` çalıştığında denetlenir; dolayısıyla bir yapılandırma hatası uygulamayı başlangıçta durdurur.

## Rotalar

`expressSession` üç rota sunar ve diğer her istekte oturumu okur:

| Rota | Ne yapar |
|---|---|
| `GET /auth/login` | Oturum açmayı başlatır. İsteğe bağlı sorgu parametreleri: `returnTo` (kullanıcının geleceği yer) ve `organization` (bir kuruluşta oturum açmak için). |
| `GET /auth/callback` | Oturum açmayı tamamlar ve oturum çerezini ayarlar. |
| `GET` veya `POST /auth/logout` | KeenAuth'ta oturumu kapatır ve uygulamanıza geri döner. |

Oturum açma, PKCE (S256) ile yetkilendirme kodu akışını kullanır. Ortamınız sunduğunda istek sunucuya gönderilir (PAR). Geri çağırmada SDK `state`, `iss` ve ID belirtecini, `nonce` dahil, denetler. Her başarılı oturum açma yeni bir oturum oluşturur; eski oturum atılır.

`returnTo`, `/orders?tab=open` gibi uygulamanıza ait bir yol olmalıdır. `//evil.example` veya tam bir URL gibi başka değerler yok sayılır ve kullanıcı `/` adresine gelir.

## Rotaları koruma

`expressRequireAuth(bff)`, bir isteğin yalnızca oturumla geçmesine izin verir:

- Oturumu olmayan bir sayfa isteği (`GET` veya `HEAD`), `returnTo` ile birlikte `/auth/login` adresine yönlendirilir;
- `/api/…` yoluna yapılan bir istek veya başka bir yöntemdeki istek, bir sorun belgesiyle birlikte `401` alır. Her yolda `401` yanıtı vermek için `{ api: true }` geçirin.

`POST`, `PUT`, `PATCH`, `DELETE` ve diğer güvensiz yöntemlerde tarayıcı isteğin uygulamanızdan geldiğini belirtmelidir: `Sec-Fetch-Site: same-origin` veya uygulamanızın kaynağına eşit bir `Origin` başlığı. Diğer istekler `403` alır. Bu, `SameSite=Lax` ile birlikte siteler arası istek sahteciliğine karşı koruma sağlar.

## Oturumu kullanın ve API'nizi çağırın

`req.keenauth` şunları içerir:

- `session`: `null` veya oturum açmış kullanıcı. `session.user`, `subject`, `email`, `name`, `orgId` ve `orgRoles` gibi tip tanımlı ID belirteci taleplerine sahiptir; tüm talepler `session.user.claims` içindedir. Oturum nesnesi asla bir belirteç içermez.
- `accessToken`: sunucu tarafı çağrılar için geçerli bir erişim belirteci veya `null`.

```ts
app.get("/api/orders", expressRequireAuth(bff), async (req, res) => {
  const upstream = await fetch("https://api.acme.example/v1/orders", {
    headers: { Authorization: `Bearer ${req.keenauth!.accessToken}` },
  });
  res.status(upstream.status).json(await upstream.json());
});
```

TypeScript'te özelliği bir kez tanımlayın:

```ts
import type { SessionContext } from "@keenauth/node/bff";

declare module "express-serve-static-core" {
  interface Request {
    keenauth?: SessionContext | null;
  }
}
```

Erişim belirtecinin süresi 60 saniye içinde dolacaksa SDK, işleyiciniz çalışmadan önce onu yeniler. Her yenileme, yenileme belirtecini döndürür ve yanıtta yeni oturum çerezini ayarlar. Sunucu yenileme belirtecini reddederse oturum sona erer. Geçici bir sunucu hatası oturumu değiştirmeden bırakır.

Erişim belirtecini tarayıcıya göndermeyin.

## Kuruluş değiştirin

`bff.switchOrganizationPath("org_01j9…", "/dashboard")`, kullanıcıyı o kuruluşta yeniden oturum açtıran bir `/auth/login` bağlantısı döndürür. KeenAuth üyeliği denetler. Oturum açtıktan sonra `session.user.orgId` kuruluşu tutar.

## Oturumu kapatın

Bir formdan `/auth/logout` adresine `POST` gönderin. SDK şunları yapar:

1. oturum çerezini siler (ve bir depo kullanıyorsanız saklanan oturumu da);
2. yenileme belirtecini iptal eder;
3. tarayıcıyı, sizi uygulamanıza geri döndüren ortamınızın çıkış uç noktasına gönderir.

Tarayıcının başka bir siteden geldiğini işaretlediği bir istek reddedilir. `Sec-Fetch-Site` göndermeyen tarayıcılar, oturumu yalnızca kendi sayfalarınızdan gelen bir `POST` ile kapatabilir.

## Oturum depolama

Varsayılan olarak tüm oturum çerezde bulunur; `secret` değerinden türetilmiş bir anahtarla AES-256-GCM kullanılarak şifrelenir ve kimlik doğrulaması yapılır. Değiştirilmiş veya kopyalanmış bir çerez reddedilir. Çerez `__Host-keenauth_session.0` adını taşır ve `Secure`, `HttpOnly`, `SameSite=Lax` ve `Path=/` ile ayarlanır. `http://localhost` üzerinde `__Host-` öneki ve `Secure` kaldırılır. Büyük oturumlar en fazla üç çereze bölünür.

Belirteçleri sunucuda tutmak için bir depo geçirin:

```ts
import { createBff, type SessionStore } from "@keenauth/node/bff";

const store: SessionStore = {
  get: async (id) => JSON.parse((await redis.get(`sess:${id}`)) ?? "null"),
  set: async (id, session, ttl) => void (await redis.set(`sess:${id}`, JSON.stringify(session), { EX: ttl })),
  delete: async (id) => void (await redis.del(`sess:${id}`)),
};
const bff = createBff({ /* … */ store });
```

Bu durumda çerez yalnızca şifrelenmiş rastgele bir oturum kimliği içerir. Her oturum açma yeni bir kimlik oluşturur ve önceki kaydı siler. Depo belirteçleri tuttuğu için erişimi kısıtlayın. `MemorySessionStore` yalnızca geliştirme ve testler içindir.

Bir oturum en fazla 7 gün sürer (`sessionMaxAge`); yenileme bu süreyi uzatmaz. Sırrı değiştirmek için bir liste geçirin: `secret: [newSecret, oldSecret]`. Eskisini `sessionMaxAge` süresinden sonra kaldırın.

## Bugünkü sınırlar

- DPoP ile bağlanmış belirteçler ve arka kanal çıkışı henüz işlenmiyor.
- Yenilemeler yalnızca tek bir sunucu süreci içinde paylaşılır. Birden fazla örnekte, `refresh_reuse_interval_s` uygulama ayarı (varsayılan 5 saniye) paralel bir yenilemenin başarılı olmasına izin verir; bunu `0` olarak ayarlarsanız ikinci yenileme oturumu sonlandırır.
