Authentik OIDC/SSO: Tam Entegrasyon Kılavuzu
Authentik, Okta veya Auth0'ın karmaşıklığı (veya maliyeti) olmadan size kurumsal düzeyde SSO, OIDC, SAML ve OAuth2 sunan açık kaynaklı bir kimlik sağlayıcısıdır. Şirket içinde barındırılan uygulamalar için Authentik'in OIDC uç noktalarını, kullanıcı yönetimini, grup tabanlı erişim kontrolünü ve kurumsal ilkelerini kendi altyapınız altında sağlar.
Bu kılavuz, Next.js 16 ve NestJS 11 ile üretim Authentik entegrasyonunu kapsar: OAuth2 sağlayıcı yapılandırması, güvenli yetkilendirme kodu akışı, tek seferlik değişim kodu modeli, JWT doğrulaması ve kimlik doğrulama uygulamasını oluşturan veya bozan ince sorunlar.
Önemli Çıkarımlar
- Kimlik doğrulama belirteçleri HttpOnly çerezlerinde bulunmalıdır; hiçbir zaman localStorage veya sorgu parametrelerinde bulunmamalıdır
- Belirteci geri aramadan ön uca geçirmek için tek seferlik bir değişim kodu (60 saniyelik TTL) kullanın
- Sunucular arası jeton alışverişi için her zaman
AUTHENTIK_INTERNAL_URLkullanın (ağ atlamalarından kaçının)- Değişim kodunun geçerlilik süresi kontrolü, önbellekten silmeden ÖNCE gerçekleştirilmelidir (yarış durumu önleme)
- API aracılığıyla yeniden oluşturulan Authentik istemci sırlarında karma uyumsuzluğu olabilir; doğrudan Django ORM'yi kullanın
- JWT talepleri
organizationIdiçermelidir — Authentik özellik eşlemesinde bunu kodlayın- Çerezlerden JWT ayıklamanın çalışması için NestJS'de
cookie-parserara katman yazılımı gereklidir- OIDC keşif uç noktası tüm belirteç/kullanıcı bilgisi URL'lerini sağlar; bunları sabit kodlamayın
Orijinal Yapılandırma
OAuth2 Sağlayıcısını Oluşturma
Authentik yönetici panelinde (/if/admin/):
- Uygulamalar > Sağlayıcılar > Oluştur'a gidin
- OAuth2/OpenID Sağlayıcı'yı seçin
- Yapılandırın:
- Ad:
ECOSIRE Web OAuth2 - Yetkilendirme akışı:
default-provider-authorization-implicit-consent - İstemci türü:
Confidential - Müşteri Kimliği:
ecosire-web(bunu siz seçersiniz) - Yönlendirme URI'leri:
https://api.ecosire.com/api/auth/callback https://ecosire.com/auth/callback - İmzalama Anahtarı: Varsayılan sertifikanızı seçin
- Kapsamlar:
openid,email,profile
- Bu sağlayıcıyı kullanan bir Uygulama oluşturun
Özel Özellik Eşlemesi
organizationId öğesini JWT'ye dahil etmek için bir özellik eşlemesi oluşturun:
Authentik admin > Özelleştirme > Özellik Eşlemeleri > Oluştur > Kapsam Eşleme'de:
# Name: Organization ID Scope
# Scope name: organization
return {
"organizationId": request.user.attributes.get("organizationId", str(request.user.pk)),
"name": request.user.name,
}
Bu eşlemeyi OAuth2 sağlayıcınızın "Kapsamlar" listesine ekleyin ve uygulamanız tarafından istenen kapsamlara organization ekleyin.
Arka Uç: NestJS Kimlik Doğrulama Modülü
JWT Stratejisi
// auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
// Support both Cookie and Bearer token
jwtFromRequest: ExtractJwt.fromExtractors([
// Cookie first (web app)
(request) => {
return request?.cookies?.ecosire_auth ?? null;
},
// Bearer fallback (API clients, mobile)
ExtractJwt.fromAuthHeaderAsBearerToken(),
]),
ignoreExpiration: false,
// Validate against Authentik's public JWKS endpoint
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${process.env.AUTHENTIK_INTERNAL_URL}/application/o/ecosire-web/jwks/`,
}),
audience: 'ecosire-web',
issuer: `${process.env.AUTHENTIK_URL}/application/o/ecosire-web/`,
});
}
async validate(payload: {
sub: string;
email: string;
name: string;
organizationId?: string;
groups?: string[];
}) {
if (!payload.sub) {
throw new UnauthorizedException('Invalid token');
}
// Map Authentik claims to your internal user type
return {
sub: payload.sub,
email: payload.email,
name: payload.name,
organizationId: payload.organizationId ?? payload.sub,
role: this.mapGroupsToRole(payload.groups ?? []),
};
}
private mapGroupsToRole(groups: string[]): 'admin' | 'support' | 'user' {
if (groups.includes('ecosire-admins')) return 'admin';
if (groups.includes('ecosire-support')) return 'support';
return 'user';
}
}
Çerez Yapılandırması
// main.ts
import cookieParser from 'cookie-parser';
const app = await NestFactory.create(AppModule);
// cookie-parser MUST be registered before JWT strategy runs
app.use(cookieParser(process.env.COOKIE_SECRET));
Kimlik Doğrulama Denetleyicisi
// auth/auth.controller.ts
import {
Controller,
Get,
Post,
Query,
Res,
Req,
Body,
UnauthorizedException,
} from '@nestjs/common';
import { Response, Request } from 'express';
import { Public } from './decorators/public.decorator';
import { AuthService } from './auth.service';
const COOKIE_OPTS = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
path: '/',
maxAge: 60 * 60 * 24 * 7 * 1000, // 7 days
};
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Get('login')
@Public()
login(@Res() res: Response) {
const params = new URLSearchParams({
client_id: process.env.AUTHENTIK_CLIENT_ID!,
redirect_uri: `${process.env.API_URL}/auth/callback`,
response_type: 'code',
scope: 'openid email profile organization',
state: this.authService.generateState(),
});
const authUrl = `${process.env.AUTHENTIK_URL}/application/o/authorize/?${params}`;
return res.redirect(authUrl);
}
@Get('callback')
@Public()
async callback(
@Query('code') code: string,
@Query('state') state: string,
@Res() res: Response
) {
// Exchange authorization code for tokens
const { accessToken, refreshToken } = await this.authService.exchangeCode(code, state);
// Create a one-time exchange code for the frontend
// This avoids putting the token in the redirect URL
const exchangeCode = await this.authService.createExchangeCode(accessToken, refreshToken);
// Redirect to frontend with the one-time code (not the token itself)
return res.redirect(
`${process.env.FRONTEND_URL}/auth/callback?exchange=${exchangeCode}`
);
}
@Post('exchange')
@Public()
async exchangeTokens(
@Body() body: { code: string },
@Res() res: Response
) {
// Frontend exchanges the one-time code for HttpOnly cookies
const tokens = await this.authService.redeemExchangeCode(body.code);
if (!tokens) {
throw new UnauthorizedException('Invalid or expired exchange code');
}
// Set HttpOnly cookies — tokens never in response body
res.cookie('ecosire_auth', tokens.accessToken, COOKIE_OPTS);
res.cookie('ecosire_refresh', tokens.refreshToken, {
...COOKIE_OPTS,
path: '/auth/refresh', // Refresh token only sent to refresh endpoint
});
return res.json({ success: true });
}
@Get('session')
async getSession(@Req() req: Request) {
// JWT guard already validated the cookie, user attached to req
return req.user; // sub, email, name, role, organizationId
}
@Post('logout')
async logout(@Res() res: Response) {
// Clear cookies with the SAME options used to set them
res.clearCookie('ecosire_auth', COOKIE_OPTS);
res.clearCookie('ecosire_refresh', {
...COOKIE_OPTS,
path: '/auth/refresh',
});
// Optionally: End Authentik session
const logoutUrl = `${process.env.AUTHENTIK_URL}/application/o/ecosire-web/end-session/`;
return res.json({ logoutUrl });
}
}
Kimlik Doğrulama Hizmeti: Değişim Kodu Modeli
Tek kullanımlık değişim kodu, URL'deki belirteçlerden (tarayıcı geçmişinde, sunucu günlüklerinde ve yönlendiren başlıklarında görünen) kaçınmanın anahtarıdır:
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Redis } from 'ioredis';
import { nanoid } from 'nanoid';
import axios from 'axios';
@Injectable()
export class AuthService {
constructor(private redis: Redis) {}
async exchangeCode(code: string, state: string) {
// Verify state matches (CSRF protection)
const storedState = await this.redis.get(`auth:state:${state}`);
if (!storedState) {
throw new UnauthorizedException('Invalid state parameter');
}
await this.redis.del(`auth:state:${state}`);
// Exchange authorization code for tokens using internal URL
// AUTHENTIK_INTERNAL_URL avoids going through Nginx/Cloudflare for
// server-to-server calls inside the same network
const response = await axios.post(
`${process.env.AUTHENTIK_INTERNAL_URL}/application/o/token/`,
new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: `${process.env.API_URL}/auth/callback`,
client_id: process.env.AUTHENTIK_CLIENT_ID!,
client_secret: process.env.AUTHENTIK_CLIENT_SECRET!,
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
};
}
async createExchangeCode(
accessToken: string,
refreshToken: string
): Promise<string> {
const code = nanoid(32);
const payload = JSON.stringify({ accessToken, refreshToken });
// 60-second TTL — enough for the redirect to complete
await this.redis.setex(`auth:exchange:${code}`, 60, payload);
return code;
}
async redeemExchangeCode(code: string) {
const key = `auth:exchange:${code}`;
// Check expiry BEFORE deleting (prevents race condition)
const ttl = await this.redis.ttl(key);
if (ttl <= 0) {
return null; // Already expired or doesn't exist
}
const data = await this.redis.getdel(key); // Atomic get-and-delete
if (!data) return null;
return JSON.parse(data);
}
generateState(): string {
const state = nanoid(32);
// Store state with 10-minute TTL
this.redis.setex(`auth:state:${state}`, 600, '1');
return state;
}
async upsertUser(payload: {
sub: string;
email: string;
name: string;
organizationId: string;
}) {
const [user] = await db
.insert(users)
.values({
id: payload.sub,
email: payload.email,
name: payload.name,
organizationId: payload.organizationId,
})
.onConflictDoUpdate({
target: users.id,
set: {
email: payload.email,
name: payload.name,
lastLoginAt: new Date(),
},
})
.returning();
return user;
}
}
Ön Uç: Geri Arama İşleyicisi
// app/auth/callback/page.tsx — Next.js page
'use client';
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
export default function AuthCallbackPage() {
const router = useRouter();
const params = useSearchParams();
useEffect(() => {
const exchangeCode = params.get('exchange');
const redirectTo = params.get('redirect') ?? '/dashboard';
if (!exchangeCode) {
router.push('/auth/login?error=no_code');
return;
}
// Prevent open redirect
const safeRedirect = redirectTo.startsWith('/') && !redirectTo.startsWith('//')
? redirectTo
: '/dashboard';
// Exchange one-time code for HttpOnly cookies
fetch('/api/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: exchangeCode }),
credentials: 'include', // Required for cookie setting
})
.then((res) => {
if (res.ok) {
router.push(safeRedirect);
} else {
router.push('/auth/login?error=exchange_failed');
}
})
.catch(() => router.push('/auth/login?error=network'));
}, []);
return (
<div className="flex items-center justify-center min-h-screen">
<p>Signing you in...</p>
</div>
);
}
İstemci Gizli Karma Sorununu Düzeltme
Yaygın bir Authentik sorunu: REST API aracılığıyla ayarlanan istemci gizli dizilerinde bazen karma uyumsuzluğu olabilir ve OIDC belirteç değişimi başarısız olur. Çözüm, sırrı doğrudan Django ORM aracılığıyla yeniden oluşturmaktır:
# Run through the authentik container's shell
cat > /tmp/regen_secret.py << 'EOF'
from authentik.providers.oauth2.models import OAuth2Provider
import secrets
provider = OAuth2Provider.objects.get(name="ECOSIRE Web OAuth2")
new_secret = secrets.token_urlsafe(64)
provider.client_secret = new_secret
provider.save()
print(f"New secret: {new_secret}")
EOF
docker exec -i authentik-worker /lifecycle/ak shell < /tmp/regen_secret.py
Daha sonra .env.local dosyanızda AUTHENTIK_CLIENT_SECRET öğesini güncelleyin.
Üretim Ortamı Değişkenleri
# Authentik
AUTHENTIK_URL=https://auth.ecosire.com
AUTHENTIK_INTERNAL_URL=http://localhost:9000 # Server-to-server
AUTHENTIK_CLIENT_ID=ecosire-web
AUTHENTIK_CLIENT_SECRET=your-generated-secret
# App URLs
API_URL=https://api.ecosire.com/api
FRONTEND_URL=https://ecosire.com
# Cookie security
COOKIE_SECRET=random-32-char-string-for-signing
NODE_ENV=production
Sıkça Sorulan Sorular
Neden NextAuth.js yerine Authentik kullanılmalı?
NextAuth.js, basit uygulamalar için mükemmel bir seçimdir ancak kimlik doğrulamasını Next.js uygulamanızla birleştirir. Authentik, NestJS, mobil uygulamalar, üçüncü taraf araçlar gibi her türlü çerçeveyle çalışan bağımsız bir kimlik sağlayıcıdır. Birden fazla uygulamada SSO'ya ihtiyacınız varsa, SAML kurumsal oturum açmayı desteklemek istiyorsanız veya kullanıcıları ve grupları uygulamanızdan ayrı olarak yönetmek için bir kullanıcı arayüzüne ihtiyacınız varsa Authentik daha iyi bir seçimdir.
OIDC ile OAuth2 arasındaki fark nedir?
OAuth2 bir yetkilendirme çerçevesidir; kimlik bilgilerini paylaşmadan erişimin nasıl verileceğini tanımlar. OIDC (OpenID Connect), OAuth2'nin üzerine kuruludur ve kimlik doğrulaması ekler; bir kullanıcının kim olduğunun nasıl doğrulanacağını belirtir. Authentik her ikisini de destekler. Uygulama girişiniz için OIDC'yi (size kullanıcı taleplerini içeren bir kimlik belirteci verir) istiyorsunuz. Yalnızca OAuth2, "bu uygulamanın Google Drive'ıma erişmesine izin ver" gibi üçüncü taraf yetkilendirme senaryoları içindir.
Jeton yenilemeyi nasıl hallederim?
Yenileme jetonunu kısıtlı bir yola sahip (ör. /auth/refresh) bir HttpOnly çerezinde saklayın. Erişim belirtecinin süresi dolduğunda, API'niz 401 değerini döndürür ve ön ucunuz, yenileme belirtecini kullanarak yeni bir erişim belirteci almak için /auth/refresh öğesini çağırır. Yenileme uç noktası, yenileme belirtecini yeni belirteçler için Authentik ile değiştirir ve yeni çerezler ayarlar. Yenileme sonrasında otomatik yeniden deneme ile API istemcinizdeki 401'i kullanın.
Authentik kurumsal SAML sağlayıcılarını yönetebilir mi?
Evet — Authentik, hem Hizmet Sağlayıcı hem de Kimlik Sağlayıcı olarak SAML 2.0'ı destekler. Okta, Azure AD veya Ping Identity kullanan kurumsal müşteriler için SAML federasyonunu, kullanıcıların kurumsal kimlik bilgileriyle oturum açmasını sağlayacak şekilde yapılandırabilirsiniz. Authentik, SAML iddialarını OIDC belirteçlerine dönüştürür; böylece uygulama kodunuzun SAML'yi doğrudan işlemesine gerek kalmaz.
Kimlik doğrulama akışlarını yerel olarak nasıl test ederim?
Authentik'i uygulamanızın yanında yerel olarak Docker Compose ile çalıştırın. Yönlendirme URI'lerini http://localhost:3000/auth/callback içerecek şekilde yapılandırın. Authentik'in test modunu yerel bir kullanıcıyla kullanın. Değişim kodu akışı açısından 60 saniyelik TTL, yerel kalkınma için yeterince cömerttir. OIDC akışında hata ayıklamanız gerekiyorsa Authentik'in yönetici paneli, Olaylar günlüğündeki tüm token değişim girişimlerini gösterir.
Sonraki Adımlar
Güvenli, üretime hazır bir kimlik doğrulama sisteminin uygulanması, herhangi bir uygulama için en kritik mühendislik zorluklarından biridir. ECOSIRE, Authentik'i HttpOnly çerez tabanlı kimlik doğrulaması, tek seferlik değişim kodları ve NestJS ile Next.js arasında tam OIDC entegrasyonu ile birden fazla uygulama için SSO sunarak üretimde çalıştırıyor.
Kimlik doğrulama sistemi mimarisine, Authentik dağıtımına veya eksiksiz bir kurumsal platforma ihtiyacınız varsa, nasıl yardımcı olabileceğimizi görmek için geliştirme hizmetlerimizi keşfedin.
Yazan
ECOSIRE Research and Development Team
ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.
İlgili Makaleler
JWT Authentication: Security Best Practices in 2026
Secure your APIs with JWT best practices: RS256 vs HS256, HttpOnly cookies, token rotation, refresh patterns, and common vulnerabilities to avoid in 2026.
Modern Uygulamalar için API Ağ Geçidi Kalıpları ve En İyi Uygulamalar
Ölçeklenebilir web mimarileri için hız sınırlama, kimlik doğrulama, istek yönlendirme, devre kesiciler ve API sürüm oluşturma dahil olmak üzere API ağ geçidi modellerini uygulayın.
Sıfır Güven Mimarisi Uygulaması: İşletmeler İçin Pratik Bir Kılavuz
Kimlik doğrulama, ağ bölümlendirme, cihaz güveni ve sürekli izlemeyi kapsayan pratik adımlarla sıfır güven mimarisini uygulayın.