Skip to content

Instantly share code, notes, and snippets.

@mcihad
Created March 10, 2026 12:37
Show Gist options
  • Select an option

  • Save mcihad/49ff9b61cf7e5590512712d316275c45 to your computer and use it in GitHub Desktop.

Select an option

Save mcihad/49ff9b61cf7e5590512712d316275c45 to your computer and use it in GitHub Desktop.

Next.js + OpenID Connect (OpenIddict) Entegrasyon Kılavuzu

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.


1. Ön Koşullar

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

2. Paket Kurulumu

pnpm add next-auth@5.0.0-beta.30

DİKKAT: next-auth@^5.0.0 yazarsan pnpm install patlar çünkü npm registry'de 5.x stable sürümü yoktur.
Mutlaka 5.0.0-beta.30 gibi açık sürüm belirt.


3. Ortam Değişkenleri (.env)

# 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"

Kritik Notlar

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

Issuer Doğrulama

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ı

4. Dosya Yapısı

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

5. Auth Yapılandırması — auth.ts

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;
        },
    },
});

Provider Tipi Seçimi

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'de userinfo_endpoint göstermiyorsa type: "oauth" kullan ve endpoint'leri elle ver.

type: "oauth" ile Kullanım (Discovery Eksikse)

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) { /* ... */ },
}

6. API Route — app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth";
export const { GET, POST } = handlers;

7. Route Koruması — proxy.ts

Next.js 16+ sürümünde middleware.ts deprecated oldu, yerine proxy.ts kullanılır.
Next.js 15 ve altında middleware.ts kullanmaya devam edebilirsin.

Next.js 16+ (proxy.ts)

export { auth as proxy } from "@/auth";

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

Next.js 15 ve altı (middleware.ts)

export { auth as middleware } from "@/auth";

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

Matcher Açıklaması

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


8. Session Provider — components/providers/session-provider.tsx

"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>;
}

Root Layout'a Ekleme (app/layout.tsx)

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>
    );
}

SessionProvider en dışta olmalı. ThemeProvider onun içinde kalır.


9. Login Sayfası — app/giris/page.tsx

"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çin Suspense ile sarmalama zorunludur.


10. Header'da Kullanıcı Bilgisi ve Çıkış

"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>
    );
}

11. OpenIddict Sunucu Tarafı Gereksinimleri

Callback'in çalışması için OpenIddict tarafında şu ayarların yapılmış olması gerekir:

Redirect URI'ler

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 akillisehir kısmı, auth.ts'deki provider id'si ile birebir aynı olmalı.

Post Logout Redirect URI

Ortam URI
Geliştirme http://localhost:3000/giris
Prod https://uygulama.sivas.bel.tr/giris

Gerekli Scope'lar

OpenIddict uygulamasında şu scope'lar tanımlı ve izinli olmalı:

  • openid (zorunlu)
  • profile
  • email
  • roles
  • offline_access

Discovery Endpoint'leri

/.well-known/openid-configuration dokümanında şu endpoint'ler mutlaka görünmeli:

  • authorization_endpoint
  • token_endpoint
  • userinfo_endpointBu yoksa type: "oidc" patlar!
  • jwks_uri

12. Sık Karşılaşılan Hatalar ve Çözümleri

ERR_PNPM_NO_MATCHING_VERSION — next-auth kurulumu

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"

Authorization server did not provide a userinfo endpoint

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).

unexpected "iss" (issuer) response parameter value

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.

invalid_request — The specified 'redirect_uri' is not valid

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

.env Issuer Uyuşmazlığı

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.

middleware deprecated uyarısı (Next.js 16+)

⚠ 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"

13. Yeni Proje Kontrol Listesi

Sıfırdan bir Next.js projesine OpenID eklerken takip edilecek adımlar:

  • pnpm add next-auth@5.0.0-beta.30
  • .env dosyasına AUTH_SECRET, AUTH_URL, AUTH_OPENID_* değişkenlerini ekle
  • auth.ts oluştur (provider id değerini not et — URI'lerde kullanılacak)
  • app/api/auth/[...nextauth]/route.ts oluştur
  • proxy.ts (veya Next.js 15 için middleware.ts) oluştur
  • components/providers/session-provider.tsx oluştur
  • app/layout.tsx'e SessionProvider ekle
  • 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_endpoint var mı?
  • AUTH_OPENID_ISSUER değerinin sondaki / dahil discovery ile eşleştiğini doğrula
  • Dev server başlat ve uçtan uca test et

14. OpenIddict Program.cs Referansı

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);
    });

SetUserInfoEndpointUris satırı eksikse Auth.js type: "oidc" patlar.


Son güncelleme: Mart 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment