Pular para o conteúdo principal

Sistema de Autenticação e Integração com Keycloak

Última atualização: 06 de janeiro de 2026

Visão Geral

Sistema completo de autenticação implementado com NextAuth.js e Keycloak como provedor de identidade. A solução implementa fluxo OAuth 2.0/OIDC com refresh automático de tokens, autenticação de múltiplas organizações e tokens contextuais para acesso aos recursos da API.

Características Principais

RecursoDescrição
Provider de AutenticaçãoKeycloak (OAuth 2.0 / OIDC)
Biblioteca de AutenticaçãoNextAuth.js v4
Estratégia de SessãoJWT (JSON Web Token)
Refresh AutomáticoToken refresh antes da expiração
Multi-organizaçãoSuporte a contexto de múltiplas organizações
Token ContextualToken específico por organização selecionada

Arquitetura do Sistema


Variáveis de Ambiente

Configurações Necessárias

# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="sua-secret-key-aqui"

# API Backend
API_URL="http://api.exemplo.com"
NEXT_PUBLIC_API_URL="http://api.exemplo.com"

# Keycloak
KEYCLOAK_URL="https://auth.exemplo.com"
KEYCLOAK_REALM="seu-realm"
KEYCLOAK_ISSUER="${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}"
KEYCLOAK_CLIENT_ID="app-frontend-manager-with-auth"
KEYCLOAK_CLIENT_SECRET="sua-client-secret"

Geração do NEXTAUTH_SECRET

# Usando OpenSSL
openssl rand -base64 32

# Usando Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Configuração do Cliente no Keycloak

Acesso ao Admin Console

  1. Acesse o Admin Console do Keycloak: https://auth.vidyacode.com.br/admin
  2. Selecione o realm configurado: vidyacode-saas
  3. Navegue para Clients no menu lateral
  4. Localize ou crie o cliente: app-frontend-manager-with-auth

Configurações Obrigatórias

1. Client Settings (Aba Settings)

CampoValorDescrição
Client IDapp-frontend-manager-with-authIdentificador único do cliente
Client Protocolopenid-connectProtocolo OAuth 2.0/OIDC
Access TypeconfidentialRequer client secret para autenticação
Standard Flow EnabledONHabilita Authorization Code Flow
Direct Access Grants EnabledONPermite autenticação direta
Service Accounts EnabledOFFDesabilitado para aplicações web

2. Valid Redirect URIs

As Redirect URIs são os endpoints para onde o Keycloak pode redirecionar após autenticação bem-sucedida.

Configuração para Desenvolvimento:

http://localhost:3000/*
http://localhost:3000/api/auth/callback/keycloak

Configuração para Produção:

https://app.exemplo.com/*
https://app.exemplo.com/api/auth/callback/keycloak
Importante

O endpoint /api/auth/callback/keycloak é criado automaticamente pelo NextAuth.js e deve estar presente na lista de Redirect URIs válidas.

3. Valid Post Logout Redirect URIs

Define para onde o usuário é redirecionado após o logout.

Configuração:

http://localhost:3000/login
http://app.manager.vidyacode.com.br/login
+
Dica

Use + para herdar todas as Valid Redirect URIs configuradas anteriormente.

4. Web Origins

Configuração de CORS para permitir requisições do frontend.

Configuração:

http://localhost:3000
https://app.manager.vidyacode.com.br
+

O símbolo + permite todas as origens das Valid Redirect URIs.

Client Secret

Obtendo o Client Secret

  1. Na tela do cliente, vá para a aba Credentials
  2. Copie o valor do campo Secret
  3. Configure na variável KEYCLOAK_CLIENT_SECRET no .env
KEYCLOAK_CLIENT_SECRET="sua-client-secret-aqui"
Segurança

Nunca commite o client secret em repositórios públicos. Use variáveis de ambiente e mantenha o .env no .gitignore.

Exemplo de Configuração Completa

Visual da Configuração no Keycloak

Keycloak Admin Console

Fluxo de Callback

Troubleshooting de Configuração

Erro: "Invalid redirect_uri"

Causa: A URL de callback não está configurada nas Valid Redirect URIs.

