Sivas Belediyesi Akıllı Şehir SSO altyapısı ile Next.js projelerinin entegrasyonu için adım adım rehber.
Bu kılavuz, yaşanan sorunlar ve çözümleriyle birlikte hazırlanmıştır.
| Bileşen | Minimum Versiyon | Not |
|---|---|---|
| Next.js | 15.x veya 16.x | App Router (pages router desteklenmez) |
| next-auth | 5.0.0-beta.30 |
npm'de ^5.0.0 yok! Beta etiketini açıkça belirt |
| Node.js | 18+ | — |
| OpenIddict Sunucusu | — | userinfo_endpoint discovery'de görünmeli |
pnpm add next-auth@5.0.0-beta.30DİKKAT:
next-auth@^5.0.0yazarsanpnpm installpatlar çünkü npm registry'de5.xstable sürümü yoktur.
Mutlaka5.0.0-beta.30gibi açık sürüm belirt.
# NextAuth.js — OpenID Connect
AUTH_SECRET="<openssl rand -base64 32 ile oluştur>"
AUTH_TRUST_HOST=true
AUTH_URL="http://localhost:3000"
# OpenID Connect Provider
AUTH_OPENID_ISSUER="https://akillisehir.sivas.bel.tr/"
AUTH_OPENID_CLIENT_ID="uygulama_adi"
AUTH_OPENID_CLIENT_SECRET="sunucudan_alinan_secret"| Değişken | Dikkat Edilecekler |
|---|---|
AUTH_SECRET |
Her ortam için farklı, rastgele, en az 32 karakter |
AUTH_OPENID_ISSUER |
Sondaki / dahil olmalı! Discovery dokümanındaki issuer değeriyle birebir aynı olmalı |
AUTH_URL |
Geliştirmede http://localhost:3000, prod'da gerçek domain |
curl https://akillisehir.sivas.bel.tr/.well-known/openid-configuration | jq .issuer
# Çıktı: "https://akillisehir.sivas.bel.tr/" ← .env'deki değer bununla aynı olmalıproje/
├── auth.ts # NextAuth yapılandırması
├── proxy.ts # Route koruması (Next.js 16+)
├── app/
│ ├── layout.tsx # SessionProvider eklenir
│ ├── giris/page.tsx # Login sayfası
│ └── api/auth/[...nextauth]/route.ts # NextAuth API handler
└── components/providers/session-provider.tsx # Client-side session wrapper
import NextAuth from "next-auth";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
{
id: "akillisehir",
name: "Akıllı Şehir",
type: "oidc",
issuer: process.env.AUTH_OPENID_ISSUER,
clientId: process.env.AUTH_OPENID_CLIENT_ID,
clientSecret: process.env.AUTH_OPENID_CLIENT_SECRET,
authorization: {
params: {
scope: "openid profile email roles offline_access",
},
},
profile(profile) {
return {
id: String(profile.sub ?? "unknown"),
name:
profile.name ??
profile.preferred_username ??
profile.email ??
"Kullanıcı",
email: profile.email ?? null,
image: null,
};
},
},
],
pages: {
signIn: "/giris",
},
trustHost: true,
callbacks: {
authorized({ auth }) {
return !!auth?.user;
},
},
});| Tip | Ne Zaman Kullanılır | Avantaj | Dezavantaj |
|---|---|---|---|
oidc |
Discovery'de tüm endpoint'ler varsa | Otomatik issuer doğrulama, endpoint keşfi | userinfo_endpoint yoksa patlar |
oauth |
Discovery eksikse veya özel endpoint gerekiyorsa | Tam kontrol | Issuer doğrulaması yapılmaz, endpoint'ler elle verilmeli |
Kural: Önce
type: "oidc"dene. Eğer sunucu discovery'deuserinfo_endpointgöstermiyorsatype: "oauth"kullan ve endpoint'leri elle ver.
const issuer = process.env.AUTH_OPENID_ISSUER ?? "https://akillisehir.sivas.bel.tr/";
{
id: "akillisehir",
name: "Akıllı Şehir",
type: "oauth",
clientId: process.env.AUTH_OPENID_CLIENT_ID,
clientSecret: process.env.AUTH_OPENID_CLIENT_SECRET,
authorization: {
url: `${issuer}Authorization/Authorize`,
params: {
scope: "openid profile email roles offline_access",
response_type: "code",
},
},
token: `${issuer}Authorization/Exchange`,
userinfo: `${issuer}connect/userinfo`,
checks: ["pkce", "state"],
profile(profile) { /* ... */ },
}import { handlers } from "@/auth";
export const { GET, POST } = handlers;Next.js 16+ sürümünde
middleware.tsdeprecated oldu, yerineproxy.tskullanılır.
Next.js 15 ve altındamiddleware.tskullanmaya devam edebilirsin.
export { auth as proxy } from "@/auth";
export const config = {
matcher: [
"/((?!giris|api/auth|_next/static|_next/image|favicon.ico).*)",
],
};export { auth as middleware } from "@/auth";
export const config = {
matcher: [
"/((?!giris|api/auth|_next/static|_next/image|favicon.ico).*)",
],
};| Pattern | Açıklama |
|---|---|
giris |
Login sayfası — korumasız olmalı |
api/auth |
NextAuth API route'ları — korumasız olmalı |
_next/static |
Next.js statik dosyaları |
_next/image |
Next.js image optimization |
favicon.ico |
Favicon |
Yeni public sayfa eklenirse matcher'a eklenmeli: giris|hakkinda|api/auth|...
"use client";
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
import type { ReactNode } from "react";
export function SessionProvider({ children }: { children: ReactNode }) {
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}import { SessionProvider } from "@/components/providers/session-provider";
import { ThemeProvider } from "@/components/providers/theme-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="tr" suppressHydrationWarning>
<body>
<SessionProvider>
<ThemeProvider>{children}</ThemeProvider>
</SessionProvider>
</body>
</html>
);
}
SessionProvideren dışta olmalı.ThemeProvideronun içinde kalır.
"use client";
import { Suspense } from "react";
import { signIn, useSession } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
function LoginContent() {
const { status } = useSession();
const router = useRouter();
const searchParams = useSearchParams();
const error = searchParams.get("error");
useEffect(() => {
if (status === "authenticated") router.replace("/");
}, [status, router]);
if (status === "loading" || status === "authenticated") {
return <div>Yükleniyor...</div>;
}
return (
<div>
{error && <p>Giriş yapılamadı. Lütfen tekrar deneyin.</p>}
<button onClick={() => signIn("akillisehir", { callbackUrl: "/" })}>
Giriş Yap
</button>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={<div>Yükleniyor...</div>}>
<LoginContent />
</Suspense>
);
}
useSearchParams()kullanıldığı içinSuspenseile sarmalama zorunludur.
"use client";
import { useSession, signOut } from "next-auth/react";
export function Header() {
const { data: session } = useSession();
return (
<header>
{session?.user && (
<>
<span>{session.user.name || session.user.email}</span>
<button onClick={() => signOut({ callbackUrl: "/giris" })}>
Çıkış Yap
</button>
</>
)}
</header>
);
}Callback'in çalışması için OpenIddict tarafında şu ayarların yapılmış olması gerekir:
| Ortam | URI |
|---|---|
| Geliştirme | http://localhost:3000/api/auth/callback/akillisehir |
| Ağ (IP) | http://10.x.x.x:3000/api/auth/callback/akillisehir |
| Prod | https://uygulama.sivas.bel.tr/api/auth/callback/akillisehir |
URI'deki
akillisehirkısmı,auth.ts'deki providerid'si ile birebir aynı olmalı.
| Ortam | URI |
|---|---|
| Geliştirme | http://localhost:3000/giris |
| Prod | https://uygulama.sivas.bel.tr/giris |
OpenIddict uygulamasında şu scope'lar tanımlı ve izinli olmalı:
openid(zorunlu)profileemailrolesoffline_access
/.well-known/openid-configuration dokümanında şu endpoint'ler mutlaka görünmeli:
authorization_endpointtoken_endpointuserinfo_endpoint← Bu yoksatype: "oidc"patlar!jwks_uri
No matching version found for next-auth@^5.0.0
Çözüm: package.json'da sürümü açıkça belirt:
"next-auth": "5.0.0-beta.30"TypeError: TODO: Authorization server did not provide a userinfo endpoint.
Neden: type: "oidc" kullanılıyor ama discovery'de userinfo_endpoint yok.
Çözüm: Ya OpenIddict'te userinfo_endpoint'i discovery'de yayınla, ya da type: "oauth" kullan (bkz. §5).
OperationProcessingError: unexpected "iss" (issuer) response parameter value
expected: "https://authjs.dev"
Neden: type: "oauth" kullanıldığında Auth.js issuer doğrulaması yapamaz.
Çözüm: type: "oidc" kullan. Bu tip, discovery'den issuer'ı otomatik alır ve doğrular.
error: invalid_request
error_description: The specified 'redirect_uri' is not valid for this client application.
error_uri: https://documentation.openiddict.com/errors/ID2043
Neden: OpenIddict tarafında callback URI kayıtlı değil.
Çözüm: Client tanımına şu URI'yi ekle:
http://localhost:3000/api/auth/callback/akillisehir
Discovery issuer: "https://example.com/" dönerken .env'de https://example.com (slash yok) yazılırsa token doğrulaması başarısız olur.
Çözüm: .env değerini discovery çıktısıyla birebir aynı yap. curl ile kontrol et.
⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.
Çözüm: middleware.ts dosyasını proxy.ts olarak yeniden adlandır ve export'u değiştir:
// middleware.ts → proxy.ts
export { auth as proxy } from "@/auth"; // "middleware" yerine "proxy"Sıfırdan bir Next.js projesine OpenID eklerken takip edilecek adımlar:
-
pnpm add next-auth@5.0.0-beta.30 -
.envdosyasınaAUTH_SECRET,AUTH_URL,AUTH_OPENID_*değişkenlerini ekle -
auth.tsoluştur (provideriddeğerini not et — URI'lerde kullanılacak) -
app/api/auth/[...nextauth]/route.tsoluştur -
proxy.ts(veya Next.js 15 içinmiddleware.ts) oluştur -
components/providers/session-provider.tsxoluştur -
app/layout.tsx'eSessionProviderekle - Login sayfası oluştur (
app/giris/page.tsx) - Header'a kullanıcı bilgisi + çıkış butonu ekle
- OpenIddict'te redirect URI kaydet:
{AUTH_URL}/api/auth/callback/{provider_id} - Discovery endpoint'ini kontrol et:
userinfo_endpointvar mı? -
AUTH_OPENID_ISSUERdeğerinin sondaki/dahil discovery ile eşleştiğini doğrula - Dev server başlat ve uçtan uca test et
Sunucu tarafında minimum gerekli endpoint tanımları:
builder.Services.AddOpenIddict()
.AddServer(options =>
{
options.SetIssuer(new Uri("https://akillisehir.sivas.bel.tr/"));
options.SetAuthorizationEndpointUris("/Authorization/Authorize")
.SetTokenEndpointUris("/Authorization/Exchange")
.SetUserInfoEndpointUris("/connect/userinfo"); // ← ZORUNLU
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow();
options.RequireProofKeyForCodeExchange();
options.DisableAccessTokenEncryption();
options.RegisterScopes(
OpenIddictConstants.Scopes.OpenId,
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Roles,
OpenIddictConstants.Scopes.OfflineAccess);
});
SetUserInfoEndpointUrissatırı eksikse Auth.jstype: "oidc"patlar.
Son güncelleme: Mart 2026