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
/loginserver-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.bre emite um cookie de sessãoxadm_session(JWT HS256, TTL 8h). ViewSecurityRule(MicronautSecurityRule, tri-estado) exige o cookie de sessão para qualquer path fora da whitelist; o cookie é lido peloSessionAuthenticationFetcher(libxadm-seguranca). Mecanismo em Segurança.- CSRF via double-submit cookie:
/logingera token aleatório → cookie HttpOnlyxadm_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¶
- Gerar novo segredo:
openssl rand -hex 32. - Substituir env no Coolify e reiniciar o container.
- 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_csrfpor causa doSameSite=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).
isseaudvalidados; 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 paraSameSite=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 doViewModelProcessor.LoginRateLimitFilterTest— unit do rate-limit de/login.ApiBearerSecurityIntegrationTest—@MicronautTestdo Bearer em/api/**.ViewSessionAuthIntegrationTest—@MicronautTestHTTP do gate de sessão das views.GoogleLoginFlowIT—@MicronautTestjornada 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
- Abrir
http://localhost:8080/contratos→ redirect para/login. - Clicar "Entrar com Google" → popup Google → logar com conta
@xadm.com.br. - Confirmar redirect para
/contratos+ email/nome no navbar + botão "Sair". - Clicar "Sair" → cookie expira → próximo acesso a view redireciona de novo.
- Testar
auth/popup-blocked: configurar browser para bloquear pop-ups e tentar login — mensagem deve aparecer "Popup bloqueado pelo browser…".