Pular para conteúdo

Login Google (Firebase) — views do integrador

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06

Mecanismo: o como da auth (beans micronaut-security, intercept-url-map) está em Segurança. Aqui foca o fluxo de login Google/Firebase das views.

Restringe acesso às telas server-rendered do integrador a usuários autenticados via Google (Firebase Auth) com email @xadm.com.br. APIs em /api/** continuam protegidas pelo Bearer estático do micronaut-security (StaticBearerTokenValidator + ApiBearerSecurityRule, ver Segurança).

Visão geral

  • Página /login server-rendered (JTE) usa Firebase Web SDK v10.13.2 via CDN do gstatic. Botão "Entrar com Google" abre popup, obtém Firebase ID token e posta para /login/callback.
  • Backend valida o Firebase ID token (RS256 via JWKS público do Google), confirma o domínio @xadm.com.br e emite um cookie de sessão xadm_session (JWT HS256, TTL 8h).
  • ViewSecurityRule (Micronaut SecurityRule, tri-estado) exige o cookie de sessão para qualquer path fora da whitelist; o cookie é lido pelo SessionAuthenticationFetcher (lib xadm-seguranca). Mecanismo em Segurança.
  • CSRF via double-submit cookie: /login gera token aleatório → cookie HttpOnly xadm_login_csrf (Path=/login) + meta <meta name="xadm-csrf"> no DOM. JS lê o meta e posta no body. Backend compara cookie ↔ body em constant-time.

Diagrama do fluxo

sequenceDiagram
    participant U as Usuário (browser)
    participant App as Integrador (Micronaut)
    participant FB as Firebase JS SDK
    participant G as Google

    U->>App: GET /contratos (sem cookie)
    App-->>U: 302 /login?from=%2Fcontratos
    U->>App: GET /login
    App-->>U: 200 HTML + Set-Cookie xadm_login_csrf + meta xadm-csrf
    U->>FB: click "Entrar com Google" → signInWithPopup
    FB->>G: OAuth popup
    G-->>FB: id_token
    FB-->>U: cred.user.getIdToken()
    U->>App: POST /login/callback {credential, csrfToken, from}
    App->>G: GET JWKS (com cache)
    G-->>App: chaves RSA
    App->>App: verify signature + iss + aud + exp + domínio @xadm.com.br
    App-->>U: 200 {redirect:"/contratos"} + Set-Cookie xadm_session
    U->>App: GET /contratos (com cookie)
    App-->>U: 200 HTML
    U->>App: POST /logout
    App-->>U: 303 /login + Set-Cookie xadm_session (Max-Age=0)

Variáveis de ambiente

Configurar no Coolify (produção) ou no shell de dev:

Env Descrição Default Obrigatório em prod
AUTH_FIREBASE_PROJECT_ID ID do projeto Firebase (xadm-6ab81) vazio ✅
AUTH_FIREBASE_API_KEY Web API key (público; vai no HTML) vazio ✅
AUTH_FIREBASE_AUTH_DOMAIN ex.: xadm-6ab81.firebaseapp.com vazio ✅
AUTH_FIREBASE_APP_ID Web App ID (1:32869493670:web:...) vazio ✅
AUTH_SESSION_SECRET Segredo HS256 ≥ 32 chars vazio ✅
AUTH_SESSION_TTL_SECONDS TTL do cookie de sessão 28800 (8h) —
AUTH_COOKIE_SECURE Força flag Secure no cookie mesmo sem X-Forwarded-Proto true em prod, false em dev —
AUTH_XADM_EMAIL_DOMAIN Domínio aceito xadm.com.br —

Modo dev: se qualquer uma das envs obrigatórias estiver vazia, o ViewSecurityRule entra em bypass total — todas as views ficam públicas. Log WARN único no startup lista as envs faltantes. Para rodar local sem TLS, basta deixar as envs em branco.

Como obter as envs do Firebase

No console do projeto xadm-6ab81: - Firebase Console → Project settings → General → Your apps → Web app → Firebase SDK snippet → Config:

const firebaseConfig = {
    apiKey: "AIza...",
    authDomain: "xadm-6ab81.firebaseapp.com",
    projectId: "xadm-6ab81",
    appId: "1:32869493670:web:..."
};

Os valores são públicos (já estão no HTML do authui, ver @C:\Work\Xadm\integracao\authui\lib\firebase_options.dart). Restrições reais ficam no Firebase Auth (provedores permitidos, domínios autorizados).

Como gerar AUTH_SESSION_SECRET

openssl rand -hex 32
# Saída: 64 chars hex → ≥ 32 bytes, suficiente para HS256

Cada deploy deve ter seu próprio segredo. Não compartilhar entre clientes Coolify.

Como rotacionar AUTH_SESSION_SECRET

  1. Gerar novo segredo: openssl rand -hex 32.
  2. Substituir env no Coolify e reiniciar o container.
  3. Impacto: todas as sessões existentes ficam inválidas — usuários terão que logar de novo. Como TTL é 8h, vale planejar a rotação fora do horário operacional.

Whitelist (paths públicos)

ViewSecurityRule libera sem cookie:

Match exato: /health, /favicon.ico, /login, /logout

Match por prefixo: /health/, /swagger-ui, /swagger-ui/, /swagger/, /login/, /css/, /images/, /api/

/api/** está na whitelist do ViewSecurityRule mas continua protegido pelo Bearer estático (ApiBearerSecurityRule + StaticBearerTokenValidator, que validam Authorization: Bearer <INTEGRADOR_API_TOKEN>).

/debug não está na whitelist — exige sessão em produção. Em dev/test continua acessível porque o ViewSecurityRule está em bypass.

Requisito do reverse proxy

Coolify (ou qualquer proxy TLS-terminator) deve repassar o header X-Forwarded-Proto para que o helper ViewSecurityRule.isHttps(request) detecte HTTPS e emita o cookie com Secure. Caso o proxy não envie o header, o default AUTH_COOKIE_SECURE=true ainda garante que o cookie sai com Secure (fallback seguro). Em dev local com HTTP simples, configurar AUTH_COOKIE_SECURE=false (já feito no application-dev.yml).

Componentes implementados

Infra transversal (br.com.xadm.comum.seguranca.*) vem da lib xadm-seguranca (0018); a policy e os controllers ficam locais em br.com.xadm.comum.

Classe / arquivo Origem Responsabilidade
…comum.seguranca.AuthSettings lib @ConfigurationProperties("auth")
…comum.seguranca.AuthException lib code ∈ {invalid_token, invalid_session, jwks_unavailable, csrf_mismatch, domain_not_allowed, ...}
…comum.seguranca.SessionTokenService lib Emite/valida JWT HS256 (claim iss = auth.issuer)
…comum.seguranca.FirebaseIdTokenValidator lib Valida Firebase ID token (RS256+JWKS) com cache e stale fallback
…comum.seguranca.SessionAuthenticationFetcher lib Lê o cookie, resolve a Authentication das views
…comum.seguranca.XadmLoginController lib GET /login, POST /login/callback, POST /logout
…comum.seguranca.XadmViewModel lib Injeta os globais core no model: appNome, appVersao, clienteXadm, currentUserEmail, currentUserName, csrfToken, authEnabled
br.com.xadm.comum.ViewSecurityRule local SecurityRule tri-estado + whitelist
br.com.xadm.comum.GlobalViewModel local Injeta só os gates de UI do app: showInternalMenus, crudWriteAllowed, debugOperationsAllowed
xadm/login.jte lib Tela de login (Firebase JS SDK), servida pelo jar desde a xadm-seguranca 0.7.1 (0032 do central). É autocontida: sem navbar, porque todo item do menu levaria de volta para ela. O jte/login.jte local foi apagado no bump
jte/kit/layout.jte kit central Navbar com email + form POST /logout

Considerações de segurança

  • Cookie xadm_session é HttpOnly (JS não consegue ler) + SameSite=Lax + Secure (quando HTTPS).
  • CSRF via double-submit: ataque cross-site não consegue gravar cookie xadm_login_csrf por causa do SameSite=Lax, e cookie é HttpOnly (não pode ser lido pelo JS atacante).
  • Comparação CSRF em constant-time (MessageDigest.isEqual).
  • Limite de tamanho de 8KB no Firebase ID token (rejeitado antes da validação).
  • iss e aud validados; clock skew de 60s tolerado.
  • Stale cache do JWKS evita queda total quando Google estiver indisponível.
  • Sessão sem revogação ativa — para invalidar todas, rotacionar AUTH_SESSION_SECRET.

Limitações conhecidas

  • TTL fixo 8h sem sliding session — reavaliar se incomodar.
  • Popup do Firebase JS pode ser bloqueado pelo browser; mensagem explícita exibida.
  • Sem fallback signInWithRedirect (deferido).
  • Sem rate limiting em /login/callback.
  • Sem multi-conta no mesmo browser.
  • Em browsers sem suporte a SameSite=Lax, o cookie degrada para SameSite=None (default antigo). Aceitável para uso interno.

Testes

A mecânica de sessão/Firebase (SessionTokenService, FirebaseIdTokenValidator) é testada no repo da lib xadm-commons. Aqui ficam os testes de policy e fluxo deste app (pacote br.com.xadm.comum):

  • LoginControllerTest — unit dos handlers /login, /login/callback, /logout.
  • SecurityRulesTest — unit das regras/validador (Bearer + view tri-estado).
  • AuthSupportTest — unit da whitelist de paths.
  • GlobalViewModelTest — unit + Micronaut do ViewModelProcessor.
  • LoginRateLimitFilterTest — unit do rate-limit de /login.
  • ApiBearerSecurityIntegrationTest — @MicronautTest do Bearer em /api/**.
  • ViewSessionAuthIntegrationTest — @MicronautTest HTTP do gate de sessão das views.
  • GoogleLoginFlowIT — @MicronautTest jornada E2E (happy path, domínio errado, CSRF mismatch, bypass /api/**, paths públicos, /debug, matriz Secure).
  • views/{LoginPageRenderingIT, NavbarRenderingIT} — rendering das telas de login/navbar.
./gradlew test --tests "*Auth*" --tests "*Login*" --tests "*Security*" --tests "GlobalViewModel*"
./gradlew test --tests "GoogleLoginFlowIT"
./gradlew coverage  # JaCoCo

Smoke manual

export AUTH_FIREBASE_PROJECT_ID=xadm-6ab81
export AUTH_FIREBASE_API_KEY=AIzaSyBeQLwetHsi6XMQ89N1pbHq2hbw_zPvtMw
export AUTH_FIREBASE_AUTH_DOMAIN=xadm-6ab81.firebaseapp.com
export AUTH_FIREBASE_APP_ID=1:32869493670:web:e411e1fd9283c583fabd67
export AUTH_SESSION_SECRET=$(openssl rand -hex 32)
export AUTH_COOKIE_SECURE=false  # dev local sem TLS

./gradlew run -t  # ou java -jar build/libs/app.jar
  1. Abrir http://localhost:8080/contratos → redirect para /login.
  2. Clicar "Entrar com Google" → popup Google → logar com conta @xadm.com.br.
  3. Confirmar redirect para /contratos + email/nome no navbar + botão "Sair".
  4. Clicar "Sair" → cookie expira → próximo acesso a view redireciona de novo.
  5. Testar auth/popup-blocked: configurar browser para bloquear pop-ups e tentar login — mensagem deve aparecer "Popup bloqueado pelo browser…".