Autenticação
A autenticação é fim-a-fim: o cliente (Android / iOS) coleta as credenciais, chama /v1/auth/login, recebe um access token (JWT) e um refresh token via cookie, e usa o JWT no header Authorization de todas as chamadas seguintes. O backend Go valida o token a cada requisição autenticada (assinatura + versão da senha + permissões da rota).
Visão geral do fluxo
┌─────────────┐ 1. POST /v1/auth/login (username, password, company_id?)
│ Mobile App │──────────────────────────────────────────────► ┌──────────────────┐
│ (Android / │ │ Go API (Fiber) │
│ iOS) │◄──────── 2. { token, expiration } + cookie ──────│ modules/auth │
└──────┬──────┘ Set-Cookie: refresh_token └──────────┬───────┘
│ │
│ 3. Persiste: jwt_token, perms_user, company_id, user_id │
│ │
│ 4. GET /v1/<qualquer> com Authorization: Bearer <jwt> │
├───────────────────────────────────────────────────────────────────►│
│ │
│ middleware: Authenticator → permissões → handler │
│◄───────────────────────────────────────────────────── 200 { data } │
│ │
│ 5. Quando JWT expira (~20 min) → POST /v1/auth/refresh │
│ (envia cookie refresh_token + body opcional) │
└───────────────────────────────────────────────────────────────────►│
│
◄── { token, expiration } novos ──┘
1 · Endpoints e regras de token
Declarações em src/modules/auth/routers/routers.go:
/v1/auth/login5-M. Audit crítico LOGIN. Aceita AuthLogin{ username, password, company_id?, remember? }./v1/auth/refreshrefresh_token (ou header). Audit crítico REFRESH./v1/auth/logoutLOGOUT./v1/auth/generate_access_tokenphone está cadastrado. Sem auth./v1/auth/validate_access_tokencpf + code). Caminho de login alternativo./v1/auth/company/changeLogin internamente (modo Refresh: true) para reemitir o JWT com novo companie_id.2 · TTLs e ambientes
| Token | Produção | Test mode | Dev (APP_ENV=dev) |
|---|---|---|---|
| Access (JWT) | 20 min | 7 dias | sem exp (nunca expira) |
| Refresh | 4 dias | 4 dias | 10 anos (cookie) / sem exp (claim) |
APP_ENV=dev, o campo expiration retornado no payload de login vem como 0. Constantes em src/middleware/jwt.go: defaultAccessTokenTTL=20m, defaultRefreshTokenTTL=4d, testModeAccessTokenTTL=7d.
3 · Claims do JWT
Geradas em middleware.GenerateToken e validadas em middleware.ValidadeToken:
| Claim | Origem | Uso no backend |
|---|---|---|
id | UUID do usuário | auth.ID — filtros por usuário, audit log |
username | CPF (normalmente) | Identificação humana, logs |
companie_id | Empresa ativa selecionada | Multi-tenant — filtra TODAS as queries Elastic/Postgres |
id_usuario | ID do vínculo usuário↔empresa | Permissões específicas por empresa |
type | Tipo da empresa (UUID) | Casa com o campo Sectors da rota |
name_complete | Nome completo | Exibição em UI, logs de auditoria |
pwdv | Hash da versão da senha | Invalida o JWT se a senha foi trocada (anti-replay) |
authorized | true fixo | Sentinela |
exp | Unix timestamp | Expiração — ausente em APP_ENV=dev |
passwordSecurity.BuildPasswordVersion(userID, username, passwordHash, JWT_Secret). Em todo request o middleware refaz esse cálculo a partir do hash salvo no Elasticsearch e compara em tempo constante. Se você redefine a senha do usuário, todos os JWTs antigos viram inválidos automaticamente — sem precisar manter blocklist.
4 · Extração do token (ordem)
Função middleware.ExtractToken aceita o token de mais de uma origem:
Authorization: Bearer <jwt>(preferencial — usado pelo mobile em todas as chamadas).Authorization: <jwt>sem prefixo (tolerado, mesmo normalizador).- Cookie
refresh_token— apenas para/v1/auth/refreshe/v1/auth/company/change.
O normalizador remove aspas, ignora "null"/"undefined" literais e faz TrimSpace. Caso típico iOS: garantir que o header não venha como "Bearer null" quando o Keychain está vazio.
5 · Login passo-a-passo (backend)
Implementação em src/modules/auth/services/login.go:
Canonicaliza username
canonicalizeLoginUsername(auth.Username) — remove máscara do CPF, trim, etc.
Busca usuário no Elastic
repository.GetUserElasticWhithPass(...). Se não achar → 401 user_not_found.
Verifica senha (multi-esquema)
Detecta automaticamente o hash: moderno (bcrypt/argon) ou legacy MD5. Se MD5, valida e regrava com hash moderno no Elastic (upgrade transparente).
Resolve empresa
Filtra empresas expiradas (RemoveExpiredUserCompanies). Se nenhuma → 401 no_active_company. Se company_id foi enviado, valida vínculo; senão usa a primeira.
Carrega tipo da empresa
GetCompanieElastic(...) retorna o type (vai para a claim type) — usado depois pelo middleware de autorização para casar com o Sectors da rota.
Gera tokens + cookie
GenerateToken(authDb) emite access + refresh. O refresh vai como Set-Cookie: refresh_token=...; HttpOnly; SameSite=None|Lax; Secure (Secure só em produção). Resposta JSON: { token, expiration }.
6 · Resposta de sucesso
HTTP/1.1 200 OK
Set-Cookie: refresh_token=eyJhbGc...; Path=/; HttpOnly; SameSite=Lax
Content-Type: application/json
{
"token": "eyJhbGc...<access JWT>",
"expiration": 1735689600
}
{ "data": ... }), o login retorna direto o objeto. O wrapper data só aparece a partir das chamadas autenticadas.
7 · Refresh e troca de empresa
Tanto POST /v1/auth/refresh quanto PATCH /v1/auth/company/change reaproveitam o serviço Login com Refresh: true:
- Valida o token recebido (do header ou do cookie
refresh_token). - Reconstrói
AuthLogin{ Username, CompanyId, Refresh: true, ... }a partir das claims. - No refresh, mantém o
companie_idda claim atual. - Na troca de empresa, usa o
company_iddo body — o backend revalida o vínculo no Elastic antes de reemitir. - Por
Refresh: true, o passo 3 (verificação de senha) é pulado — não há credencial nesses fluxos.
8 · Pipeline de autorização (após login)
Toda rota com Autentication: true passa por:
techlog → audit → FormatModel → Authenticator → permissões → RateLimit → Handler
O Authenticator (middleware.Authenticator):
- Extrai o token (ordem acima).
- Parseia com
JWT_Secret(HMAC-SHA256). - Revalida o
pwdvcontra o hash atual no Elastic. - Popula
c.Locals("tokenClaims")com*authModel.Auth. - Em caso de falha:
401 { "erro": "..." }.
Em seguida o middleware de permissão (src/services/auth.go) valida:
- Sectors —
auth.Type∈route.Sectors(ex.:"T"). - Serviços contratados da empresa cobrem
route.Permissions. - Permissões usuário/setor com
OperatorANDouOR. - PermissionRestricted — bloqueio explícito.
9 · Como o mobile usa tudo isso
| Operação | Android | iOS |
|---|---|---|
| Login | LoginFragment → POST /v1/auth/login |
LoginViewController → POST /v1/auth/login |
| Persistir JWT | SharedPreferences "jwt_tokens" · chave "jwt_token" |
Keychain · chave "jwt_token" |
| Persistir company_id | SharedPreferences "selected_company" · chave "company_id" |
UserDefaults · chave "selected_company" |
| Persistir perms_user | SharedPreferences "DATA" · chave "perms_user" |
UserDefaults · chave "perms_user" |
| Header em chamadas | Interceptor OkHttp adiciona Authorization: Bearer <jwt> |
URLRequest.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization") |
| Refresh quando expira | JWTTokenMonitor.shared.ensureValidTokenForAPICall() antes de cada chamada crítica |
Mesma estratégia — verifica expiration salvo e chama /auth/refresh proativamente |
| OTP / Biometria | CodeConfirmationFragment usa generate_access_token + validate_access_token |
Fluxo equivalente em login/options/ · biometria via LAContext |
| Troca de empresa | ConfigurationFragment → PATCH /v1/auth/company/change → reroda analyzePermissionIds() |
ConfigViewController → mesmo endpoint → reescreve perms_user |
company_id precisa estar persistido antes de iniciar o ciclo de refresh. Senão o novo JWT é reemitido apontando para a empresa errada. Tanto o Android quanto o iOS gravam o selected_company de forma síncrona dentro do callback de sucesso do login, antes de navegar para a Home.
10 · API legada (em descomissionamento)
O backend antigo (api.monisat.online) usava três headers fixos por requisição:
Client-Id: <client_id> User-Id: <user_id> Token: <token estático>
Esse modelo persiste apenas em chamadas listadas no doc Migração legacy → v1. Todos os módulos novos do mobile já usam exclusivamente o Bearer JWT da v1.
Erros comuns
| Sintoma | Causa provável | Onde investigar |
|---|---|---|
401 token invalido | JWT expirado ou pwdv divergente (senha trocada) | Forçar refresh; se persistir, exigir novo login. |
401 token ausente | Header não chegou (interceptor desativado, Keychain vazio) | Logar o header antes do envio; ver ExtractToken. |
401 Credenciais incorretas | Login: usuário não encontrado, hash inválido ou empresa expirada | Buscar reason no audit log (campos password_scheme, company_id_requested). |
401 requested_company_not_linked | O company_id enviado não está na lista de empresas ativas do usuário | Recarregar a lista no app; usuário pode ter sido removido da empresa. |
Token aceito mas 403 em rotas | Permissão insuficiente (sector, serviço contratado ou ID de perm) | src/services/auth.go + lista Permissions/Sectors da rota. |
Arquivos-fonte
| Camada | Arquivo |
|---|---|
| Geração / validação JWT | msystem_api_go/src/middleware/jwt.go |
| Login + Refresh + Company Change | msystem_api_go/src/modules/auth/services/{login,refresh,company_change}.go |
| OTP por SMS | msystem_api_go/src/modules/auth/services/{generate,validate}_access_token.go |
| Cookie do refresh | msystem_api_go/src/modules/auth/services/refresh_cookie_policy.go |
| Versão da senha (pwdv) | msystem_api_go/src/security/password/password.go |
| Autorização por rota | msystem_api_go/src/services/auth.go |
| Mobile · Android | Ver Login (Android) |
| Mobile · iOS | Ver Login (iOS) |