
# Next.js SDK

`@keenauth/nextjs`, bir Next.js App Router uygulamasına oturum açma özelliği ekler. Uygulamanız OAuth istemcisidir ve tüm belirteçleri sunucuda tutar. Tarayıcıya yalnızca şifreli, HttpOnly bir oturum çerezi gönderilir; tarayıcı hiçbir zaman erişim, yenileme veya ID belirteci görmez. Bu, backend-for-frontend (BFF) desenidir. Paketin çalışma zamanı bağımlılığı yoktur. Henüz npm'de yayımlanmamıştır.

## Başlamadan önce

1. Panelde, ortamınızda bir web uygulaması oluşturun.
2. İstemci kimlik doğrulaması olarak `private_key_jwt` seçin ve genel anahtarınızı kaydedin. İstemci gizli anahtarı da çalışır.
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 gizli anahtarı üretin; örneğin `openssl rand -base64 32` komutuyla. Bunu depoya değil, gizli anahtar deponuzda saklayın.

## Örneği oluşturun

```ts
// lib/auth.ts
import { createKeenAuth } from "@keenauth/nextjs";

export const auth = createKeenAuth({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "private_key_jwt", privateKey: JSON.parse(process.env.KEENAUTH_CLIENT_KEY!) },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
```

`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. Düz `http` yalnızca `localhost` için kabul edilir.

Seçenekler örnek oluşturulurken denetlenir; bu nedenle bir yapılandırma hatası uygulamayı başlangıçta durdurur.

## Yolları ekleyin

```ts
// app/auth/[...keenauth]/route.ts
import { auth } from "@/lib/auth";

export const { GET, POST } = auth.handlers;
```

Bu, üç yolu sunar:

| Yol | Ne yapar |
|---|---|
| `/auth/login` | Oturum açmayı başlatır. İsteğe bağlı `returnTo` sorgu parametresi, kullanıcının sonradan geleceği yeri belirler. |
| `/auth/callback` | Oturum açmayı tamamlar ve oturum çerezini ayarlar. |
| `/auth/logout` | KeenAuth'ta oturumu kapatır ve uygulamanıza döner. |

Oturum açma, PKCE ile yetkilendirme kodu akışını kullanır. Ortamınız destekliyorsa istek sunucuya gönderilir (PAR). Callback'te SDK, `state`, `iss` ve ID belirtecini, `nonce` dahil, denetler.

`returnTo`, `/orders?tab=open` gibi uygulamanıza ait bir yol olmalıdır. `//evil.example` veya tam bir URL gibi diğer değerler yok sayılır ve kullanıcı `/` sayfasına yönlendirilir.

## Sayfaları koruyun ve belirteçleri yenileyin

```ts
// proxy.ts (Next.js 16'dan önce middleware.ts)
import { NextResponse, type NextRequest } from "next/server";
import { auth } from "@/lib/auth";

export async function proxy(request: NextRequest) {
  const result = await auth.proxy(request, {
    protect: (path) => path.startsWith("/orders") || path.startsWith("/api/"),
  });
  if (result.response) return result.response;
  const response = NextResponse.next({ request: { headers: result.requestHeaders } });
  result.apply(response);
  return response;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```

Oturumu olmayan korumalı bir yol için:

- sayfa isteği (`GET` veya `HEAD`), `returnTo` ile `/auth/login` adresine yönlendirilir;
- `/api/…` isteği veya başka bir yöntemle yapılan istek, bir problem belgesiyle `401` alır.

Erişim belirtecinin süresi 60 saniye içinde dolacaksa proxy onu yeniler. Her yenilemede yenileme belirteci döndürülür ve yeni oturum çerezi yanıta yazılır. `requestHeaders`, yeni oturumu aynı isteği işleyen sayfalara aktarır.

Sunucu yenileme belirtecini reddederse oturum sona erer. Geçici bir sunucu hatası oturumu değiştirmeden bırakır.

Ayrıca her Server Function ve route handler içinde oturumu denetleyin. Bir proxy matcher'ı bir yolu kaçırabilir.

## Oturumu okuyun

Bir Server Component içinde çerez deposunu geçirin:

```tsx
// app/orders/page.tsx
import { cookies } from "next/headers";
import { auth } from "@/lib/auth";

export default async function Orders() {
  const session = await auth.getSession(await cookies());
  if (!session) return <a href="/auth/login?returnTo=/orders">Sign in</a>;
  return <p>Signed in as {session.user.email}</p>;
}
```

`session.user`, ID belirtecinin türlendirilmiş taleplerini (`subject`, `email`, `emailVerified`, `name`, `orgId`, `orgRoles` ve diğerleri) tutar. Özel olanlar dahil tüm talepler `session.user.claims` içindedir. Oturum nesnesi hiçbir zaman bir belirteç içermez.

Bir Server Component çerez ayarlayamaz; bu nedenle oturumu orada okumak onu hiçbir zaman yenilemez.

## API'nizi çağırın

Bir Server Component içinde erişim belirtecini çerez deposundan alın:

```ts
const token = await auth.getAccessToken(await cookies());
const res = await fetch("https://api.acme.example/v1/orders", {
  headers: { Authorization: `Bearer ${token}` },
});
```