Solução:

  1. Verifique se http://localhost:3000/api/auth/callback/keycloak está na lista
  2. Certifique-se de usar /* para permitir todas as rotas
  3. Salve as alterações no Keycloak

Erro: "Invalid client or Invalid client credentials"

Causa: Client ID ou Client Secret incorretos.

Solução:

  1. Verifique se KEYCLOAK_CLIENT_ID corresponde ao Client ID no Keycloak
  2. Regenere o Client Secret se necessário (aba Credentials)
  3. Atualize a variável KEYCLOAK_CLIENT_SECRET

Erro: CORS Policy

Causa: Web Origins não configurado corretamente.

Solução:

  1. Adicione o domínio do frontend em Web Origins
  2. Use + para herdar todas as origens válidas
  3. Reinicie a aplicação

Verificação da Configuração

Script de Teste

# Testar endpoint de descoberta (well-known)
curl https://auth.exemplo.com/realms/seu-realm/.well-known/openid-configuration

# Testar issuer
curl https://auth.exemplo.com/realms/seu-realm

Resposta Esperada

{
"issuer": "https://auth.exemplo.com/realms/seu-realm",
"authorization_endpoint": "https://auth.exemplo.com/realms/seu-realm/protocol/openid-connect/auth",
"token_endpoint": "https://auth.exemplo.com/realms/seu-realm/protocol/openid-connect/token",
"end_session_endpoint": "https://auth.exemplo.com/realms/seu-realm/protocol/openid-connect/logout"
}

Fluxo de Autenticação

1. Login do Usuário

Componente: LoginForm

Arquivo: src/app/(public)/login/components/login-form.tsx

const handleLoginFormSubmit = useCallback(async (data: LoginSchema) => {
const signInResponse = await signIn(
"keycloak",
{
callbackUrl: "/home",
},
{
login_hint: data.email,
},
);

if (signInResponse?.error) {
toast.error(getSignInError(signInResponse?.error));
return;
}
}, []);

Validação do formulário:

export const loginSchema = z.object({
email: z
.string()
.min(1, "Digite seu e-mail para prosseguir")
.email({ message: "E-mail inválido" })
.trim(),
});

Fluxo:

  1. Usuário insere o email
  2. Sistema redireciona para Keycloak com login_hint
  3. Keycloak processa autenticação
  4. Callback retorna para aplicação com código de autorização
  5. NextAuth troca código por tokens

2. Configuração do NextAuth

Arquivo: src/lib/auth-options.ts

Configuração Completa do NextAuth
export const authOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [
Keycloak({
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
issuer: process.env.KEYCLOAK_ISSUER! as string,
}),
],
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, account }): Promise<JWT> {
// Primeiro login - armazena tokens
if (account) {
return {
...token,
access_token: account.access_token as string,
expires_at: account.expires_at as number,
refresh_token: account.refresh_token as string,
};
}

// Token ainda válido
else if (Date.now() < (token.expires_at as number) * 1000) {
return token;
}

// Token expirado - refresh
else {
if (!token.refresh_token) throw new TypeError("Missing refresh_token");

try {
const response = await fetch(
`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`,
{
method: "POST",
body: new URLSearchParams({
client_id: process.env.KEYCLOAK_CLIENT_ID!,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refresh_token!,
}),
},
);

const tokensOrError = await response.json();

if (!response.ok) throw tokensOrError;

const newTokens = tokensOrError as {
access_token: string;
expires_in: number;
refresh_token?: string;
};

return {
...token,
access_token: newTokens.access_token,
expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
refresh_token: newTokens.refresh_token ?? token.refresh_token,
};
} catch (error) {
console.error("Error refreshing access_token", error);
token.error = "RefreshTokenError";
return token;
}
}
},

async session({ session, token }) {
session.accessToken = token.access_token as string;
session.error = token.error;
return session;
},
},
};

Callbacks Principais:

CallbackResponsabilidade
jwtGerencia lifecycle dos tokens (criação, validação, refresh)
sessionAdiciona accessToken e error à sessão do cliente

3. Refresh Automático de Token

Algoritmo de Refresh

Endpoint de Refresh:

POST {KEYCLOAK_ISSUER}/protocol/openid-connect/token

Body:
- client_id: KEYCLOAK_CLIENT_ID
- client_secret: KEYCLOAK_CLIENT_SECRET
- grant_type: refresh_token
- refresh_token: {refresh_token_atual}

Resposta:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI...",
"expires_in": 300,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI..."
}

Cadastro de Novo Usuário

1. Formulário de Registro

Componente: RegisterForm

Arquivo: src/app/(public)/login/components/register-form.tsx

Campos do formulário:

CampoTipoValidaçãoObrigatório
organizationTextMin 1 caractere
activityTypeSelectID da categoria
nameTextMin 5, max 80 caracteres
emailEmailEmail válido
passwordPasswordMin 8 chars, 1 maiúscula, 1 número, 1 especial
confirmPasswordPasswordDeve corresponder à senha
sandboxCheckbox"S" ou "N"

Validação do Schema:

Schema de Validação Zod
export const registerSchema = z
.object({
organization: z.string().trim().min(1, "Nome da organização é obrigatório"),
activityType: z.string().trim().min(1, "Tipo de atividade é obrigatório"),
name: z
.string()
.min(5, "O nome dever ter pelo menos 5 caracteres")
.max(80, "O nome dever ter no máximo 80 caracteres")
.nonempty({ message: "Nome obrigatório" }),
email: z
.string()
.email({ message: "Email inválido" })
.nonempty({ message: "Email obrigatório" }),
password: z
.string()
.nonempty({ message: "Senha obrigatória" })
.min(8, "A senha dever ter pelo menos 8 caracteres")
.regex(/[A-Z]/, "Insira ao menos uma letra maiúscula")
.regex(/\d/, "Insira ao menos um número")
.regex(/[\W_]/, "Insira ao menos um caractere especial"),
confirmPassword: z
.string()
.nonempty({ message: "Confirmação de senha obrigatória" }),
sandbox: z.enum(["S", "N"]).optional(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "As senhas não coincidem",
path: ["confirmPassword"],
});

2. Envio do Cadastro

Hook: useUserRegister

Arquivo: src/hooks/auth/useUserRegister.ts

export const useUserRegister = () => {
const { data, isPending, mutate, mutateAsync } = useMutation({
mutationFn: userRegisterController,
onSuccess: async ({ data: user }) => {
toast.success(user?.message ?? "Cadastro realizado com sucesso");
},
onError: (error: AxiosError) => {
showToastError(error);
},
});

return { data, isPending, mutate, mutateAsync };
};

3. Controller de Registro

Arquivo: src/data/auth/user-register.controller.ts

export const userRegisterController = async (
user: RegisterRequestDTO,
): AxiosPromise<DefinitiveRegisterResponseDTO> => {
const data = await ApiService.api.post<DefinitiveRegisterResponseDTO>(
"/auth/signup",
user,
);
return data;
};

Request DTO:

interface RegisterRequestDTO {
firstName: string;
lastName: string;
email: string;
password: string;
organizationName: string;
categoryId: string;
sandbox?: "S" | "N";
}

Response DTO:

interface DefinitiveRegisterResponseDTO {
message: string;
user: User;
organization: Organization;
}

4. Confirmação de Email

Após o cadastro bem-sucedido, o sistema exibe um modal solicitando confirmação de email:

Componente: EmailVerificationModalContent

Arquivo: src/app/(public)/login/components/email-verification-modal-content.tsx

Mensagem:

"Enviamos um link de confirmação para o endereço de e-mail informado no cadastro. Acesse sua caixa de entrada e confirme o e-mail para ativar sua conta antes de fazer login."


Sistema de Token Contextual

Conceito

O Token Contextual é um JWT específico para cada organização que o usuário tem acesso. Ele é necessário para fazer requisições autenticadas à API do backend, pois contém informações sobre:

  • Organização selecionada
  • Permissões do usuário na organização
  • Configurações do ERP (URL, database, etc.)

Fluxo de Geração

1. Server Action: generateUserContext

Arquivo: src/app/actions/generate-user-context/index.ts

Código Completo
export async function generateUserContext(): Promise<GenerateUserContextResponse> {
// 1. Obter sessão do NextAuth
const session = await getServerSession(authOptions);

if (!session) {
return {
isUserContextGenerated: false,
organizations: [],
contextualToken: "",
};
}

// 2. Buscar organizações do usuário
const organizations = await organizationsGetAllController();

if (!organizations?.length) {
return {
isUserContextGenerated: false,
organizations: [],
contextualToken: "",
};
}

// 3. Gerar token contextual para primeira organização
const userContextualToken = await getUserTokenController(
organizations[0].organizationId,
);

if (!userContextualToken?.token) {
return {
isUserContextGenerated: false,
organizations: [],
contextualToken: "",
};
}

// 4. Verificar se organização precisa de configuração inicial
const { shouldInitConfig } = await shouldInitOrganizationConfig({
user: userContextualToken,
});

if (shouldInitConfig) {
redirect(`/configurar-organizacao/${organizations[0].organizationId}`);
}

return {
isUserContextGenerated: true,
organizations,
contextualToken: userContextualToken.token,
};
}

Interface de Resposta:

interface GenerateUserContextResponse {
isUserContextGenerated: boolean;
organizations: OrganizationGetAllResponse[];
contextualToken: string;
}

interface OrganizationGetAllResponse {
organizationId: string;
organizationName: string;
operationMode: string;
}

2. Controller: getUserTokenController

Arquivo: src/data/auth-contexts/get-user-token.controller.ts

export const getUserTokenController = async (
organizationId: string,
): Promise<GetUserTokenResponse | undefined> => {
try {
const session = await getServerSession(authOptions);

if (!session?.accessToken) {
throw new Error("No access token found in session");
}

const response = await fetch(`${process.env.API_URL}/auth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ organizationId }),
});

if (!response.ok) {
console.error("Failed to fetch user token:", await response.text());
return undefined;
}

return (await response.json()) as GetUserTokenResponse;
} catch (error) {
console.error("Error fetching user token:", error);
}
};

Interface de Resposta:

interface GetUserTokenResponse {
token: string;
expiresIn: number;
organizationId: string;
erp: string | null;
urlerp: string | null;
orgToken: string | null;
database: string | null;
}

Componente: ClientSetContextualToken

Arquivo: src/app/(private)/components/client-set-contextual-token.tsx

const ClientSetContextualToken = ({ token }: { token: string }) => {
useEffect(() => {
if (!token) return;
setContextualTokenCookie(token);
}, [token]);

return null;
};

Função auxiliar:

Arquivo: src/lib/api/contextual-token/set-contextual-token-cookie.ts

export const setContextualTokenCookie = (token: string) => {
fetch("/api/contextual-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
credentials: "include",
});
};

Arquivo: src/app/api/contextual-token/route.ts

export async function POST(request: NextRequest) {
const { token } = await request.json();
const MAX_AGE = 60 * 60 * 24 * 30; // 30 dias

if (!token) {
return NextResponse.json(
{ error: "Contextual token is required." },
{ status: 400 },
);
}

return new Response(null, {
headers: {
"Set-Cookie": `contextual-token=${token}; HttpOnly; SameSite=Lax; Max-Age=${MAX_AGE}; Path=/`,
},
status: 200,
statusText: "Contextual token set successfully",
});
}

Atributos do Cookie:

AtributoValorDescrição
HttpOnlytruePrevine acesso via JavaScript (XSS)
SameSiteLaxProteção contra CSRF
Max-Age259200030 dias em segundos
Path/Disponível em toda aplicação

5. Hook Cliente: useUserGenerateClientContextToken

Arquivo: src/hooks/auth/useUserGenerateClientContextToken.ts

export const useUserGenerateClientContextToken = () => {
const { mutateAsync, isPending, isSuccess, error } = useMutation({
mutationFn: getUserTokenClientController,
onError: (error: AxiosError) => {
showToastError(error);
},
});

return { mutateAsync, isPending, isSuccess, error };
};

Controller:

Arquivo: src/data/auth-contexts/get-user-token-client.controller.ts

export const getUserTokenClientController = async ({
organizationId,
accessToken,
}: GetUserTokenParams): AxiosPromise<GetUserTokenResponse> => {
const data = await ApiService.api.post<GetUserTokenResponse>(
"/auth/token",
{ organizationId },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);

return data;
};

Monitoramento de Sessão

SessionWatch Component

Arquivo: src/app/(private)/components/session-watch.tsx

const SessionWatch: React.FC<{
refreshError: "RefreshTokenError" | undefined;
}> = ({ refreshError }) => {
const { data: session } = useSession();
const { data: isValidContextual, isLoading } = useContextualTokenValidate();
const isLoggingOut = useRef(false);

const handleLogout = async () => {
if (isLoggingOut.current) return;
isLoggingOut.current = true;

await Promise.all([
signOut({ callbackUrl: "/login" }),
deleteCookies(["contextual-token", "selected-organization"]),
]);
};

// Monitora validade do token contextual
useEffect(() => {
if (isLoading) return;
if (isValidContextual === false) {
handleLogout();
}
}, [isLoading, isValidContextual]);

// Monitora erros de refresh
useEffect(() => {
if (
session?.error === "RefreshTokenError" ||
refreshError === "RefreshTokenError"
) {
handleLogout();
}
}, [session?.error, refreshError]);

return null;
};

Funcionalidades:

  1. Validação Periódica: Verifica token contextual a cada 60 segundos
  2. Detecção de Erro: Identifica falhas no refresh do access token
  3. Logout Automático: Remove cookies e redireciona para login
  4. Prevenção de Duplicação: Usa useRef para evitar múltiplos logouts simultâneos

Hook de Validação

Arquivo: src/hooks/use-contextual-token-validate.ts

export const useContextualTokenValidate = () => {
const { data, isLoading } = useQuery({
queryKey: ["contextual-token-validate"],
queryFn: async (): Promise<boolean | undefined> => {
const response = await fetch("/api/contextual-token/validate", {
method: "GET",
cache: "no-store",
});

if (!response.ok) {
throw new Error("Failed to validate contextual token");
}

const data: { valid: boolean | undefined } = await response.json();
return data.valid;
},
refetchInterval: 60_000, // 1 minuto
refetchOnWindowFocus: true,
});

return { data, isLoading };
};

Layout Privado

Estrutura

Arquivo: src/app/(private)/layout.tsx

Código Completo do Layout
const PrivateLayout: React.FC<PrivateLayoutProps> = async ({ children }) => {
// 1. Verificar sessão
const session = await getServerSession(authOptions);

if (!session) {
redirect("/login");
}

// 2. Gerar contexto do usuário
const { isUserContextGenerated, organizations, contextualToken } =
await generateUserContext();

if (!isUserContextGenerated) {
redirect("/login");
}

// 3. Preparar estado inicial (React Query)
const dehydrateState = await dehydrateFeaturesState();

return (
<HydrationBoundary state={dehydrateState}>
{/* Monitoramento de sessão */}
<SessionWatch refreshError={session?.error} />

