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​