Authentik OIDC/SSO: Guia completo de integração
Authentik é um provedor de identidade de código aberto que oferece SSO, OIDC, SAML e OAuth2 de nível empresarial sem a complexidade (ou custo) do Okta ou Auth0. Para aplicativos auto-hospedados, ele fornece endpoints OIDC da Authentik, gerenciamento de usuários, controle de acesso baseado em grupo e políticas corporativas – tudo em sua própria infraestrutura.
Este guia cobre uma integração de produção do Authentik com Next.js 16 e NestJS 11: configuração do provedor OAuth2, o fluxo de código de autorização seguro, padrão de código de troca único, validação JWT e os problemas sutis que determinam ou interrompem uma implementação de autenticação.
Principais conclusões
- Os tokens de autenticação devem residir em cookies HttpOnly — nunca em localStorage ou parâmetros de consulta
- Use um código de troca único (TTL de 60 segundos) para passar o token do retorno de chamada para o frontend
- Sempre use
AUTHENTIK_INTERNAL_URLpara troca de tokens de servidor para servidor (evite saltos de rede)- A verificação de expiração do código de troca deve acontecer ANTES da exclusão do cache (prevenção de condição de corrida)
- Os segredos do cliente Authentik regenerados via API podem ter uma incompatibilidade de hash - use Django ORM diretamente
- As declarações JWT devem incluir
organizationId— codifique-o no mapeamento de propriedades Authentik- O middleware
cookie-parseré necessário no NestJS antes que a extração JWT dos cookies funcione- O endpoint de descoberta OIDC fornece todos os URLs de token/informações do usuário — não os codifique
Configuração autêntica
Criando o provedor OAuth2
No painel de administração do Authentik (/if/admin/):
- Vá para Aplicativos > Provedores > Criar
- Selecione Provedor OAuth2/OpenID
- Configurar:
- Nome:
ECOSIRE Web OAuth2 - Fluxo de autorização:
default-provider-authorization-implicit-consent - Tipo de cliente:
Confidential - ID do cliente:
ecosire-web(você escolhe isso) - URIs de redirecionamento:
https://api.ecosire.com/api/auth/callback https://ecosire.com/auth/callback - Chave de assinatura: selecione seu certificado padrão
- Escopos:
openid,email,profile
- Crie um Aplicativo que use este provedor
Mapeamento de propriedades personalizadas
Para incluir organizationId no JWT, crie um mapeamento de propriedades:
No administrador do Authentik > Personalização > Mapeamentos de propriedades > Criar > Mapeamento de escopo:
# Name: Organization ID Scope
# Scope name: organization
return {
"organizationId": request.user.attributes.get("organizationId", str(request.user.pk)),
"name": request.user.name,
}
Adicione esse mapeamento à lista de "Escopos" do seu provedor OAuth2 e inclua organization nos escopos solicitados pelo seu aplicativo.
Back-end: Módulo de autenticação NestJS
Estratégia JWT
// 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';
}
}
Configuração de cookies
// 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));
Controlador de autenticação
// 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 });
}
}
Serviço de autenticação: padrão de código Exchange
O código de troca único é a chave para evitar token-in-URL (que aparece no histórico do navegador, logs do servidor e cabeçalhos de referência):
// 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: manipulador de retorno de chamada
// 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>
);
}
Corrigindo o problema de hashing do segredo do cliente
Um problema comum do Authentik: os segredos do cliente definidos por meio da API REST às vezes apresentam incompatibilidade de hash e falham na troca de token OIDC. A correção é regenerar o segredo diretamente via Django ORM:
# 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
Em seguida, atualize AUTHENTIK_CLIENT_SECRET em seu .env.local.
Variáveis de ambiente de produção
# 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
Perguntas frequentes
Por que usar Authentik em vez de NextAuth.js?
NextAuth.js é uma ótima opção para aplicativos simples, mas combina autenticação com seu aplicativo Next.js. Authentik é um provedor de identidade independente que funciona com qualquer estrutura – NestJS, aplicativos móveis, ferramentas de terceiros. Se você precisar de SSO em vários aplicativos, quiser oferecer suporte ao login corporativo SAML ou precisar de uma UI para gerenciar usuários e grupos separados do seu aplicativo, o Authentik é a melhor escolha.
Qual é a diferença entre OIDC e OAuth2?
OAuth2 é uma estrutura de autorização — define como conceder acesso sem compartilhar credenciais. OIDC (OpenID Connect) é construído sobre OAuth2 e adiciona autenticação – especifica como verificar quem é um usuário. Authentik oferece suporte a ambos. Para o login do seu aplicativo, você deseja o OIDC (que fornece um token de ID com declarações do usuário). Somente o OAuth2 serve para cenários de autorização de terceiros, como "permitir que este aplicativo acesse meu Google Drive".
Como lidar com a atualização do token?
Armazene o token de atualização em um cookie HttpOnly com um caminho restrito (por exemplo, /auth/refresh). Quando o token de acesso expira, sua API retorna 401 e seu frontend chama /auth/refresh para obter um novo token de acesso usando o token de atualização. O endpoint de atualização troca o token de atualização com o Authentik por novos tokens e define novos cookies. Lide com o 401 em seu cliente API com nova tentativa automática após atualização.
O Authentik pode lidar com provedores SAML corporativos?
Sim — Authentik oferece suporte a SAML 2.0 como provedor de serviços e provedor de identidade. Para clientes empresariais que usam Okta, Azure AD ou Ping Identity, você pode configurar a federação SAML para que os usuários façam login com suas credenciais corporativas. Authentik traduz asserções SAML em tokens OIDC, para que o código do seu aplicativo não precise lidar diretamente com SAML.
Como faço para testar fluxos de autenticação localmente?
Execute o Authentik com Docker Compose localmente junto com seu aplicativo. Configure URIs de redirecionamento para incluir http://localhost:3000/auth/callback. Use o modo de teste do Authentik com um usuário local. Para o fluxo do código de troca, o TTL de 60 segundos é generoso o suficiente para o desenvolvimento local. Se você precisar depurar o fluxo OIDC, o painel de administração do Authentik mostrará todas as tentativas de troca de tokens no log de eventos.
Próximas etapas
Implementar um sistema de autenticação seguro e pronto para produção é um dos desafios de engenharia mais críticos para qualquer aplicação. ECOSIRE executa Authentik em produção servindo SSO para vários aplicativos, com autenticação baseada em cookie HttpOnly, códigos de troca únicos e integração OIDC completa entre NestJS e Next.js.
Se você precisa de uma arquitetura de sistema de autenticação, implantação do Authentik ou uma plataforma corporativa completa, explore nossos serviços de desenvolvimento para ver como podemos ajudar.
Escrito por
ECOSIRE Research and Development Team
Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.
Artigos Relacionados
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.
Padrões de gateway de API e práticas recomendadas para aplicativos modernos
Implemente padrões de gateway de API, incluindo limitação de taxa, autenticação, roteamento de solicitações, disjuntores e controle de versão de API para arquiteturas web escaláveis.
Implementação da arquitetura Zero Trust: um guia prático para empresas
Implemente uma arquitetura de confiança zero com etapas práticas que abrangem verificação de identidade, segmentação de rede, confiança em dispositivos e monitoramento contínuo.