{/* Modal global */}
<Modal />

{/* Armazenar token contextual */}
<ClientSetContextualToken token={contextualToken} />

<div className="app-main overflow-x-hidden pt-16">
<SidebarProvider>
{/* Header */}
<Header />

{/* Verificador de organização */}
<OrganizationChecker userOrganizations={organizations}>
<OrganizationOptionsModal userOrganizations={organizations} />
</OrganizationChecker>

<div className="flex w-full">
{/* Sidebar */}
<AppSidebar userOrganizations={organizations} />

{/* Conteúdo principal */}
<main className="min-w-0 flex-1 overflow-x-hidden">
<div className="mx-auto w-full max-w-full">{children}</div>
</main>
</div>
</SidebarProvider>
</div>
</HydrationBoundary>
);
};

Ordem de execução:

  1. Verifica se existe sessão válida do NextAuth
  2. Executa generateUserContext() para obter organizações e token contextual
  3. Valida se o contexto foi gerado com sucesso
  4. Prepara estado inicial do React Query (SSR)
  5. Renderiza componentes de monitoramento e UI

Tratamento de Erros

Erros de Login

Arquivo: src/app/(public)/login/utils/sign-in-errors.ts

export const getSignInError = (error: string): string => {
const errors: Record<string, string> = {
OAuthSignin: "Erro ao iniciar autenticação com o provedor",
OAuthCallback: "Erro ao processar resposta do provedor",
OAuthCreateAccount: "Erro ao criar conta",
EmailCreateAccount: "Erro ao criar conta com email",
Callback: "Erro no callback de autenticação",
OAuthAccountNotLinked: "Esta conta já está vinculada a outro provedor",
EmailSignin: "Erro ao enviar email de autenticação",
CredentialsSignin: "Email ou senha inválidos",
SessionRequired: "Por favor, faça login para continuar",
Default: "Erro desconhecido ao tentar fazer login",
};

return errors[error] || errors.Default;
};

