Authentik OIDC/SSO: Vollständiger Integrationsleitfaden
Authentik ist ein Open-Source-Identitätsanbieter, der Ihnen SSO, OIDC, SAML und OAuth2 der Unternehmensklasse ohne die Komplexität (oder Kosten) von Okta oder Auth0 bietet. Für selbst gehostete Anwendungen stellt es die OIDC-Endpunkte, Benutzerverwaltung, gruppenbasierte Zugriffskontrolle und Unternehmensrichtlinien von Authentik bereit – alles in Ihrer eigenen Infrastruktur.
Dieser Leitfaden behandelt eine Produktions-Authentik-Integration mit Next.js 16 und NestJS 11: OAuth2-Anbieterkonfiguration, den sicheren Autorisierungscodefluss, einmaliges Austauschcodemuster, JWT-Validierung und die subtilen Probleme, die eine Authentifizierungsimplementierung ausmachen oder scheitern lassen.
Wichtige Erkenntnisse
– Authentifizierungstoken müssen in HttpOnly-Cookies gespeichert sein – niemals in localStorage oder Abfrageparametern – Verwenden Sie einen einmaligen Austauschcode (60-Sekunden-TTL), um das Token vom Rückruf an das Frontend zu übergeben – Verwenden Sie immer
AUTHENTIK_INTERNAL_URLfür den Server-zu-Server-Token-Austausch (Netzwerk-Hops vermeiden)
- Die Ablaufprüfung des Austauschcodes muss VOR dem Löschen aus dem Cache erfolgen (Verhinderung von Race Conditions). – Über die API neu generierte Authentik-Client-Geheimnisse weisen möglicherweise eine Hashing-Diskrepanz auf – verwenden Sie Django ORM direkt – JWT-Ansprüche müssen
organizationIdenthalten – kodieren Sie es in der Authentik-Eigenschaftszuordnungcookie-parser-Middleware ist in NestJS erforderlich, bevor die JWT-Extraktion aus Cookies funktioniert – Der OIDC-Erkennungsendpunkt stellt alle Token-/Benutzerinfo-URLs bereit – codieren Sie sie nicht fest
Authentik-Konfiguration
Erstellen des OAuth2-Anbieters
Im Authentik-Admin-Panel (/if/admin/):
- Gehen Sie zu Anwendungen > Anbieter > Erstellen
- Wählen Sie OAuth2/OpenID-Anbieter
- Konfigurieren:
- Name:
ECOSIRE Web OAuth2 - Autorisierungsablauf:
default-provider-authorization-implicit-consent - Client-Typ:
Confidential - Client-ID:
ecosire-web(Sie wählen dies) - Umleitungs-URIs:
https://api.ecosire.com/api/auth/callback https://ecosire.com/auth/callback - Signaturschlüssel: Wählen Sie Ihr Standardzertifikat aus
- Umfang:
openid,email,profile
- Erstellen Sie eine Anwendung, die diesen Anbieter verwendet
Benutzerdefinierte Eigenschaftszuordnung
Um organizationId in das JWT aufzunehmen, erstellen Sie eine Eigenschaftszuordnung:
In Authentik Admin > Anpassung > Eigenschaftszuordnungen > Erstellen > Bereichszuordnung:
# Name: Organization ID Scope
# Scope name: organization
return {
"organizationId": request.user.attributes.get("organizationId", str(request.user.pk)),
"name": request.user.name,
}
Fügen Sie diese Zuordnung zur „Scopes“-Liste Ihres OAuth2-Anbieters hinzu und fügen Sie organization in die von Ihrer Anwendung angeforderten Bereiche ein.
Backend: NestJS-Authentifizierungsmodul
JWT-Strategie
// 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';
}
}
Cookie-Konfiguration
// 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));
Auth-Controller
// 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 });
}
}
Authentifizierungsdienst: Codemuster austauschen
Der einmalige Austauschcode ist der Schlüssel zur Vermeidung von Token-in-URL (die im Browserverlauf, in Serverprotokollen und in Referrer-Headern angezeigt werden):
// 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;
}
}
Frontend: Callback-Handler
// 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>
);
}
Behebung des Client-Secret-Hashing-Problems
Ein häufiges Authentik-Problem: Über die REST-API festgelegte Client-Geheimnisse weisen manchmal eine Hashing-Diskrepanz auf und schlagen beim OIDC-Token-Austausch fehl. Die Lösung besteht darin, das Geheimnis direkt über Django ORM neu zu generieren:
# 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
Aktualisieren Sie dann AUTHENTIK_CLIENT_SECRET in Ihrem .env.local.
Produktionsumgebungsvariablen
# 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
Häufig gestellte Fragen
Warum Authentik anstelle von NextAuth.js verwenden?
NextAuth.js ist eine gute Wahl für einfache Anwendungen, aber es koppelt die Authentifizierung an Ihre Next.js-App. Authentik ist ein eigenständiger Identitätsanbieter, der mit jedem Framework funktioniert – NestJS, mobile Apps, Tools von Drittanbietern. Wenn Sie SSO über mehrere Anwendungen hinweg benötigen, die SAML-Unternehmensanmeldung unterstützen möchten oder eine Benutzeroberfläche zum Verwalten von Benutzern und Gruppen getrennt von Ihrer App benötigen, ist Authentik die bessere Wahl.
Was ist der Unterschied zwischen OIDC und OAuth2?
OAuth2 ist ein Autorisierungsframework – es definiert, wie Zugriff gewährt wird, ohne Anmeldeinformationen weiterzugeben. OIDC (OpenID Connect) basiert auf OAuth2 und fügt Authentifizierung hinzu – es gibt an, wie überprüft wird, wer ein Benutzer ist. Authentik unterstützt beides. Für Ihre Anwendungsanmeldung benötigen Sie OIDC (das Ihnen ein ID-Token mit Benutzeransprüchen gibt). OAuth2 allein ist für Drittanbieter-Autorisierungsszenarien wie „Dieser App den Zugriff auf mein Google Drive erlauben“ vorgesehen.
Wie gehe ich mit der Tokenaktualisierung um?
Speichern Sie das Aktualisierungstoken in einem HttpOnly-Cookie mit einem eingeschränkten Pfad (z. B. /auth/refresh). Wenn das Zugriffstoken abläuft, gibt Ihre API 401 zurück und Ihr Frontend ruft /auth/refresh auf, um mithilfe des Aktualisierungstokens ein neues Zugriffstoken abzurufen. Der Refresh-Endpunkt tauscht den Refresh-Token mit Authentik gegen neue Token aus und setzt neue Cookies. Behandeln Sie den 401-Fehler in Ihrem API-Client mit einem automatischen Wiederholungsversuch nach der Aktualisierung.
Kann Authentik mit Unternehmens-SAML-Anbietern umgehen?
Ja – Authentik unterstützt SAML 2.0 sowohl als Dienstanbieter als auch als Identitätsanbieter. Für Unternehmenskunden, die Okta, Azure AD oder Ping Identity verwenden, können Sie den SAML-Verbund konfigurieren, sodass sich Benutzer mit ihren Unternehmensanmeldeinformationen anmelden. Authentik übersetzt SAML-Assertionen in OIDC-Tokens, sodass Ihr Anwendungscode SAML nicht direkt verarbeiten muss.
Wie teste ich Authentifizierungsflüsse lokal?
Führen Sie Authentik mit Docker Compose lokal neben Ihrer Anwendung aus. Konfigurieren Sie Umleitungs-URIs so, dass sie http://localhost:3000/auth/callback enthalten. Verwenden Sie den Testmodus von Authentik mit einem lokalen Benutzer. Für den Austauschcodefluss ist die 60-Sekunden-TTL großzügig genug für die lokale Entwicklung. Wenn Sie den OIDC-Fluss debuggen müssen, zeigt das Admin-Panel von Authentik alle Token-Austauschversuche im Ereignisprotokoll an.
Nächste Schritte
Die Implementierung eines sicheren, produktionsbereiten Authentifizierungssystems ist eine der kritischsten technischen Herausforderungen für jede Anwendung. ECOSIRE führt Authentik in der Produktion aus und bietet SSO für mehrere Anwendungen mit HttpOnly-Cookie-basierter Authentifizierung, einmaligen Austauschcodes und vollständiger OIDC-Integration zwischen NestJS und Next.js.
Ganz gleich, ob Sie eine Authentifizierungssystemarchitektur, eine Authentik-Bereitstellung oder eine vollständige Unternehmensplattform benötigen, [entdecken Sie unsere Entwicklungsdienste] (/services), um zu erfahren, wie wir Ihnen helfen können.
Geschrieben von
ECOSIRE Research and Development Team
Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.
Verwandte Artikel
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.
API-Gateway-Muster und Best Practices für moderne Anwendungen
Implementieren Sie API-Gateway-Muster einschließlich Ratenbegrenzung, Authentifizierung, Anforderungsrouting, Leistungsschalter und API-Versionierung für skalierbare Webarchitekturen.
Implementierung der Zero-Trust-Architektur: Ein praktischer Leitfaden für Unternehmen
Implementieren Sie eine Zero-Trust-Architektur mit praktischen Schritten zur Identitätsüberprüfung, Netzwerksegmentierung, Gerätevertrauen und kontinuierlicher Überwachung.