`getAccessToken` şu durumlarda hata verir:

- belirteç sona ermeye yakınsa ve proxy tarafından yenilenmiş olması gerekiyorsa `refresh_required`;
- oturum yoksa `login_required`.

Bir route handler içinde `updateSession` de yeniler:

```ts
// app/api/orders/route.ts
import { auth } from "@/lib/auth";

export async function GET(request: Request) {
  const { accessToken, apply } = await auth.updateSession(request);
  if (!accessToken) return new Response(null, { status: 401 });
  const upstream = await fetch("https://api.acme.example/v1/orders", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const response = new Response(upstream.body, { status: upstream.status });
  apply(response);
  return response;
}
```

`auth.authorizedFetch`, aynı işi tek çağrıda yapar. Belirteci `getAccessToken` gibi okur, `Authorization` başlığını ekler ve DPoP kullanılıyorsa kanıtı da ekler:

```ts
const res = await auth.authorizedFetch(await cookies(), "https://api.acme.example/v1/orders");
```

Erişim belirtecini bir Client Component'e geçirmeyin. API'nizi sunucudan çağırın ve tarayıcıya yalnızca verileri gönderin. Bu belirteçleri API'nizde doğrulamak için bkz. [API'nizde belirteçleri doğrulama](/docs/sdk/token-verification).

## Çıkış yapın

`/auth/logout` adresine bağlantı verin veya bir formdan bu adrese `POST` gönderin. SDK şunları yapar:

1. oturum çerezini siler;
2. yenileme belirtecini iptal eder;
3. tarayıcıyı, 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; böylece başka bir site kullanıcılarınızın oturumunu kapatamaz. `Sec-Fetch-Site` göndermeyen tarayıcılarda çıkış yalnızca kendi sayfalarınızdan gelen bir `POST` ile yapılabilir; `/auth/logout` adresine gönderen bir form kullanmanız önerilir.

## Gönderen kısıtlamalı belirteçler (DPoP)

DPoP (RFC 9449) ile erişim belirteciniz yalnızca sunucunuzun tuttuğu bir anahtara bağlanır. Sızdırılan bir belirteç, örneğin bir API günlüğünden, bu anahtar olmadan kullanılamaz.

1. Uygulama için bir ES256 (P-256) anahtarı üretin ve özel JWK'yı gizli anahtar deponuzda saklayın.
2. Bunu `dpop` olarak geçirin:

```ts
export const auth = createKeenAuth({
  // ...the options above
  dpop: { privateKey: JSON.parse(process.env.KEENAUTH_DPOP_KEY!) },
});
```

SDK bundan sonra şunları yapar:

- yetkilendirme kodunu anahtara bağlar (`dpop_jkt`, gönderilen yetkilendirme isteğiyle birlikte iletilir);
- her belirteç ve yenileme isteğiyle bir DPoP kanıtı gönderir ve sunucu yeni bir nonce istediğinde bir kez yeniden dener;
- `token_type` değeri `DPoP` olmayan bir belirteç yanıtını reddeder.

API'nizi çağırmak için `auth.authorizedFetch` kullanın. Her istek için `Authorization: DPoP <token>` ve `ath` içeren yeni bir kanıt gönderir; API'niz `error="use_dpop_nonce"` ile `401` yanıtı verirse bir kez yeniden dener. Kendi HTTP istemcinizi kullanıyorsanız kanıtı `auth.createDpopProof({ method, url, accessToken, nonce })` ile oluşturun.

Her sunucu sürecinde aynı anahtarı kullanın: ortamınız belirteçleri ve yenileme belirtecini buna bağlar. Anahtarı değiştirirseniz veya DPoP'yi açıp kapatırsanız mevcut oturumlar sona erer ve kullanıcılar yeniden oturum açmak zorunda kalır.

## Sunucu tarafı oturumlar

Varsayılan olarak tüm oturum çerezin içine mühürlenir. Oturumu bunun yerine sunucuda tutmak için bir `sessionStore` geçirin. Bu durumda çerez yalnızca rastgele bir oturum kimliğine şifreli bir başvuru tutar.

```ts
import { createKeenAuth, memorySessionStore, type SessionStore } from "@keenauth/nextjs";

// Next.js bundles proxy.ts, route handlers and pages separately, and each bundle runs its own copy of this module.
// Keep the store on globalThis so that they all share one store in the server process.
const shared = globalThis as { keenauthSessions?: SessionStore };
const sessionStore = (shared.keenauthSessions ??= memorySessionStore());

export const auth = createKeenAuth({
  // ...the options above
  sessionStore,
});
```

Bellek içi deposu, yukarıdaki gibi süreç başına bir kez oluşturun. `globalThis` kullanmadan modül düzeyinde çağrılan bir `memorySessionStore()`, proxy'ye, her route handler'a ve sayfalara kendi boş deposunu verir: callback yolunun kaydettiği oturum proxy veya bir sayfa tarafından bulunamaz ve kullanıcı oturumu kapatmış gibi görünür. Verilerini süreç dışında tutan bir depo (Redis, veritabanı) bu sorunu yaşamaz.