Erros de Refresh Token

Quando o refresh falha, o sistema:

  1. Marca o token com error: "RefreshTokenError"
  2. SessionWatch detecta o erro
  3. Executa logout automático
  4. Remove cookies de sessão
  5. Redireciona para /login

Arquivos Envolvidos

Autenticação

ArquivoResponsabilidade
src/lib/auth-options.tsConfiguração do NextAuth com Keycloak
src/app/(public)/login/components/login-form.tsxFormulário de login
src/app/(public)/login/components/register-form.tsxFormulário de cadastro
src/app/(public)/login/components/email-verification-modal-content.tsxModal de confirmação de email

Token Contextual

ArquivoResponsabilidade
src/app/actions/generate-user-context/index.tsServer action para gerar contexto
src/data/auth-contexts/get-user-token.controller.tsBuscar token contextual (servidor)
src/data/auth-contexts/get-user-token-client.controller.tsBuscar token contextual (cliente)
src/data/auth-contexts/organizations-get-all.controller.tsListar organizações do usuário
src/app/(private)/components/client-set-contextual-token.tsxArmazenar token em cookie (client)
src/lib/api/contextual-token/set-contextual-token-cookie.tsHelper para salvar cookie
src/app/api/contextual-token/route.tsAPI route para criar cookie HttpOnly

