Skip to content

Authentication - Autenticação OAuth

Fluxo de Login Social

O sistema utiliza autenticação OAuth 2.0 com popup para os provedores: Google, LinkedIn, Microsoft e GitHub.

IMPORTANTE: A URL OAuth é construída diretamente no frontend usando as credenciais de cada provedor. O backend NÃO fornece URLs de OAuth.

LoginPage

Clica no botão social (ex: Google)

useOAuthPopup.openPopup('google')

Frontend constrói URL OAuth com buildOAuthUrl('google')

Popup abre com URL OAuth do provedor

Usuário autoriza no provedor

Provedor redireciona para /auth/callback/:provider?code=...

OAuthCallback.vue (no popup):
  - Extrai o código de autorização da URL
  - Envia postMessage({ type: 'oauth:success', provider, code })
  - Fecha popup

LoginPage recebe mensagem:
  - Chama socialAuthService({ provider, code })

Backend troca code por token com provedor e retorna:
  {
    user, token, companies,
    action: 'login' | 'register',
    needsProfileCompletion: boolean
  }

Se needsProfileCompletion:
  → Redireciona para /complete-profile
Senão:
  → Redireciona para /select-company

Construção da URL OAuth (Frontend)

A URL OAuth é construída usando constantes definidas em src/domain/auth/constants.ts:

typescript
export const OAUTH_CONFIGS: Record<AuthProvider, OAuthConfig> = {
  google: {
    authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
    clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
    scope: 'openid email profile',
    responseType: 'code',
  },
  // ... outros provedores
}

export const buildOAuthUrl = (provider: AuthProvider): string => {
  const config = OAUTH_CONFIGS[provider]
  const params = new URLSearchParams({
    client_id: config.clientId,
    redirect_uri: `${OAUTH_REDIRECT_URI}/${provider}`,
    response_type: config.responseType,
    scope: config.scope,
    state: crypto.randomUUID(),
  })
  return `${config.authUrl}?${params.toString()}`
}

Variáveis de Ambiente para OAuth

env
VITE_GOOGLE_CLIENT_ID="seu-google-client-id"
VITE_LINKEDIN_CLIENT_ID="seu-linkedin-client-id"
VITE_MICROSOFT_CLIENT_ID="seu-microsoft-client-id"
VITE_AUTH_GITHUB_CLIENT_ID="seu-github-client-id"
VITE_MOCK_MODE="false"

Endpoints de Autenticação

EndpointMétodoDescrição
/auth/socialPOSTAutentica com código OAuth e retorna dados do usuário
/auth/complete-profilePOSTCompleta dados do perfil do usuário
/auth/companiesGETLista empresas do usuário autenticado
/auth/create-companyPOSTCadastra nova empresa
/users/meGETRetorna dados do usuário autenticado
/users/mePUTAtualiza perfil do usuário (nome, telefone, CPF)
/users/me/passwordPUTAltera a senha do usuário
/app/exchange-tokenPOSTTroca QR token ou master key por sessão autenticada (público, sem JWT)
/app/mobile-logoutPOSTLogout do app mobile; envia deviceId para revogar a sessão (autenticado)
/app/devicesGETLista os aparelhos conectados da conta (autenticado)
/app/devices/:idDELETEDesconecta/revoga um aparelho específico (autenticado)

Login do app mobile

O app aceita duas variantes do mesmo input para autenticar:

  1. QR token: gerado no painel web, validade 2 min, uso único.
  2. Master key: token administrativo configurado via env var no backend, usado para QA interno e revisão da Google Play Store.

Ambos são trocados pela mesma rota POST /app/exchange-token com body { token, deviceId, deviceInfo }. O frontend não distingue um do outro: qualquer string aceita pelo backend libera a sessão. O backend valida o formato/origem e responde com AuthResponseDto (user + accessToken + companies + tenant).

Tenant no login do app (pular a tela de access)

Desde 2026-07-03, o accessToken do app carrega o claim tenant (a empresa do QR/pareamento) e a resposta AuthResponseDto inclui tenant: { companyId, slug } | null. O app não decodifica o JWT; ele lê o tenant do corpo da resposta. No AuthContext.login, quando há tenant.companyId, o app carrega o /me/access (já escopado pelo backend), resolve a experiência e seleciona automaticamente:

  • contrato cujo borrower.id === tenant.companyIdexperience = Contract (entra direto no contrato);
  • senão, empresa cujo id === tenant.companyIdexperience = Borrower.

Com a experience já definida, o resolveRootRedirect leva direto a /contract ou /borrower, sem passar pela tela /(access). O claim assinado no token é o que o backend usa para o bloqueio; o tenant no corpo é só dica de roteamento.

Identificação do dispositivo e revogação

O app gera e persiste um deviceId estável (UUID) no SecureStore (contrasync_device_id) e o envia no exchange-token e no mobile-logout. O backend cria/atualiza uma sessão por (userId, deviceId) em app_device_sessions e devolve um accessToken com o claim sid. Vários aparelhos podem ficar conectados ao mesmo tempo (sem limite). Quando o painel desconecta um aparelho, o sid é revogado e a próxima requisição daquele app recebe 401 — o interceptor de resposta já trata 401 limpando o SecureStore e disparando o logout. O deviceId não é apagado no logout, para o mesmo aparelho reusar a sessão ao parear de novo. Detalhes em api/app-devices.md.


Headers de Requisição

Todas as requisições autenticadas incluem automaticamente:

HeaderOrigemDescrição
AuthorizationlocalStorageToken Bearer do usuário
x-company-idsessionStorageID da empresa selecionada

O header x-company-id é adicionado automaticamente pelo interceptor do Axios quando há uma empresa selecionada no sessionStorage.


Request do Endpoint /auth/social

typescript
type SocialAuthRequest = {
  provider: AuthProvider // 'google' | 'linkedin' | 'microsoft' | 'github'
  code: string // Código de autorização retornado pelo provedor
}

Response do Endpoint /auth/social

typescript
type SocialAuthResponse = {
  user: User
  token: string
  companies: Company[]
  action: 'login' | 'register'
  needsProfileCompletion: boolean
}

Composable useOAuthPopup

Gerencia o fluxo de popup OAuth:

typescript
const { openPopup, isLoading, error } = useOAuthPopup()

const result = await openPopup('google')
// result: { provider: 'google', code: '...' }

Tela de Completar Cadastro

Exibida quando needsProfileCompletion: true na resposta do /auth/social.

IMPORTANTE: Esta é uma página full-screen sem menu lateral (não usa ContainerApp).

Campos obrigatórios:

  • Nome Completo
  • Telefone

Campos opcionais:

  • CPF

Páginas de Autenticação Full-Screen

As seguintes páginas são renderizadas em full-screen, sem o layout ContainerApp:

PáginaRotaDescrição
LoginPage/loginTela de login social
OAuthCallback/auth/callback/:providerCallback do OAuth (popup)
CompleteProfilePage/complete-profileCompletar cadastro
SelectCompanyPage/select-companySelecionar empresa