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

// 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

// 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

// 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:

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:

// 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:

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:

Bir route handler içinde updateSession de yeniler:

// 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:

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.

Çı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:
export const auth = createKeenAuth({
  // ...the options above
  dpop: { privateKey: JSON.parse(process.env.KEENAUTH_DPOP_KEY!) },
});

SDK bundan sonra şunları yapar:

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.

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:

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 }

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

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