Monitoramento

ArquivoResponsabilidade
src/app/(private)/components/session-watch.tsxMonitora validade da sessão
src/hooks/use-contextual-token-validate.tsHook de validação do token contextual
src/hooks/use-contextual-token-validate.tsHook de validação do token contextual

Hooks

ArquivoResponsabilidade
src/hooks/auth/useUserRegister.tsHook para cadastro de usuário
src/hooks/auth/useUserGenerateContextToken.tsHook para gerar token (servidor)
src/hooks/auth/useUserGenerateClientContextToken.tsHook para gerar token (cliente)

Layout

ArquivoResponsabilidade
src/app/(private)/layout.tsxLayout privado com verificações

Tipos TypeScript

Session do NextAuth

import "next-auth";

declare module "next-auth" {
interface Session {
accessToken?: string;
error?: "RefreshTokenError" | string;
}
}

declare module "next-auth/jwt" {
interface JWT {
access_token?: string;
expires_at?: number;
refresh_token?: string;
error?: "RefreshTokenError" | string;
}
}

DTOs de Cadastro

// Request
interface RegisterRequestDTO {
firstName: string;
lastName: string;
email: string;
password: string;
organizationName: string;
categoryId: string;
sandbox?: "S" | "N";
}

// Response
interface DefinitiveRegisterResponseDTO {
message: string;
user: User;
organization: Organization;
}

Token Contextual

interface GetUserTokenResponse {
token: string;
expiresIn: number;
organizationId: string;
erp: string | null;
urlerp: string | null;
orgToken: string | null;
database: string | null;
}

Diagrama de Sequência Completo


Segurança

Boas Práticas Implementadas

PráticaImplementaçãoArquivo
HttpOnly CookiesToken contextual inacessível via JavaScriptsrc/app/api/contextual-token/route.ts
SameSite LaxProteção contra CSRFsrc/app/api/contextual-token/route.ts
Validação de SenhaMínimo 8 chars, maiúscula, número, especialsrc/app/(public)/login/types/schemas.ts
Server-Side SessionSessão verificada no servidorsrc/app/(private)/layout.tsx
Token RefreshRenovação automática antes da expiraçãosrc/lib/auth-options.ts
Logout em ErroDesconexão automática em falhassrc/app/(private)/components/session-watch.tsx
Environment VariablesSecrets armazenados em variáveis de ambiente.env

Referências