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
- Panelde, ortamınızda bir web uygulaması oluşturun.
- İstemci kimlik doğrulaması olarak
private_key_jwtseçin ve genel anahtarınızı kaydedin. İstemci gizli anahtarı da çalışır. https://app.acme.example/auth/callbackyönlendirme URI'sini vehttps://app.acme.example/çıkış sonrası yönlendirme URI'sini ekleyin.- En az 32 karakterlik bir çerez gizli anahtarı üretin; örneğin
openssl rand -base64 32komutuyla. 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:
- sayfa isteği (
GETveyaHEAD),returnToile/auth/loginadresine yönlendirilir; /api/…isteği veya başka bir yöntemle yapılan istek, bir problem belgesiyle401alı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:
// 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:
- 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:
// 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:
- oturum çerezini siler;
- yenileme belirtecini iptal eder;
- 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.
- Uygulama için bir ES256 (P-256) anahtarı üretin ve özel JWK'yı gizli anahtar deponuzda saklayın.
- Bunu
dpopolarak geçirin:
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_typedeğeriDPoPolmayan 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.
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 }
value, yinesecretdeğeriniz ile şifrelenir; bu nedenle deponun bir kopyası hiçbir belirteci açığa çıkarmaz. Günlüğe yazmayın.sidiledeleteByLogout, osiddeğerine sahip oturumları siler;subde verilirse ikisinin de eşleşmesi gerekir. Yalnızcasubverilirse, o kullanıcının tüm oturumlarını siler.expiresAtsü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.
https://app.acme.example/auth/backchannel-logoutadresini uygulamanın arka kanal oturum kapatma URI'si olarak kaydedin.- 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,
secretdeğ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 veSecure,HttpOnly,SameSite=LaxvePath=/ile ayarlanır.http://localhostüzerinde__Host-öneki veSecurekullanı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) ikinci yenilemenin başarılı olmasını sağlar; değeri0yaparsanız ikinci yenileme oturumu sonlandırır. sessionStoreolmadan arka kanal oturum kapatma kullanılamaz; bu durumda yenileme belirteci reddedildiğinde oturum sunucuda sona erer.