`memorySessionStore`, oturumları tek bir sürecin belleğinde tutar ve yeniden başlatmada bunları kaybeder. Geliştirme ve tek örnekli sunucular için kullanın. Birden çok örnek veya sunucusuz işlevler kullanıyorsanız, Redis veya veritabanınız gibi paylaşılan bir depoda `SessionStore` arayüzünü uygulayın:

```ts
interface SessionStore {
  get(id: string): Promise<SessionRecord | null>;
  set(id: string, record: SessionRecord): Promise<void>;
  delete(id: string): Promise<void>;
  deleteByLogout(target: { sub?: string; sid?: string }): Promise<void>;
}
// SessionRecord: { value: string; sub: string; sid?: string; expiresAt: number }
```

- `value`, yine `secret` değeriniz ile şifrelenir; bu nedenle deponun bir kopyası hiçbir belirteci açığa çıkarmaz. Günlüğe yazmayın.
- `sid` ile `deleteByLogout`, o `sid` değerine sahip oturumları siler; `sub` de verilirse ikisinin de eşleşmesi gerekir. Yalnızca `sub` verilirse, o kullanıcının tüm oturumlarını siler.
- `expiresAt` süresinden sonra kaydı silin.
- Paketin deposunda, deponuzun geçmesi gereken bir sözleşme testi vardır (`clients/nextjs/test/store-contract.ts`).

Depo başarısız olursa SDK, kullanıcıyı oturumu kapalı saymak yerine bir `session_store_error` hatası fırlatır. Çıkış yapmak yine de çerezi siler.

## Arka kanal oturumu kapatma

Bir `sessionStore` ile SDK, `/auth/backchannel-logout` adresini de sunar. Bir oturum KeenAuth'ta sona erdiğinde (örneğin kullanıcı başka bir uygulamadan çıkış yaptığında veya bir yönetici oturumu iptal ettiğinde), ortamınız bu adrese imzalı bir oturum kapatma belirteci gönderir. SDK eşleşen oturumları siler; böylece kullanıcının uygulamanızdaki sonraki isteğinde oturum kalmaz.

1. `https://app.acme.example/auth/backchannel-logout` adresini uygulamanın arka kanal oturum kapatma URI'si olarak kaydedin.
2. Yolun internetten erişilebilir olduğundan emin olun. Proxy matcher'ınız bunu içerebilir: proxy bu isteğe izin verir.

SDK, oturum kapatma belirtecini OpenID Connect Back-Channel Logout 1.0'ın gerektirdiği gibi denetler: imza, issuer, audience, `iat` ve `exp`, oturum kapatma olayı, `sub` veya `sid`, `nonce` olmaması ve `logout+jwt` türü. Başarılıysa `200`, değilse `400` yanıtı verir; ortamınız başarısız teslimatı yeniden dener.

Her oturum kapatma belirtecinin `jti` değeri, belirteç sona erene kadar hatırlanır; böylece ele geçirilen bir belirteç yeniden oynatılamaz. Tekrarlanan bir `jti`, yeniden işlem yapılmadan `200` ile yanıtlanır. Varsayılan önbellek tek bir süreçte yaşar; birden çok örnekte, `remember(jti, expiresAt)` ve `forget(jti)` işlevlerini atomik olarak uygulayan paylaşılan bir `logoutReplayCache` geçirin (örneğin Redis `SET NX`).

## Oturum çerezi

- Çerez, `secret` değerinden türetilen bir anahtarla AES-256-GCM kullanılarak şifrelenir ve doğrulanır. Değiştirilmiş veya kopyalanmış bir çerez reddedilir.
- Adı `__Host-keenauth_session.0`'dır ve `Secure`, `HttpOnly`, `SameSite=Lax` ve `Path=/` ile ayarlanır. `http://localhost` üzerinde `__Host-` öneki ve `Secure` kullanılmaz.
- Bir oturum en fazla 7 gün sürer (`sessionMaxAge`); yenileme bu süreyi uzatmaz.
- Büyük oturumlar en fazla üç çereze bölünür.

Gizli anahtarı değiştirmek için bir liste geçirin: `secret: [newSecret, oldSecret]`. İlk anahtar şifreler, hepsi çözer. Eski anahtarı `sessionMaxAge` süresinden sonra kaldırın.

## Bugünkü sınırlar

- Gömülü bileşenlerle oturum açma, bu paketde henüz kullanılamaz.
- Yenilemeler yalnızca tek bir sunucu süreci içinde paylaşılır. Birden çok örnekte, ikisi aynı oturumu aynı anda yenileyebilir. Uygulama ayarı `refresh_reuse_interval_s` (varsayılan 5 saniye, bkz. [Bir uygulamayı güncelleme](/docs/api/management/applications/update)) ikinci yenilemenin başarılı olmasını sağlar; değeri `0` yaparsanız ikinci yenileme oturumu sonlandırır.
- `sessionStore` olmadan arka kanal oturum kapatma kullanılamaz; bu durumda yenileme belirteci reddedildiğinde oturum sunucuda sona erer.
