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
| Recurso | Descrição |
|---|---|
| Provider de Autenticação | Keycloak (OAuth 2.0 / OIDC) |
| Biblioteca de Autenticação | NextAuth.js v4 |
| Estratégia de Sessão | JWT (JSON Web Token) |
| Refresh Automático | Token refresh antes da expiração |
| Multi-organização | Suporte a contexto de múltiplas organizações |
| Token Contextual | Token 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
- Acesse o Admin Console do Keycloak:
https://auth.vidyacode.com.br/admin - Selecione o realm configurado:
vidyacode-saas - Navegue para Clients no menu lateral
- Localize ou crie o cliente:
app-frontend-manager-with-auth
Configurações Obrigatórias
1. Client Settings (Aba Settings)
| Campo | Valor | Descrição |
|---|---|---|
| Client ID | app-frontend-manager-with-auth | Identificador único do cliente |
| Client Protocol | openid-connect | Protocolo OAuth 2.0/OIDC |
| Access Type | confidential | Requer client secret para autenticação |
| Standard Flow Enabled | ON | Habilita Authorization Code Flow |
| Direct Access Grants Enabled | ON | Permite autenticação direta |
| Service Accounts Enabled | OFF | Desabilitado 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
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
+
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
- Na tela do cliente, vá para a aba Credentials
- Copie o valor do campo Secret
- Configure na variável
KEYCLOAK_CLIENT_SECRETno.env
KEYCLOAK_CLIENT_SECRET="sua-client-secret-aqui"
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

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:
- Verifique se
http://localhost:3000/api/auth/callback/keycloakestá na lista - Certifique-se de usar
/*para permitir todas as rotas - Salve as alterações no Keycloak
Erro: "Invalid client or Invalid client credentials"
Causa: Client ID ou Client Secret incorretos.
Solução:
- Verifique se
KEYCLOAK_CLIENT_IDcorresponde ao Client ID no Keycloak - Regenere o Client Secret se necessário (aba Credentials)
- Atualize a variável
KEYCLOAK_CLIENT_SECRET
Erro: CORS Policy
Causa: Web Origins não configurado corretamente.
Solução:
- Adicione o domínio do frontend em Web Origins
- Use
+para herdar todas as origens válidas - 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:
- Usuário insere o email
- Sistema redireciona para Keycloak com
login_hint - Keycloak processa autenticação
- Callback retorna para aplicação com código de autorização
- 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:
| Callback | Responsabilidade |
|---|---|
jwt | Gerencia lifecycle dos tokens (criação, validação, refresh) |
session | Adiciona 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:
| Campo | Tipo | Validação | Obrigatório |
|---|---|---|---|
organization | Text | Min 1 caractere | ✅ |
activityType | Select | ID da categoria | ✅ |
name | Text | Min 5, max 80 caracteres | ✅ |
email | Email válido | ✅ | |
password | Password | Min 8 chars, 1 maiúscula, 1 número, 1 especial | ✅ |
confirmPassword | Password | Deve corresponder à senha | ✅ |
sandbox | Checkbox | "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;
}
3. Armazenamento em Cookie
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",
});
};
4. API Route: Salvar Cookie
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:
| Atributo | Valor | Descrição |
|---|---|---|
HttpOnly | true | Previne acesso via JavaScript (XSS) |
SameSite | Lax | Proteção contra CSRF |
Max-Age | 2592000 | 30 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:
- Validação Periódica: Verifica token contextual a cada 60 segundos
- Detecção de Erro: Identifica falhas no refresh do access token
- Logout Automático: Remove cookies e redireciona para login
- Prevenção de Duplicação: Usa
useRefpara 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:
- Verifica se existe sessão válida do NextAuth
- Executa
generateUserContext()para obter organizações e token contextual - Valida se o contexto foi gerado com sucesso
- Prepara estado inicial do React Query (SSR)
- 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:
- Marca o token com
error: "RefreshTokenError" SessionWatchdetecta o erro- Executa logout automático
- Remove cookies de sessão
- Redireciona para
/login
Arquivos Envolvidos
Autenticação
| Arquivo | Responsabilidade |
|---|---|
| src/lib/auth-options.ts | Configuração do NextAuth com Keycloak |
| src/app/(public)/login/components/login-form.tsx | Formulário de login |
| src/app/(public)/login/components/register-form.tsx | Formulário de cadastro |
| src/app/(public)/login/components/email-verification-modal-content.tsx | Modal de confirmação de email |
Token Contextual
| Arquivo | Responsabilidade |
|---|---|
| src/app/actions/generate-user-context/index.ts | Server action para gerar contexto |
| src/data/auth-contexts/get-user-token.controller.ts | Buscar token contextual (servidor) |
| src/data/auth-contexts/get-user-token-client.controller.ts | Buscar token contextual (cliente) |
| src/data/auth-contexts/organizations-get-all.controller.ts | Listar organizações do usuário |
| src/app/(private)/components/client-set-contextual-token.tsx | Armazenar token em cookie (client) |
| src/lib/api/contextual-token/set-contextual-token-cookie.ts | Helper para salvar cookie |
| src/app/api/contextual-token/route.ts | API route para criar cookie HttpOnly |
Monitoramento
| Arquivo | Responsabilidade |
|---|---|
| src/app/(private)/components/session-watch.tsx | Monitora validade da sessão |
| src/hooks/use-contextual-token-validate.ts | Hook de validação do token contextual |
src/hooks/use-contextual-token-validate.ts | Hook de validação do token contextual |
Hooks
| Arquivo | Responsabilidade |
|---|---|
| src/hooks/auth/useUserRegister.ts | Hook para cadastro de usuário |
| src/hooks/auth/useUserGenerateContextToken.ts | Hook para gerar token (servidor) |
| src/hooks/auth/useUserGenerateClientContextToken.ts | Hook para gerar token (cliente) |
Layout
| Arquivo | Responsabilidade |
|---|---|
| src/app/(private)/layout.tsx | Layout 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ática | Implementação | Arquivo |
|---|---|---|
| HttpOnly Cookies | Token contextual inacessível via JavaScript | src/app/api/contextual-token/route.ts |
| SameSite Lax | Proteção contra CSRF | src/app/api/contextual-token/route.ts |
| Validação de Senha | Mínimo 8 chars, maiúscula, número, especial | src/app/(public)/login/types/schemas.ts |
| Server-Side Session | Sessão verificada no servidor | src/app/(private)/layout.tsx |
| Token Refresh | Renovação automática antes da expiração | src/lib/auth-options.ts |
| Logout em Erro | Desconexão automática em falhas | src/app/(private)/components/session-watch.tsx |
| Environment Variables | Secrets armazenados em variáveis de ambiente | .env |