@luanpdd/kit-mcp 1.32.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +168 -168
- package/gates/agent-no-recursive-dispatch.md +84 -84
- package/kit/COMANDOS.md +138 -138
- package/kit/COMPATIBILITY.md +70 -70
- package/kit/README.md +76 -76
- package/kit/agents/advisor-researcher.md +109 -109
- package/kit/agents/ai-mutation-tester.md +289 -289
- package/kit/agents/assumptions-analyzer.md +110 -110
- package/kit/agents/audit-log-implementer.md +314 -314
- package/kit/agents/auditor-consistencia-isolamento.md +414 -414
- package/kit/agents/b2b-saas-architect.md +157 -157
- package/kit/agents/burn-rate-forecaster.md +153 -153
- package/kit/agents/cascading-failures-auditor.md +299 -299
- package/kit/agents/codebase-mapper.md +769 -769
- package/kit/agents/crm-pipeline-implementer.md +257 -257
- package/kit/agents/debugger.md +814 -814
- package/kit/agents/designer-ui.md +216 -0
- package/kit/agents/detector-tenant-quente.md +338 -338
- package/kit/agents/evolution-go-integrator.md +201 -201
- package/kit/agents/example-reviewer.md +22 -22
- package/kit/agents/executor.md +565 -565
- package/kit/agents/golden-signals-instrumenter.md +232 -232
- package/kit/agents/incident-investigator.md +238 -238
- package/kit/agents/integration-checker.md +203 -203
- package/kit/agents/invite-flow-implementer.md +190 -190
- package/kit/agents/legacy-characterizer.md +369 -369
- package/kit/agents/lgpd-compliance-auditor.md +296 -296
- package/kit/agents/load-shedding-instrumenter.md +290 -290
- package/kit/agents/multi-tenant-isolation-auditor.md +254 -254
- package/kit/agents/multi-tenant-rls-writer.md +341 -341
- package/kit/agents/nyquist-auditor.md +181 -181
- package/kit/agents/observability-coverage-auditor.md +316 -316
- package/kit/agents/observability-instrumenter.md +191 -191
- package/kit/agents/omm-auditor.md +291 -291
- package/kit/agents/org-onboarding-implementer.md +224 -224
- package/kit/agents/payload-capture-instrumenter.md +274 -274
- package/kit/agents/phase-researcher.md +697 -697
- package/kit/agents/plan-checker.md +275 -275
- package/kit/agents/planner.md +923 -923
- package/kit/agents/postmortem-writer.md +273 -273
- package/kit/agents/project-researcher.md +653 -653
- package/kit/agents/prr-conductor.md +287 -287
- package/kit/agents/refactor-safety-auditor.md +405 -405
- package/kit/agents/release-pipeline-auditor.md +364 -364
- package/kit/agents/research-synthesizer.md +246 -246
- package/kit/agents/roadmapper.md +678 -678
- package/kit/agents/schema-checker.md +160 -160
- package/kit/agents/seam-finder.md +360 -360
- package/kit/agents/shotgun-surgery-detector.md +350 -350
- package/kit/agents/slo-engineer.md +217 -217
- package/kit/agents/storytelling-analyst.md +300 -300
- package/kit/agents/supabase-architect.md +249 -249
- package/kit/agents/supabase-auth-bootstrapper.md +400 -400
- package/kit/agents/supabase-auth-hook-writer.md +418 -418
- package/kit/agents/supabase-branching-architect.md +563 -563
- package/kit/agents/supabase-cicd-pipeline-implementer.md +778 -778
- package/kit/agents/supabase-column-privileges-writer.md +400 -400
- package/kit/agents/supabase-edge-fn-tester.md +288 -288
- package/kit/agents/supabase-edge-fn-writer.md +341 -341
- package/kit/agents/supabase-mfa-implementer.md +439 -439
- package/kit/agents/supabase-migration-writer.md +386 -386
- package/kit/agents/supabase-oauth-server-implementer.md +507 -507
- package/kit/agents/supabase-rbac-implementer.md +393 -393
- package/kit/agents/supabase-realtime-implementer.md +364 -364
- package/kit/agents/supabase-rls-hardener.md +522 -522
- package/kit/agents/supabase-rls-writer.md +324 -324
- package/kit/agents/supabase-roles-implementer.md +356 -356
- package/kit/agents/supabase-social-auth-implementer.md +451 -451
- package/kit/agents/supabase-sso-saml-architect.md +549 -549
- package/kit/agents/supabase-storage-implementer.md +407 -407
- package/kit/agents/super-admin-implementer.md +282 -282
- package/kit/agents/toil-auditor.md +268 -268
- package/kit/agents/ui-auditor.md +438 -438
- package/kit/agents/ui-checker.md +305 -305
- package/kit/agents/ui-researcher.md +356 -356
- package/kit/agents/user-profiler.md +176 -176
- package/kit/agents/validador-evolucao-schema.md +336 -336
- package/kit/agents/verifier.md +729 -729
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-cascading.md +111 -111
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-observabilidade-cobertura-workflow.md +121 -0
- package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
- package/kit/commands/auditar-refactor.md +219 -219
- package/kit/commands/auditar-release.md +109 -109
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/burn-rate-status.md +408 -408
- package/kit/commands/capturar-payloads.md +193 -193
- package/kit/commands/caracterizar.md +212 -212
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/dados-distribuidos.md +188 -188
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/detectar-duplicacao.md +197 -197
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/encontrar-seams.md +136 -136
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/legacy.md +263 -263
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/load-shedding.md +117 -117
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/multi-tenant.md +163 -163
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/refactor-seguro.md +321 -321
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/storytelling.md +179 -179
- package/kit/commands/supabase.md +238 -238
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +13 -3
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/kit-attribution-reminder.cjs +92 -92
- package/kit/hooks/kit-router.cjs +137 -137
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
- package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
- package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
- package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
- package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
- package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
- package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
- package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
- package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
- package/kit/skills/legacy-extract-class/SKILL.md +203 -203
- package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
- package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
- package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
- package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
- package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
- package/kit/skills/member-invite-flow/SKILL.md +305 -305
- package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
- package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
- package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
- package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
- package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
- package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
- package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
- package/kit/skills/supabase-auth-hardening/SKILL.md +674 -674
- package/kit/skills/supabase-auth-hooks/SKILL.md +875 -875
- package/kit/skills/supabase-auth-methods/SKILL.md +486 -486
- package/kit/skills/supabase-auth-sessions/SKILL.md +579 -579
- package/kit/skills/supabase-auth-ssr/SKILL.md +306 -306
- package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
- package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
- package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
- package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
- package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
- package/kit/skills/supabase-edge-functions/SKILL.md +330 -330
- package/kit/skills/supabase-edge-functions-auth/SKILL.md +309 -309
- package/kit/skills/supabase-edge-functions-limits/SKILL.md +302 -302
- package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +279 -279
- package/kit/skills/supabase-edge-functions-testing/SKILL.md +277 -277
- package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +357 -357
- package/kit/skills/supabase-enterprise-sso-saml/SKILL.md +545 -545
- package/kit/skills/supabase-jwt-signing-keys/SKILL.md +399 -399
- package/kit/skills/supabase-mfa/SKILL.md +488 -488
- package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
- package/kit/skills/supabase-migrations/SKILL.md +297 -297
- package/kit/skills/supabase-oauth-server/SKILL.md +537 -537
- package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
- package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
- package/kit/skills/supabase-realtime/SKILL.md +460 -460
- package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
- package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
- package/kit/skills/supabase-social-oauth/SKILL.md +480 -480
- package/kit/skills/supabase-third-party-auth/SKILL.md +450 -450
- package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
- package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
- package/kit/skills/ui-anti-padroes-ia/SKILL.md +261 -0
- package/kit/skills/ui-contexto-produto/SKILL.md +248 -0
- package/kit/skills/ui-cor-estrategia/SKILL.md +213 -0
- package/kit/skills/ui-critica-auditoria/SKILL.md +260 -0
- package/kit/skills/ui-motion-funcional/SKILL.md +264 -0
- package/kit/skills/ui-ritmo-espacial/SKILL.md +259 -0
- package/kit/skills/ui-tipografia/SKILL.md +211 -0
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
- package/kit/workflows/auditar-observabilidade-cobertura.workflow.js +250 -0
- package/package.json +65 -63
- package/src/core/kit.js +333 -216
- package/src/core/reflect.js +247 -247
- package/src/core/registry.js +123 -112
- package/src/core/reverse-sync.js +448 -372
- package/src/core/sync.js +477 -437
- package/src/core/watch.js +121 -121
- package/src/mcp-server/index.js +794 -794
|
@@ -1,875 +1,875 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: supabase-auth-hooks
|
|
3
|
-
description: Use ao implementar Auth Hooks no Supabase — custom access token, send email/SMS, before user created, MFA e password verification hooks.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Supabase — Auth Hooks
|
|
7
|
-
|
|
8
|
-
## Quando usar
|
|
9
|
-
|
|
10
|
-
LLM carrega esta skill quando implementar **Auth Hooks** no Supabase — endpoints que interceptam e modificam o fluxo padrão de autenticação em pontos de execução específicos.
|
|
11
|
-
|
|
12
|
-
Trigger phrases:
|
|
13
|
-
|
|
14
|
-
- "auth hook Supabase", "custom access token hook"
|
|
15
|
-
- "send email hook", "send sms hook"
|
|
16
|
-
- "before user created hook", "MFA verification hook"
|
|
17
|
-
- "password verification hook"
|
|
18
|
-
- "Postgres function hook", "HTTP hook Supabase"
|
|
19
|
-
- `custom_access_token_hook`, `supabase_auth_admin`
|
|
20
|
-
- "como bloquear signup por domínio", "como customizar email de auth"
|
|
21
|
-
- "hook para rate-limit de login", "Standard Webhooks Supabase"
|
|
22
|
-
|
|
23
|
-
## Princípio canônico
|
|
24
|
-
|
|
25
|
-
Auth Hooks são **endpoints síncronos** que o Supabase Auth invoca em pontos específicos do fluxo de autenticação. O hook recebe um payload JSON, pode modificar o comportamento e retorna um JSON de resposta. Erros retornados pelo hook **bloqueiam** a operação de auth correspondente.
|
|
26
|
-
|
|
27
|
-
**6 hooks disponíveis:**
|
|
28
|
-
|
|
29
|
-
| Hook | Disponibilidade | Quando é invocado |
|
|
30
|
-
|------|----------------|-------------------|
|
|
31
|
-
| Before User Created | Free / Pro | Antes de criar novo usuário |
|
|
32
|
-
| Custom Access Token | Free / Pro | Antes de emitir JWT (login + refresh) |
|
|
33
|
-
| Send SMS | Free / Pro | Quando Supabase Auth precisa enviar SMS (OTP, MFA) |
|
|
34
|
-
| Send Email | Free / Pro | Quando Supabase Auth precisa enviar email (confirmação, magic link) |
|
|
35
|
-
| MFA Verification | Teams / Enterprise | Ao verificar código MFA |
|
|
36
|
-
| Password Verification | Teams / Enterprise | Ao verificar senha de login |
|
|
37
|
-
|
|
38
|
-
**2 tipos de hook:**
|
|
39
|
-
|
|
40
|
-
| Tipo | URI | Quando usar |
|
|
41
|
-
|------|-----|-------------|
|
|
42
|
-
| **Postgres function** | `pg-functions://postgres/public/<nome_fn>` | Lógica simples, acesso direto ao DB, sem I/O externo |
|
|
43
|
-
| **HTTP endpoint** | URL HTTPS | Lógica complexa, chamada a APIs externas, Edge Functions |
|
|
44
|
-
|
|
45
|
-
## Modelo de segurança — Postgres Function Hook
|
|
46
|
-
|
|
47
|
-
A função hook roda com o Postgres role `supabase_auth_admin` — precisa de grants explícitos:
|
|
48
|
-
|
|
49
|
-
```sql
|
|
50
|
-
-- OBRIGATÓRIO: grants para supabase_auth_admin
|
|
51
|
-
grant usage on schema public to supabase_auth_admin;
|
|
52
|
-
|
|
53
|
-
grant execute
|
|
54
|
-
on function public.minha_funcao_hook
|
|
55
|
-
to supabase_auth_admin;
|
|
56
|
-
|
|
57
|
-
-- OBRIGATÓRIO: revogar de roles públicos
|
|
58
|
-
revoke execute
|
|
59
|
-
on function public.minha_funcao_hook
|
|
60
|
-
from authenticated, anon, public;
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**Por quê não usar `security definer`:** prefira grants explícitos — `security definer` faz a função rodar com privilégios do owner (geralmente `postgres`) o que é mais amplo do que necessário. Grants são o mínimo necessário e mais auditáveis.
|
|
64
|
-
|
|
65
|
-
**Exceção:** se a função precisar acessar tabelas ou schemas que `supabase_auth_admin` não tem permissão, use `security definer` com `set search_path = ''` para evitar injeção via search_path:
|
|
66
|
-
|
|
67
|
-
```sql
|
|
68
|
-
create or replace function public.custom_access_token_hook(event jsonb)
|
|
69
|
-
returns jsonb
|
|
70
|
-
language plpgsql
|
|
71
|
-
stable
|
|
72
|
-
-- security definer apenas se necessário para acesso a schema restrito
|
|
73
|
-
-- set search_path = '' -- anti-injeção de schema
|
|
74
|
-
as $$
|
|
75
|
-
-- implementação
|
|
76
|
-
$$;
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Modelo de segurança — HTTP Hook
|
|
80
|
-
|
|
81
|
-
Hooks HTTP seguem a especificação **Standard Webhooks** para autenticidade das requisições:
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
// Headers enviados pelo Supabase Auth em cada chamada HTTP ao hook
|
|
85
|
-
// webhook-id: <uuid único por chamada>
|
|
86
|
-
// webhook-timestamp: <unix timestamp em segundos>
|
|
87
|
-
// webhook-signature: v1=<assinatura HMAC-SHA256 base64>
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Verificar assinatura com a lib `standardwebhooks`:**
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
// deno / Edge Function
|
|
94
|
-
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
|
95
|
-
|
|
96
|
-
const secret = Deno.env.get('SEND_EMAIL_HOOK_SECRET')!
|
|
97
|
-
// formato do secret configurado no Supabase: v1,whsec_<base64-secret>
|
|
98
|
-
|
|
99
|
-
Deno.serve(async (req) => {
|
|
100
|
-
const payload = await req.text()
|
|
101
|
-
const headers = Object.fromEntries(req.headers)
|
|
102
|
-
|
|
103
|
-
const wh = new Webhook(secret)
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
// lança erro se assinatura inválida
|
|
107
|
-
const event = wh.verify(payload, headers)
|
|
108
|
-
// processar evento verificado
|
|
109
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
110
|
-
headers: { 'Content-Type': 'application/json' },
|
|
111
|
-
})
|
|
112
|
-
} catch (err) {
|
|
113
|
-
// assinatura inválida → rejeitar
|
|
114
|
-
return new Response(
|
|
115
|
-
JSON.stringify({ error: { http_code: 401, message: 'Assinatura inválida' } }),
|
|
116
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
**Formato do secret:** Supabase gera um secret no formato `v1,whsec_<base64>` — usar exatamente este formato ao instanciar `new Webhook(secret)`.
|
|
123
|
-
|
|
124
|
-
## Configuração — Dashboard e config.toml
|
|
125
|
-
|
|
126
|
-
**Via Dashboard (produção):**
|
|
127
|
-
|
|
128
|
-
1. Acessar `Authentication > Hooks` no Dashboard
|
|
129
|
-
2. Selecionar o tipo de hook (ex: "Custom Access Token")
|
|
130
|
-
3. Escolher tipo: Postgres function ou HTTP
|
|
131
|
-
4. Para Postgres: selecionar a função no dropdown
|
|
132
|
-
5. Para HTTP: digitar a URL + configurar o secret
|
|
133
|
-
6. Salvar
|
|
134
|
-
|
|
135
|
-
**Via `config.toml` (desenvolvimento local):**
|
|
136
|
-
|
|
137
|
-
```toml
|
|
138
|
-
# supabase/config.toml
|
|
139
|
-
|
|
140
|
-
# Custom Access Token Hook (Postgres function)
|
|
141
|
-
[auth.hook.custom_access_token]
|
|
142
|
-
enabled = true
|
|
143
|
-
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
|
144
|
-
|
|
145
|
-
# Custom Access Token Hook (HTTP — Edge Function local)
|
|
146
|
-
[auth.hook.custom_access_token]
|
|
147
|
-
enabled = true
|
|
148
|
-
uri = "http://localhost:54321/functions/v1/custom-access-token-hook"
|
|
149
|
-
secrets = "v1,whsec_dGVzdHNlY3JldA=="
|
|
150
|
-
|
|
151
|
-
# Send Email Hook
|
|
152
|
-
[auth.hook.send_email]
|
|
153
|
-
enabled = true
|
|
154
|
-
uri = "http://localhost:54321/functions/v1/send-email-hook"
|
|
155
|
-
secrets = "v1,whsec_dGVzdHNlY3JldA=="
|
|
156
|
-
|
|
157
|
-
# Send SMS Hook
|
|
158
|
-
[auth.hook.send_sms]
|
|
159
|
-
enabled = true
|
|
160
|
-
uri = "http://localhost:54321/functions/v1/send-sms-hook"
|
|
161
|
-
secrets = "v1,whsec_dGVzdHNlY3JldA=="
|
|
162
|
-
|
|
163
|
-
# Before User Created Hook
|
|
164
|
-
[auth.hook.before_user_created]
|
|
165
|
-
enabled = true
|
|
166
|
-
uri = "pg-functions://postgres/public/before_user_created_hook"
|
|
167
|
-
|
|
168
|
-
# MFA Verification Hook (apenas Teams/Enterprise)
|
|
169
|
-
[auth.hook.mfa_verification_attempt]
|
|
170
|
-
enabled = true
|
|
171
|
-
uri = "pg-functions://postgres/public/mfa_verification_hook"
|
|
172
|
-
|
|
173
|
-
# Password Verification Hook (apenas Teams/Enterprise)
|
|
174
|
-
[auth.hook.password_verification_attempt]
|
|
175
|
-
enabled = true
|
|
176
|
-
uri = "pg-functions://postgres/public/password_verification_hook"
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## Error Handling — Status Codes e Retry
|
|
180
|
-
|
|
181
|
-
**Formato de erro retornado pelo hook:**
|
|
182
|
-
|
|
183
|
-
```json
|
|
184
|
-
{
|
|
185
|
-
"error": {
|
|
186
|
-
"http_code": 403,
|
|
187
|
-
"message": "Domínio de email não permitido"
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
**Comportamento por status code:**
|
|
193
|
-
|
|
194
|
-
| Status code retornado pelo hook | Comportamento do Supabase Auth |
|
|
195
|
-
|---------------------------------|-------------------------------|
|
|
196
|
-
| `200` / `202` / `204` | Sucesso — continua o fluxo de auth |
|
|
197
|
-
| `400` | Falha permanente — converte em `500` para o cliente (não retry) |
|
|
198
|
-
| `403` | Falha permanente — converte em `500` para o cliente (não retry) |
|
|
199
|
-
| `429` | Rate-limit — **retry** automático (usar header `Retry-After`) |
|
|
200
|
-
| `503` | Serviço indisponível — **retry** automático |
|
|
201
|
-
|
|
202
|
-
**Respostas retry-able:**
|
|
203
|
-
|
|
204
|
-
```ts
|
|
205
|
-
// Hook que sinaliza rate-limit com retry
|
|
206
|
-
return new Response(
|
|
207
|
-
JSON.stringify({ error: { http_code: 429, message: 'Muitas tentativas' } }),
|
|
208
|
-
{
|
|
209
|
-
status: 429,
|
|
210
|
-
headers: {
|
|
211
|
-
'Content-Type': 'application/json',
|
|
212
|
-
'Retry-After': '60', // Supabase Auth vai retentar após 60 segundos
|
|
213
|
-
},
|
|
214
|
-
}
|
|
215
|
-
)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
**Regra:** TODAS as respostas de hook (sucesso ou erro) devem ter header `Content-Type: application/json`.
|
|
219
|
-
|
|
220
|
-
## Hook 1 — Custom Access Token
|
|
221
|
-
|
|
222
|
-
Invocado antes de cada emissão de JWT (login inicial + refresh). Permite injetar claims customizados.
|
|
223
|
-
|
|
224
|
-
**Input:**
|
|
225
|
-
```json
|
|
226
|
-
{
|
|
227
|
-
"user_id": "uuid-do-usuario",
|
|
228
|
-
"claims": {
|
|
229
|
-
"aal": "aal1",
|
|
230
|
-
"sub": "uuid-do-usuario",
|
|
231
|
-
"email": "usuario@empresa.com",
|
|
232
|
-
"role": "authenticated",
|
|
233
|
-
"exp": 1704067200,
|
|
234
|
-
"iat": 1704063600,
|
|
235
|
-
"iss": "https://proj.supabase.co/auth/v1",
|
|
236
|
-
"session_id": "uuid-da-sessao",
|
|
237
|
-
"amr": [{"method": "password", "timestamp": 1704063600}]
|
|
238
|
-
},
|
|
239
|
-
"authentication_method": "password"
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
**Output (modificar claims):**
|
|
244
|
-
```json
|
|
245
|
-
{
|
|
246
|
-
"claims": {
|
|
247
|
-
"aal": "aal1",
|
|
248
|
-
"sub": "uuid-do-usuario",
|
|
249
|
-
"email": "usuario@empresa.com",
|
|
250
|
-
"role": "authenticated",
|
|
251
|
-
"exp": 1704067200,
|
|
252
|
-
"iat": 1704063600,
|
|
253
|
-
"iss": "https://proj.supabase.co/auth/v1",
|
|
254
|
-
"session_id": "uuid-da-sessao",
|
|
255
|
-
"amr": [{"method": "password", "timestamp": 1704063600}],
|
|
256
|
-
"user_role": "admin",
|
|
257
|
-
"org_id": "uuid-da-org"
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
**Implementação Postgres canônica (ver skill `supabase-custom-claims-rbac` para pattern completo):**
|
|
263
|
-
|
|
264
|
-
```sql
|
|
265
|
-
create or replace function public.custom_access_token_hook(event jsonb)
|
|
266
|
-
returns jsonb
|
|
267
|
-
language plpgsql
|
|
268
|
-
stable
|
|
269
|
-
as $$
|
|
270
|
-
declare
|
|
271
|
-
claims jsonb;
|
|
272
|
-
user_role public.app_role;
|
|
273
|
-
begin
|
|
274
|
-
select role into user_role
|
|
275
|
-
from public.user_roles
|
|
276
|
-
where user_id = (event->>'user_id')::uuid;
|
|
277
|
-
|
|
278
|
-
claims := event->'claims';
|
|
279
|
-
|
|
280
|
-
claims := jsonb_set(
|
|
281
|
-
claims,
|
|
282
|
-
'{user_role}',
|
|
283
|
-
case when user_role is not null then to_jsonb(user_role) else 'null'::jsonb end
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
return jsonb_set(event, '{claims}', claims);
|
|
287
|
-
end;
|
|
288
|
-
$$;
|
|
289
|
-
|
|
290
|
-
-- grants obrigatórios
|
|
291
|
-
grant usage on schema public to supabase_auth_admin;
|
|
292
|
-
grant execute on function public.custom_access_token_hook to supabase_auth_admin;
|
|
293
|
-
revoke execute on function public.custom_access_token_hook from authenticated, anon, public;
|
|
294
|
-
grant all on table public.user_roles to supabase_auth_admin;
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**Usos canônicos do Custom Access Token hook:**
|
|
298
|
-
|
|
299
|
-
- Reduzir tamanho do JWT (omitir claims não necessários)
|
|
300
|
-
- Adicionar claim `user_role`, `org_id`, `plan` ao JWT
|
|
301
|
-
- Restringir login por tipo de autenticação (`authentication_method`)
|
|
302
|
-
- Adicionar claim `is_admin` baseado em SSO provider
|
|
303
|
-
|
|
304
|
-
```sql
|
|
305
|
-
-- restringir acesso por método de autenticação (ex: SSO apenas para admins)
|
|
306
|
-
create or replace function public.custom_access_token_hook(event jsonb)
|
|
307
|
-
returns jsonb
|
|
308
|
-
language plpgsql
|
|
309
|
-
stable
|
|
310
|
-
as $$
|
|
311
|
-
declare
|
|
312
|
-
claims jsonb;
|
|
313
|
-
auth_method text;
|
|
314
|
-
begin
|
|
315
|
-
auth_method := event->>'authentication_method';
|
|
316
|
-
claims := event->'claims';
|
|
317
|
-
|
|
318
|
-
-- adicionar claim que indica se login foi via SSO
|
|
319
|
-
claims := jsonb_set(claims, '{via_sso}', to_jsonb(auth_method = 'sso/saml'));
|
|
320
|
-
|
|
321
|
-
return jsonb_set(event, '{claims}', claims);
|
|
322
|
-
end;
|
|
323
|
-
$$;
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## Hook 2 — Before User Created
|
|
327
|
-
|
|
328
|
-
Invocado antes de criar um novo usuário. Retornar `error` **rejeita** o signup.
|
|
329
|
-
|
|
330
|
-
**Input:**
|
|
331
|
-
```json
|
|
332
|
-
{
|
|
333
|
-
"user": {
|
|
334
|
-
"id": "uuid-gerado",
|
|
335
|
-
"email": "novo@dominio.com",
|
|
336
|
-
"phone": "",
|
|
337
|
-
"app_metadata": {},
|
|
338
|
-
"user_metadata": {"nome": "João"},
|
|
339
|
-
"identities": [],
|
|
340
|
-
"created_at": "2026-05-19T00:00:00Z",
|
|
341
|
-
"updated_at": "2026-05-19T00:00:00Z"
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
**Bloquear domínios descartáveis:**
|
|
347
|
-
|
|
348
|
-
```sql
|
|
349
|
-
create or replace function public.before_user_created_hook(event jsonb)
|
|
350
|
-
returns jsonb
|
|
351
|
-
language plpgsql
|
|
352
|
-
stable
|
|
353
|
-
as $$
|
|
354
|
-
declare
|
|
355
|
-
email_address text;
|
|
356
|
-
email_domain text;
|
|
357
|
-
dominio_bloqueado bool;
|
|
358
|
-
begin
|
|
359
|
-
email_address := event->'user'->>'email';
|
|
360
|
-
email_domain := split_part(email_address, '@', 2);
|
|
361
|
-
|
|
362
|
-
-- checar em tabela de domínios bloqueados
|
|
363
|
-
select exists(
|
|
364
|
-
select 1 from public.blocked_email_domains
|
|
365
|
-
where domain = lower(email_domain)
|
|
366
|
-
) into dominio_bloqueado;
|
|
367
|
-
|
|
368
|
-
if dominio_bloqueado then
|
|
369
|
-
return jsonb_build_object(
|
|
370
|
-
'error', jsonb_build_object(
|
|
371
|
-
'http_code', 422,
|
|
372
|
-
'message', 'Domínio de email não permitido. Use um email corporativo.'
|
|
373
|
-
)
|
|
374
|
-
);
|
|
375
|
-
end if;
|
|
376
|
-
|
|
377
|
-
-- retornar event sem modificações (signup permitido)
|
|
378
|
-
return event;
|
|
379
|
-
end;
|
|
380
|
-
$$;
|
|
381
|
-
|
|
382
|
-
-- tabela de domínios bloqueados
|
|
383
|
-
create table public.blocked_email_domains (
|
|
384
|
-
domain text primary key,
|
|
385
|
-
motivo text,
|
|
386
|
-
criado_em timestamptz default now()
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
-- seed inicial de domínios descartáveis comuns
|
|
390
|
-
insert into public.blocked_email_domains (domain, motivo) values
|
|
391
|
-
('mailinator.com', 'Email descartável'),
|
|
392
|
-
('guerrillamail.com', 'Email descartável'),
|
|
393
|
-
('tempmail.com', 'Email descartável'),
|
|
394
|
-
('throwam.com', 'Email descartável'),
|
|
395
|
-
('yopmail.com', 'Email descartável');
|
|
396
|
-
|
|
397
|
-
-- grants
|
|
398
|
-
grant usage on schema public to supabase_auth_admin;
|
|
399
|
-
grant execute on function public.before_user_created_hook to supabase_auth_admin;
|
|
400
|
-
revoke execute on function public.before_user_created_hook from authenticated, anon, public;
|
|
401
|
-
grant select on table public.blocked_email_domains to supabase_auth_admin;
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
**Bloquear por provider — rejeitar signup via email (apenas SSO permitido):**
|
|
405
|
-
|
|
406
|
-
```sql
|
|
407
|
-
-- em aplicação B2B que só aceita login via SSO corporativo
|
|
408
|
-
create or replace function public.before_user_created_hook(event jsonb)
|
|
409
|
-
returns jsonb
|
|
410
|
-
language plpgsql
|
|
411
|
-
stable
|
|
412
|
-
as $$
|
|
413
|
-
declare
|
|
414
|
-
identities jsonb;
|
|
415
|
-
tem_sso bool;
|
|
416
|
-
begin
|
|
417
|
-
identities := event->'user'->'identities';
|
|
418
|
-
|
|
419
|
-
-- checar se tem identidade SSO (provider começa com 'sso:')
|
|
420
|
-
select exists(
|
|
421
|
-
select 1 from jsonb_array_elements(identities) as i
|
|
422
|
-
where (i->>'provider') like 'sso:%'
|
|
423
|
-
) into tem_sso;
|
|
424
|
-
|
|
425
|
-
if not tem_sso then
|
|
426
|
-
return jsonb_build_object(
|
|
427
|
-
'error', jsonb_build_object(
|
|
428
|
-
'http_code', 403,
|
|
429
|
-
'message', 'Apenas login via SSO corporativo é permitido.'
|
|
430
|
-
)
|
|
431
|
-
);
|
|
432
|
-
end if;
|
|
433
|
-
|
|
434
|
-
return event;
|
|
435
|
-
end;
|
|
436
|
-
$$;
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
## Hook 3 — Send SMS
|
|
440
|
-
|
|
441
|
-
Substitui o envio de SMS do Supabase por provider customizado (Twilio, AWS SNS, Vonage).
|
|
442
|
-
|
|
443
|
-
**Input:**
|
|
444
|
-
```json
|
|
445
|
-
{
|
|
446
|
-
"user": { "id": "uuid", "phone": "+5511999999999" },
|
|
447
|
-
"sms": { "otp": "123456" }
|
|
448
|
-
}
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
**Edge Function com Twilio:**
|
|
452
|
-
|
|
453
|
-
```ts
|
|
454
|
-
// supabase/functions/send-sms-hook/index.ts
|
|
455
|
-
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
|
456
|
-
|
|
457
|
-
const secret = Deno.env.get('SEND_SMS_HOOK_SECRET')!
|
|
458
|
-
const TWILIO_ACCOUNT_SID = Deno.env.get('TWILIO_ACCOUNT_SID')!
|
|
459
|
-
const TWILIO_AUTH_TOKEN = Deno.env.get('TWILIO_AUTH_TOKEN')!
|
|
460
|
-
const TWILIO_FROM = Deno.env.get('TWILIO_FROM_NUMBER')!
|
|
461
|
-
|
|
462
|
-
Deno.serve(async (req) => {
|
|
463
|
-
const payload = await req.text()
|
|
464
|
-
const headers = Object.fromEntries(req.headers)
|
|
465
|
-
|
|
466
|
-
// verificar assinatura Standard Webhooks
|
|
467
|
-
const wh = new Webhook(secret)
|
|
468
|
-
let event: { user: { phone: string }; sms: { otp: string } }
|
|
469
|
-
try {
|
|
470
|
-
event = wh.verify(payload, headers) as typeof event
|
|
471
|
-
} catch {
|
|
472
|
-
return new Response(
|
|
473
|
-
JSON.stringify({ error: { http_code: 401, message: 'Assinatura inválida' } }),
|
|
474
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
475
|
-
)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const { phone } = event.user
|
|
479
|
-
const { otp } = event.sms
|
|
480
|
-
|
|
481
|
-
// enviar via Twilio
|
|
482
|
-
const resp = await fetch(
|
|
483
|
-
`https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json`,
|
|
484
|
-
{
|
|
485
|
-
method: 'POST',
|
|
486
|
-
headers: {
|
|
487
|
-
Authorization: `Basic ${btoa(`${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}`)}`,
|
|
488
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
489
|
-
},
|
|
490
|
-
body: new URLSearchParams({
|
|
491
|
-
To: phone,
|
|
492
|
-
From: TWILIO_FROM,
|
|
493
|
-
Body: `Seu código de verificação: ${otp}. Válido por 5 minutos.`,
|
|
494
|
-
}),
|
|
495
|
-
}
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
if (!resp.ok) {
|
|
499
|
-
const err = await resp.json()
|
|
500
|
-
console.error('Erro Twilio:', err)
|
|
501
|
-
// 503 → Supabase vai retentar
|
|
502
|
-
return new Response(
|
|
503
|
-
JSON.stringify({ error: { http_code: 503, message: 'Falha no envio de SMS' } }),
|
|
504
|
-
{ status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': '30' } }
|
|
505
|
-
)
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return new Response(JSON.stringify({}), {
|
|
509
|
-
headers: { 'Content-Type': 'application/json' },
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
## Hook 4 — Send Email
|
|
515
|
-
|
|
516
|
-
Substitui emails transacionais do Supabase por provider customizado (Resend, SendGrid, AWS SES).
|
|
517
|
-
|
|
518
|
-
**Input (exemplo para magic link):**
|
|
519
|
-
```json
|
|
520
|
-
{
|
|
521
|
-
"user": { "id": "uuid", "email": "usuario@empresa.com" },
|
|
522
|
-
"email_data": {
|
|
523
|
-
"token": "token-opaque",
|
|
524
|
-
"token_hash": "hash-do-token",
|
|
525
|
-
"redirect_to": "https://app.com/auth/callback",
|
|
526
|
-
"email_action_type": "magic_link",
|
|
527
|
-
"site_url": "https://app.com",
|
|
528
|
-
"token_new": "",
|
|
529
|
-
"token_hash_new": ""
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
**Tipos de `email_action_type`:** `signup`, `magic_link`, `recovery`, `invite`, `email_change_new`, `email_change_current`.
|
|
535
|
-
|
|
536
|
-
**Edge Function com Resend:**
|
|
537
|
-
|
|
538
|
-
```ts
|
|
539
|
-
// supabase/functions/send-email-hook/index.ts
|
|
540
|
-
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
|
541
|
-
|
|
542
|
-
const secret = Deno.env.get('SEND_EMAIL_HOOK_SECRET')!
|
|
543
|
-
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!
|
|
544
|
-
|
|
545
|
-
type EmailActionType = 'signup' | 'magic_link' | 'recovery' | 'invite' |
|
|
546
|
-
'email_change_new' | 'email_change_current'
|
|
547
|
-
|
|
548
|
-
function montarConteudo(tipo: EmailActionType, emailData: any): { subject: string; html: string } {
|
|
549
|
-
const link = `${emailData.site_url}/auth/confirm?token_hash=${emailData.token_hash}&type=${tipo}&next=${emailData.redirect_to}`
|
|
550
|
-
|
|
551
|
-
switch (tipo) {
|
|
552
|
-
case 'magic_link':
|
|
553
|
-
return {
|
|
554
|
-
subject: 'Seu link de acesso',
|
|
555
|
-
html: `<p>Clique <a href="${link}">aqui</a> para acessar. Válido por 1 hora.</p>`,
|
|
556
|
-
}
|
|
557
|
-
case 'signup':
|
|
558
|
-
return {
|
|
559
|
-
subject: 'Confirme seu email',
|
|
560
|
-
html: `<p>Bem-vindo! <a href="${link}">Confirme seu email</a> para começar.</p>`,
|
|
561
|
-
}
|
|
562
|
-
case 'recovery':
|
|
563
|
-
return {
|
|
564
|
-
subject: 'Recuperação de senha',
|
|
565
|
-
html: `<p><a href="${link}">Redefina sua senha</a>. Válido por 1 hora.</p>`,
|
|
566
|
-
}
|
|
567
|
-
default:
|
|
568
|
-
return {
|
|
569
|
-
subject: 'Ação necessária',
|
|
570
|
-
html: `<p><a href="${link}">Clique aqui</a> para continuar.</p>`,
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
Deno.serve(async (req) => {
|
|
576
|
-
const payload = await req.text()
|
|
577
|
-
const headers = Object.fromEntries(req.headers)
|
|
578
|
-
|
|
579
|
-
const wh = new Webhook(secret)
|
|
580
|
-
let event: { user: { email: string }; email_data: any }
|
|
581
|
-
try {
|
|
582
|
-
event = wh.verify(payload, headers) as typeof event
|
|
583
|
-
} catch {
|
|
584
|
-
return new Response(
|
|
585
|
-
JSON.stringify({ error: { http_code: 401, message: 'Assinatura inválida' } }),
|
|
586
|
-
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
587
|
-
)
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const { email } = event.user
|
|
591
|
-
const { email_action_type, ...emailData } = event.email_data
|
|
592
|
-
const { subject, html } = montarConteudo(email_action_type, { email_action_type, ...emailData })
|
|
593
|
-
|
|
594
|
-
const resp = await fetch('https://api.resend.com/emails', {
|
|
595
|
-
method: 'POST',
|
|
596
|
-
headers: {
|
|
597
|
-
Authorization: `Bearer ${RESEND_API_KEY}`,
|
|
598
|
-
'Content-Type': 'application/json',
|
|
599
|
-
},
|
|
600
|
-
body: JSON.stringify({
|
|
601
|
-
from: 'no-reply@empresa.com',
|
|
602
|
-
to: [email],
|
|
603
|
-
subject,
|
|
604
|
-
html,
|
|
605
|
-
}),
|
|
606
|
-
})
|
|
607
|
-
|
|
608
|
-
if (!resp.ok) {
|
|
609
|
-
return new Response(
|
|
610
|
-
JSON.stringify({ error: { http_code: 503, message: 'Falha no envio de email' } }),
|
|
611
|
-
{ status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': '60' } }
|
|
612
|
-
)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return new Response(JSON.stringify({}), {
|
|
616
|
-
headers: { 'Content-Type': 'application/json' },
|
|
617
|
-
})
|
|
618
|
-
})
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
## Hook 5 — MFA Verification (Teams/Enterprise)
|
|
622
|
-
|
|
623
|
-
Rate-limit customizado para tentativas de verificação MFA. Roda a cada `mfa.verify()`.
|
|
624
|
-
|
|
625
|
-
**Input:**
|
|
626
|
-
```json
|
|
627
|
-
{
|
|
628
|
-
"factor_id": "uuid-do-fator",
|
|
629
|
-
"factor_type": "totp",
|
|
630
|
-
"user_id": "uuid-do-usuario",
|
|
631
|
-
"valid": true
|
|
632
|
-
}
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
**Output:**
|
|
636
|
-
```json
|
|
637
|
-
{
|
|
638
|
-
"decision": "continue",
|
|
639
|
-
"message": ""
|
|
640
|
-
}
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
```sql
|
|
644
|
-
-- rate-limit: máximo 5 tentativas erradas em 15 minutos
|
|
645
|
-
create table public.mfa_attempt_log (
|
|
646
|
-
user_id uuid not null references auth.users(id) on delete cascade,
|
|
647
|
-
factor_id uuid not null,
|
|
648
|
-
tentativa timestamptz not null default now(),
|
|
649
|
-
sucesso boolean not null
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
create index on public.mfa_attempt_log (user_id, tentativa);
|
|
653
|
-
|
|
654
|
-
create or replace function public.mfa_verification_hook(event jsonb)
|
|
655
|
-
returns jsonb
|
|
656
|
-
language plpgsql
|
|
657
|
-
as $$
|
|
658
|
-
declare
|
|
659
|
-
user_id_val uuid;
|
|
660
|
-
factor_id_val uuid;
|
|
661
|
-
valida bool;
|
|
662
|
-
tentativas_erradas int;
|
|
663
|
-
begin
|
|
664
|
-
user_id_val := (event->>'user_id')::uuid;
|
|
665
|
-
factor_id_val := (event->>'factor_id')::uuid;
|
|
666
|
-
valida := (event->>'valid')::boolean;
|
|
667
|
-
|
|
668
|
-
-- checar tentativas erradas nos últimos 15 minutos
|
|
669
|
-
select count(*) into tentativas_erradas
|
|
670
|
-
from public.mfa_attempt_log
|
|
671
|
-
where user_id = user_id_val
|
|
672
|
-
and factor_id = factor_id_val
|
|
673
|
-
and sucesso = false
|
|
674
|
-
and tentativa > now() - interval '15 minutes';
|
|
675
|
-
|
|
676
|
-
-- logar tentativa atual
|
|
677
|
-
insert into public.mfa_attempt_log (user_id, factor_id, sucesso)
|
|
678
|
-
values (user_id_val, factor_id_val, valida);
|
|
679
|
-
|
|
680
|
-
if tentativas_erradas >= 5 then
|
|
681
|
-
return jsonb_build_object(
|
|
682
|
-
'decision', 'reject',
|
|
683
|
-
'message', 'Muitas tentativas incorretas. Aguarde 15 minutos.'
|
|
684
|
-
);
|
|
685
|
-
end if;
|
|
686
|
-
|
|
687
|
-
return jsonb_build_object('decision', 'continue', 'message', '');
|
|
688
|
-
end;
|
|
689
|
-
$$;
|
|
690
|
-
|
|
691
|
-
grant usage on schema public to supabase_auth_admin;
|
|
692
|
-
grant execute on function public.mfa_verification_hook to supabase_auth_admin;
|
|
693
|
-
revoke execute on function public.mfa_verification_hook from authenticated, anon, public;
|
|
694
|
-
grant all on table public.mfa_attempt_log to supabase_auth_admin;
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
## Hook 6 — Password Verification (Teams/Enterprise)
|
|
698
|
-
|
|
699
|
-
Bloquear login após N tentativas erradas de senha.
|
|
700
|
-
|
|
701
|
-
**Input:**
|
|
702
|
-
```json
|
|
703
|
-
{
|
|
704
|
-
"user_id": "uuid-do-usuario",
|
|
705
|
-
"valid": false
|
|
706
|
-
}
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
```sql
|
|
710
|
-
create or replace function public.password_verification_hook(event jsonb)
|
|
711
|
-
returns jsonb
|
|
712
|
-
language plpgsql
|
|
713
|
-
as $$
|
|
714
|
-
declare
|
|
715
|
-
uid uuid;
|
|
716
|
-
valida bool;
|
|
717
|
-
erros_recentes int;
|
|
718
|
-
begin
|
|
719
|
-
uid := (event->>'user_id')::uuid;
|
|
720
|
-
valida := (event->>'valid')::boolean;
|
|
721
|
-
|
|
722
|
-
-- logar tentativa
|
|
723
|
-
insert into public.login_attempt_log (user_id, sucesso)
|
|
724
|
-
values (uid, valida);
|
|
725
|
-
|
|
726
|
-
-- contar erros nos últimos 30 minutos (apenas se tentativa inválida)
|
|
727
|
-
if not valida then
|
|
728
|
-
select count(*) into erros_recentes
|
|
729
|
-
from public.login_attempt_log
|
|
730
|
-
where user_id = uid
|
|
731
|
-
and sucesso = false
|
|
732
|
-
and tentativa > now() - interval '30 minutes';
|
|
733
|
-
|
|
734
|
-
if erros_recentes >= 10 then
|
|
735
|
-
return jsonb_build_object(
|
|
736
|
-
'decision', 'reject',
|
|
737
|
-
'message', 'Conta temporariamente bloqueada. Tente novamente em 30 minutos.'
|
|
738
|
-
);
|
|
739
|
-
end if;
|
|
740
|
-
end if;
|
|
741
|
-
|
|
742
|
-
return jsonb_build_object('decision', 'continue', 'message', '');
|
|
743
|
-
end;
|
|
744
|
-
$$;
|
|
745
|
-
|
|
746
|
-
create table public.login_attempt_log (
|
|
747
|
-
id bigint generated by default as identity primary key,
|
|
748
|
-
user_id uuid not null references auth.users(id) on delete cascade,
|
|
749
|
-
sucesso boolean not null,
|
|
750
|
-
tentativa timestamptz default now()
|
|
751
|
-
);
|
|
752
|
-
create index on public.login_attempt_log (user_id, tentativa);
|
|
753
|
-
|
|
754
|
-
grant usage on schema public to supabase_auth_admin;
|
|
755
|
-
grant execute on function public.password_verification_hook to supabase_auth_admin;
|
|
756
|
-
revoke execute on function public.password_verification_hook from authenticated, anon, public;
|
|
757
|
-
grant all on table public.login_attempt_log to supabase_auth_admin;
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
## Regras absolutas
|
|
761
|
-
|
|
762
|
-
1. **SEMPRE `grant execute` ao `supabase_auth_admin`** — sem este grant, hook Postgres falha silenciosamente; JWT é emitido sem modificações.
|
|
763
|
-
2. **SEMPRE `revoke execute` de `authenticated`, `anon`, `public`** — sem isso, qualquer cliente pode invocar a função diretamente.
|
|
764
|
-
3. **Hooks HTTP devem verificar assinatura Standard Webhooks** — usar `standardwebhooks` com o secret configurado. Nunca processar payload sem verificação.
|
|
765
|
-
4. **Evitar `security definer` desnecessariamente** — prefira grants explícitos; `security definer` amplia o acesso mais do que necessário.
|
|
766
|
-
5. **TODAS as respostas precisam `Content-Type: application/json`** — Supabase Auth rejeita respostas sem este header.
|
|
767
|
-
6. **Hook deve ser rápido (< 10ms idealmente)** — roda a cada login e refresh; query lenta degrada latência de auth de toda a aplicação.
|
|
768
|
-
7. **Hooks MFA/Password Verification são Teams/Enterprise only** — verificar plano antes de implementar.
|
|
769
|
-
|
|
770
|
-
## Anti-patterns
|
|
771
|
-
|
|
772
|
-
### Anti-pattern 1: Esquecer grants ao supabase_auth_admin
|
|
773
|
-
|
|
774
|
-
**Errado:**
|
|
775
|
-
```sql
|
|
776
|
-
create or replace function public.custom_access_token_hook(event jsonb)
|
|
777
|
-
returns jsonb language plpgsql stable as $$
|
|
778
|
-
-- implementação aqui
|
|
779
|
-
$$;
|
|
780
|
-
-- sem GRANT EXECUTE TO supabase_auth_admin
|
|
781
|
-
-- sem REVOKE de anon/authenticated
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
**Por quê:** Auth hook falha silenciosamente. O JWT é gerado **sem** as modificações do hook — claims customizados não aparecem. Difícil de debugar pois não há erro explícito.
|
|
785
|
-
|
|
786
|
-
**Certo:**
|
|
787
|
-
```sql
|
|
788
|
-
-- SEMPRE após criar a função
|
|
789
|
-
grant usage on schema public to supabase_auth_admin;
|
|
790
|
-
grant execute on function public.custom_access_token_hook to supabase_auth_admin;
|
|
791
|
-
revoke execute on function public.custom_access_token_hook from authenticated, anon, public;
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
### Anti-pattern 2: Não verificar assinatura do webhook HTTP
|
|
795
|
-
|
|
796
|
-
**Errado:**
|
|
797
|
-
```ts
|
|
798
|
-
Deno.serve(async (req) => {
|
|
799
|
-
const event = await req.json() // ERRADO: aceitar sem verificar assinatura
|
|
800
|
-
// processar event...
|
|
801
|
-
})
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
**Por quê:** qualquer requisição HTTP pode acionar o hook — atacante pode forjar eventos de auth e manipular JWTs, criar usuários, etc.
|
|
805
|
-
|
|
806
|
-
**Certo:** sempre verificar com `standardwebhooks`:
|
|
807
|
-
```ts
|
|
808
|
-
const wh = new Webhook(secret)
|
|
809
|
-
const event = wh.verify(payload, headers) // lança erro se inválido
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
### Anti-pattern 3: Hook com query custosa (JOINs, N+1)
|
|
813
|
-
|
|
814
|
-
**Errado:**
|
|
815
|
-
```sql
|
|
816
|
-
create or replace function public.custom_access_token_hook(event jsonb)
|
|
817
|
-
returns jsonb language plpgsql stable as $$
|
|
818
|
-
declare claims jsonb;
|
|
819
|
-
begin
|
|
820
|
-
claims := event->'claims';
|
|
821
|
-
-- query com múltiplos JOINs — roda em CADA login e refresh
|
|
822
|
-
select jsonb_build_object(
|
|
823
|
-
'org_name', o.name,
|
|
824
|
-
'plan', s.plan,
|
|
825
|
-
'feature_flags', ff.flags
|
|
826
|
-
) into ...
|
|
827
|
-
from auth.users u
|
|
828
|
-
join public.organizations o on u.raw_user_meta_data->>'org_id' = o.id::text
|
|
829
|
-
join public.subscriptions s on o.id = s.org_id
|
|
830
|
-
join public.feature_flags ff on s.plan = ff.plan
|
|
831
|
-
where u.id = (event->>'user_id')::uuid;
|
|
832
|
-
-- ...
|
|
833
|
-
end;
|
|
834
|
-
$$;
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
**Por quê:** hook roda em cada login E cada refresh de JWT. Query com JOINs pode adicionar 50-200ms em cada operação de auth — inaceitável em produção.
|
|
838
|
-
|
|
839
|
-
**Certo:** denormalizar dados na tabela de roles/perfis; hook faz query simples em única tabela:
|
|
840
|
-
```sql
|
|
841
|
-
-- tabela denormalizada: user_profile com tudo necessário
|
|
842
|
-
select org_name, plan, feature_flags
|
|
843
|
-
into user_data
|
|
844
|
-
from public.user_profiles
|
|
845
|
-
where user_id = (event->>'user_id')::uuid;
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
### Anti-pattern 4: Usar `security definer` desnecessariamente
|
|
849
|
-
|
|
850
|
-
**Errado:**
|
|
851
|
-
```sql
|
|
852
|
-
create or replace function public.before_user_created_hook(event jsonb)
|
|
853
|
-
returns jsonb
|
|
854
|
-
language plpgsql
|
|
855
|
-
security definer -- DESNECESSÁRIO se grants explícitos suficientes
|
|
856
|
-
as $$
|
|
857
|
-
-- ...
|
|
858
|
-
$$;
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
**Por quê:** `security definer` faz a função rodar com privilégios do owner (`postgres`) — acesso irrestrito a todos os schemas e tabelas. Risco de path injection e acesso não intencional.
|
|
862
|
-
|
|
863
|
-
**Certo:** grants explícitos ao `supabase_auth_admin` para tabelas específicas + sem `security definer`:
|
|
864
|
-
```sql
|
|
865
|
-
grant select on table public.blocked_email_domains to supabase_auth_admin;
|
|
866
|
-
-- sem security definer na função
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
## Ver também
|
|
870
|
-
|
|
871
|
-
- [supabase-custom-claims-rbac](../supabase-custom-claims-rbac/SKILL.md) — `custom_access_token_hook` completo com RBAC
|
|
872
|
-
- [supabase-edge-functions-auth](../supabase-edge-functions-auth/SKILL.md) — autenticação e segurança em Edge Functions
|
|
873
|
-
- [supabase-mfa](../supabase-mfa/SKILL.md) — MFA TOTP e Phone; MFA Verification Hook para rate-limit
|
|
874
|
-
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — políticas RLS que consomem claims do `custom_access_token_hook`
|
|
875
|
-
- [supabase-auth-hook-writer](../../agents/supabase-auth-hook-writer.md) — agente que escreve Auth Hooks com grants corretos e testes
|
|
1
|
+
---
|
|
2
|
+
name: supabase-auth-hooks
|
|
3
|
+
description: Use ao implementar Auth Hooks no Supabase — custom access token, send email/SMS, before user created, MFA e password verification hooks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Auth Hooks
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando implementar **Auth Hooks** no Supabase — endpoints que interceptam e modificam o fluxo padrão de autenticação em pontos de execução específicos.
|
|
11
|
+
|
|
12
|
+
Trigger phrases:
|
|
13
|
+
|
|
14
|
+
- "auth hook Supabase", "custom access token hook"
|
|
15
|
+
- "send email hook", "send sms hook"
|
|
16
|
+
- "before user created hook", "MFA verification hook"
|
|
17
|
+
- "password verification hook"
|
|
18
|
+
- "Postgres function hook", "HTTP hook Supabase"
|
|
19
|
+
- `custom_access_token_hook`, `supabase_auth_admin`
|
|
20
|
+
- "como bloquear signup por domínio", "como customizar email de auth"
|
|
21
|
+
- "hook para rate-limit de login", "Standard Webhooks Supabase"
|
|
22
|
+
|
|
23
|
+
## Princípio canônico
|
|
24
|
+
|
|
25
|
+
Auth Hooks são **endpoints síncronos** que o Supabase Auth invoca em pontos específicos do fluxo de autenticação. O hook recebe um payload JSON, pode modificar o comportamento e retorna um JSON de resposta. Erros retornados pelo hook **bloqueiam** a operação de auth correspondente.
|
|
26
|
+
|
|
27
|
+
**6 hooks disponíveis:**
|
|
28
|
+
|
|
29
|
+
| Hook | Disponibilidade | Quando é invocado |
|
|
30
|
+
|------|----------------|-------------------|
|
|
31
|
+
| Before User Created | Free / Pro | Antes de criar novo usuário |
|
|
32
|
+
| Custom Access Token | Free / Pro | Antes de emitir JWT (login + refresh) |
|
|
33
|
+
| Send SMS | Free / Pro | Quando Supabase Auth precisa enviar SMS (OTP, MFA) |
|
|
34
|
+
| Send Email | Free / Pro | Quando Supabase Auth precisa enviar email (confirmação, magic link) |
|
|
35
|
+
| MFA Verification | Teams / Enterprise | Ao verificar código MFA |
|
|
36
|
+
| Password Verification | Teams / Enterprise | Ao verificar senha de login |
|
|
37
|
+
|
|
38
|
+
**2 tipos de hook:**
|
|
39
|
+
|
|
40
|
+
| Tipo | URI | Quando usar |
|
|
41
|
+
|------|-----|-------------|
|
|
42
|
+
| **Postgres function** | `pg-functions://postgres/public/<nome_fn>` | Lógica simples, acesso direto ao DB, sem I/O externo |
|
|
43
|
+
| **HTTP endpoint** | URL HTTPS | Lógica complexa, chamada a APIs externas, Edge Functions |
|
|
44
|
+
|
|
45
|
+
## Modelo de segurança — Postgres Function Hook
|
|
46
|
+
|
|
47
|
+
A função hook roda com o Postgres role `supabase_auth_admin` — precisa de grants explícitos:
|
|
48
|
+
|
|
49
|
+
```sql
|
|
50
|
+
-- OBRIGATÓRIO: grants para supabase_auth_admin
|
|
51
|
+
grant usage on schema public to supabase_auth_admin;
|
|
52
|
+
|
|
53
|
+
grant execute
|
|
54
|
+
on function public.minha_funcao_hook
|
|
55
|
+
to supabase_auth_admin;
|
|
56
|
+
|
|
57
|
+
-- OBRIGATÓRIO: revogar de roles públicos
|
|
58
|
+
revoke execute
|
|
59
|
+
on function public.minha_funcao_hook
|
|
60
|
+
from authenticated, anon, public;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Por quê não usar `security definer`:** prefira grants explícitos — `security definer` faz a função rodar com privilégios do owner (geralmente `postgres`) o que é mais amplo do que necessário. Grants são o mínimo necessário e mais auditáveis.
|
|
64
|
+
|
|
65
|
+
**Exceção:** se a função precisar acessar tabelas ou schemas que `supabase_auth_admin` não tem permissão, use `security definer` com `set search_path = ''` para evitar injeção via search_path:
|
|
66
|
+
|
|
67
|
+
```sql
|
|
68
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
69
|
+
returns jsonb
|
|
70
|
+
language plpgsql
|
|
71
|
+
stable
|
|
72
|
+
-- security definer apenas se necessário para acesso a schema restrito
|
|
73
|
+
-- set search_path = '' -- anti-injeção de schema
|
|
74
|
+
as $$
|
|
75
|
+
-- implementação
|
|
76
|
+
$$;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Modelo de segurança — HTTP Hook
|
|
80
|
+
|
|
81
|
+
Hooks HTTP seguem a especificação **Standard Webhooks** para autenticidade das requisições:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// Headers enviados pelo Supabase Auth em cada chamada HTTP ao hook
|
|
85
|
+
// webhook-id: <uuid único por chamada>
|
|
86
|
+
// webhook-timestamp: <unix timestamp em segundos>
|
|
87
|
+
// webhook-signature: v1=<assinatura HMAC-SHA256 base64>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Verificar assinatura com a lib `standardwebhooks`:**
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// deno / Edge Function
|
|
94
|
+
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
|
95
|
+
|
|
96
|
+
const secret = Deno.env.get('SEND_EMAIL_HOOK_SECRET')!
|
|
97
|
+
// formato do secret configurado no Supabase: v1,whsec_<base64-secret>
|
|
98
|
+
|
|
99
|
+
Deno.serve(async (req) => {
|
|
100
|
+
const payload = await req.text()
|
|
101
|
+
const headers = Object.fromEntries(req.headers)
|
|
102
|
+
|
|
103
|
+
const wh = new Webhook(secret)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// lança erro se assinatura inválida
|
|
107
|
+
const event = wh.verify(payload, headers)
|
|
108
|
+
// processar evento verificado
|
|
109
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
})
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// assinatura inválida → rejeitar
|
|
114
|
+
return new Response(
|
|
115
|
+
JSON.stringify({ error: { http_code: 401, message: 'Assinatura inválida' } }),
|
|
116
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Formato do secret:** Supabase gera um secret no formato `v1,whsec_<base64>` — usar exatamente este formato ao instanciar `new Webhook(secret)`.
|
|
123
|
+
|
|
124
|
+
## Configuração — Dashboard e config.toml
|
|
125
|
+
|
|
126
|
+
**Via Dashboard (produção):**
|
|
127
|
+
|
|
128
|
+
1. Acessar `Authentication > Hooks` no Dashboard
|
|
129
|
+
2. Selecionar o tipo de hook (ex: "Custom Access Token")
|
|
130
|
+
3. Escolher tipo: Postgres function ou HTTP
|
|
131
|
+
4. Para Postgres: selecionar a função no dropdown
|
|
132
|
+
5. Para HTTP: digitar a URL + configurar o secret
|
|
133
|
+
6. Salvar
|
|
134
|
+
|
|
135
|
+
**Via `config.toml` (desenvolvimento local):**
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
# supabase/config.toml
|
|
139
|
+
|
|
140
|
+
# Custom Access Token Hook (Postgres function)
|
|
141
|
+
[auth.hook.custom_access_token]
|
|
142
|
+
enabled = true
|
|
143
|
+
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
|
144
|
+
|
|
145
|
+
# Custom Access Token Hook (HTTP — Edge Function local)
|
|
146
|
+
[auth.hook.custom_access_token]
|
|
147
|
+
enabled = true
|
|
148
|
+
uri = "http://localhost:54321/functions/v1/custom-access-token-hook"
|
|
149
|
+
secrets = "v1,whsec_dGVzdHNlY3JldA=="
|
|
150
|
+
|
|
151
|
+
# Send Email Hook
|
|
152
|
+
[auth.hook.send_email]
|
|
153
|
+
enabled = true
|
|
154
|
+
uri = "http://localhost:54321/functions/v1/send-email-hook"
|
|
155
|
+
secrets = "v1,whsec_dGVzdHNlY3JldA=="
|
|
156
|
+
|
|
157
|
+
# Send SMS Hook
|
|
158
|
+
[auth.hook.send_sms]
|
|
159
|
+
enabled = true
|
|
160
|
+
uri = "http://localhost:54321/functions/v1/send-sms-hook"
|
|
161
|
+
secrets = "v1,whsec_dGVzdHNlY3JldA=="
|
|
162
|
+
|
|
163
|
+
# Before User Created Hook
|
|
164
|
+
[auth.hook.before_user_created]
|
|
165
|
+
enabled = true
|
|
166
|
+
uri = "pg-functions://postgres/public/before_user_created_hook"
|
|
167
|
+
|
|
168
|
+
# MFA Verification Hook (apenas Teams/Enterprise)
|
|
169
|
+
[auth.hook.mfa_verification_attempt]
|
|
170
|
+
enabled = true
|
|
171
|
+
uri = "pg-functions://postgres/public/mfa_verification_hook"
|
|
172
|
+
|
|
173
|
+
# Password Verification Hook (apenas Teams/Enterprise)
|
|
174
|
+
[auth.hook.password_verification_attempt]
|
|
175
|
+
enabled = true
|
|
176
|
+
uri = "pg-functions://postgres/public/password_verification_hook"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Error Handling — Status Codes e Retry
|
|
180
|
+
|
|
181
|
+
**Formato de erro retornado pelo hook:**
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"error": {
|
|
186
|
+
"http_code": 403,
|
|
187
|
+
"message": "Domínio de email não permitido"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Comportamento por status code:**
|
|
193
|
+
|
|
194
|
+
| Status code retornado pelo hook | Comportamento do Supabase Auth |
|
|
195
|
+
|---------------------------------|-------------------------------|
|
|
196
|
+
| `200` / `202` / `204` | Sucesso — continua o fluxo de auth |
|
|
197
|
+
| `400` | Falha permanente — converte em `500` para o cliente (não retry) |
|
|
198
|
+
| `403` | Falha permanente — converte em `500` para o cliente (não retry) |
|
|
199
|
+
| `429` | Rate-limit — **retry** automático (usar header `Retry-After`) |
|
|
200
|
+
| `503` | Serviço indisponível — **retry** automático |
|
|
201
|
+
|
|
202
|
+
**Respostas retry-able:**
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// Hook que sinaliza rate-limit com retry
|
|
206
|
+
return new Response(
|
|
207
|
+
JSON.stringify({ error: { http_code: 429, message: 'Muitas tentativas' } }),
|
|
208
|
+
{
|
|
209
|
+
status: 429,
|
|
210
|
+
headers: {
|
|
211
|
+
'Content-Type': 'application/json',
|
|
212
|
+
'Retry-After': '60', // Supabase Auth vai retentar após 60 segundos
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Regra:** TODAS as respostas de hook (sucesso ou erro) devem ter header `Content-Type: application/json`.
|
|
219
|
+
|
|
220
|
+
## Hook 1 — Custom Access Token
|
|
221
|
+
|
|
222
|
+
Invocado antes de cada emissão de JWT (login inicial + refresh). Permite injetar claims customizados.
|
|
223
|
+
|
|
224
|
+
**Input:**
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"user_id": "uuid-do-usuario",
|
|
228
|
+
"claims": {
|
|
229
|
+
"aal": "aal1",
|
|
230
|
+
"sub": "uuid-do-usuario",
|
|
231
|
+
"email": "usuario@empresa.com",
|
|
232
|
+
"role": "authenticated",
|
|
233
|
+
"exp": 1704067200,
|
|
234
|
+
"iat": 1704063600,
|
|
235
|
+
"iss": "https://proj.supabase.co/auth/v1",
|
|
236
|
+
"session_id": "uuid-da-sessao",
|
|
237
|
+
"amr": [{"method": "password", "timestamp": 1704063600}]
|
|
238
|
+
},
|
|
239
|
+
"authentication_method": "password"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Output (modificar claims):**
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"claims": {
|
|
247
|
+
"aal": "aal1",
|
|
248
|
+
"sub": "uuid-do-usuario",
|
|
249
|
+
"email": "usuario@empresa.com",
|
|
250
|
+
"role": "authenticated",
|
|
251
|
+
"exp": 1704067200,
|
|
252
|
+
"iat": 1704063600,
|
|
253
|
+
"iss": "https://proj.supabase.co/auth/v1",
|
|
254
|
+
"session_id": "uuid-da-sessao",
|
|
255
|
+
"amr": [{"method": "password", "timestamp": 1704063600}],
|
|
256
|
+
"user_role": "admin",
|
|
257
|
+
"org_id": "uuid-da-org"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Implementação Postgres canônica (ver skill `supabase-custom-claims-rbac` para pattern completo):**
|
|
263
|
+
|
|
264
|
+
```sql
|
|
265
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
266
|
+
returns jsonb
|
|
267
|
+
language plpgsql
|
|
268
|
+
stable
|
|
269
|
+
as $$
|
|
270
|
+
declare
|
|
271
|
+
claims jsonb;
|
|
272
|
+
user_role public.app_role;
|
|
273
|
+
begin
|
|
274
|
+
select role into user_role
|
|
275
|
+
from public.user_roles
|
|
276
|
+
where user_id = (event->>'user_id')::uuid;
|
|
277
|
+
|
|
278
|
+
claims := event->'claims';
|
|
279
|
+
|
|
280
|
+
claims := jsonb_set(
|
|
281
|
+
claims,
|
|
282
|
+
'{user_role}',
|
|
283
|
+
case when user_role is not null then to_jsonb(user_role) else 'null'::jsonb end
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return jsonb_set(event, '{claims}', claims);
|
|
287
|
+
end;
|
|
288
|
+
$$;
|
|
289
|
+
|
|
290
|
+
-- grants obrigatórios
|
|
291
|
+
grant usage on schema public to supabase_auth_admin;
|
|
292
|
+
grant execute on function public.custom_access_token_hook to supabase_auth_admin;
|
|
293
|
+
revoke execute on function public.custom_access_token_hook from authenticated, anon, public;
|
|
294
|
+
grant all on table public.user_roles to supabase_auth_admin;
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Usos canônicos do Custom Access Token hook:**
|
|
298
|
+
|
|
299
|
+
- Reduzir tamanho do JWT (omitir claims não necessários)
|
|
300
|
+
- Adicionar claim `user_role`, `org_id`, `plan` ao JWT
|
|
301
|
+
- Restringir login por tipo de autenticação (`authentication_method`)
|
|
302
|
+
- Adicionar claim `is_admin` baseado em SSO provider
|
|
303
|
+
|
|
304
|
+
```sql
|
|
305
|
+
-- restringir acesso por método de autenticação (ex: SSO apenas para admins)
|
|
306
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
307
|
+
returns jsonb
|
|
308
|
+
language plpgsql
|
|
309
|
+
stable
|
|
310
|
+
as $$
|
|
311
|
+
declare
|
|
312
|
+
claims jsonb;
|
|
313
|
+
auth_method text;
|
|
314
|
+
begin
|
|
315
|
+
auth_method := event->>'authentication_method';
|
|
316
|
+
claims := event->'claims';
|
|
317
|
+
|
|
318
|
+
-- adicionar claim que indica se login foi via SSO
|
|
319
|
+
claims := jsonb_set(claims, '{via_sso}', to_jsonb(auth_method = 'sso/saml'));
|
|
320
|
+
|
|
321
|
+
return jsonb_set(event, '{claims}', claims);
|
|
322
|
+
end;
|
|
323
|
+
$$;
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Hook 2 — Before User Created
|
|
327
|
+
|
|
328
|
+
Invocado antes de criar um novo usuário. Retornar `error` **rejeita** o signup.
|
|
329
|
+
|
|
330
|
+
**Input:**
|
|
331
|
+
```json
|
|
332
|
+
{
|
|
333
|
+
"user": {
|
|
334
|
+
"id": "uuid-gerado",
|
|
335
|
+
"email": "novo@dominio.com",
|
|
336
|
+
"phone": "",
|
|
337
|
+
"app_metadata": {},
|
|
338
|
+
"user_metadata": {"nome": "João"},
|
|
339
|
+
"identities": [],
|
|
340
|
+
"created_at": "2026-05-19T00:00:00Z",
|
|
341
|
+
"updated_at": "2026-05-19T00:00:00Z"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Bloquear domínios descartáveis:**
|
|
347
|
+
|
|
348
|
+
```sql
|
|
349
|
+
create or replace function public.before_user_created_hook(event jsonb)
|
|
350
|
+
returns jsonb
|
|
351
|
+
language plpgsql
|
|
352
|
+
stable
|
|
353
|
+
as $$
|
|
354
|
+
declare
|
|
355
|
+
email_address text;
|
|
356
|
+
email_domain text;
|
|
357
|
+
dominio_bloqueado bool;
|
|
358
|
+
begin
|
|
359
|
+
email_address := event->'user'->>'email';
|
|
360
|
+
email_domain := split_part(email_address, '@', 2);
|
|
361
|
+
|
|
362
|
+
-- checar em tabela de domínios bloqueados
|
|
363
|
+
select exists(
|
|
364
|
+
select 1 from public.blocked_email_domains
|
|
365
|
+
where domain = lower(email_domain)
|
|
366
|
+
) into dominio_bloqueado;
|
|
367
|
+
|
|
368
|
+
if dominio_bloqueado then
|
|
369
|
+
return jsonb_build_object(
|
|
370
|
+
'error', jsonb_build_object(
|
|
371
|
+
'http_code', 422,
|
|
372
|
+
'message', 'Domínio de email não permitido. Use um email corporativo.'
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
end if;
|
|
376
|
+
|
|
377
|
+
-- retornar event sem modificações (signup permitido)
|
|
378
|
+
return event;
|
|
379
|
+
end;
|
|
380
|
+
$$;
|
|
381
|
+
|
|
382
|
+
-- tabela de domínios bloqueados
|
|
383
|
+
create table public.blocked_email_domains (
|
|
384
|
+
domain text primary key,
|
|
385
|
+
motivo text,
|
|
386
|
+
criado_em timestamptz default now()
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
-- seed inicial de domínios descartáveis comuns
|
|
390
|
+
insert into public.blocked_email_domains (domain, motivo) values
|
|
391
|
+
('mailinator.com', 'Email descartável'),
|
|
392
|
+
('guerrillamail.com', 'Email descartável'),
|
|
393
|
+
('tempmail.com', 'Email descartável'),
|
|
394
|
+
('throwam.com', 'Email descartável'),
|
|
395
|
+
('yopmail.com', 'Email descartável');
|
|
396
|
+
|
|
397
|
+
-- grants
|
|
398
|
+
grant usage on schema public to supabase_auth_admin;
|
|
399
|
+
grant execute on function public.before_user_created_hook to supabase_auth_admin;
|
|
400
|
+
revoke execute on function public.before_user_created_hook from authenticated, anon, public;
|
|
401
|
+
grant select on table public.blocked_email_domains to supabase_auth_admin;
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Bloquear por provider — rejeitar signup via email (apenas SSO permitido):**
|
|
405
|
+
|
|
406
|
+
```sql
|
|
407
|
+
-- em aplicação B2B que só aceita login via SSO corporativo
|
|
408
|
+
create or replace function public.before_user_created_hook(event jsonb)
|
|
409
|
+
returns jsonb
|
|
410
|
+
language plpgsql
|
|
411
|
+
stable
|
|
412
|
+
as $$
|
|
413
|
+
declare
|
|
414
|
+
identities jsonb;
|
|
415
|
+
tem_sso bool;
|
|
416
|
+
begin
|
|
417
|
+
identities := event->'user'->'identities';
|
|
418
|
+
|
|
419
|
+
-- checar se tem identidade SSO (provider começa com 'sso:')
|
|
420
|
+
select exists(
|
|
421
|
+
select 1 from jsonb_array_elements(identities) as i
|
|
422
|
+
where (i->>'provider') like 'sso:%'
|
|
423
|
+
) into tem_sso;
|
|
424
|
+
|
|
425
|
+
if not tem_sso then
|
|
426
|
+
return jsonb_build_object(
|
|
427
|
+
'error', jsonb_build_object(
|
|
428
|
+
'http_code', 403,
|
|
429
|
+
'message', 'Apenas login via SSO corporativo é permitido.'
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
end if;
|
|
433
|
+
|
|
434
|
+
return event;
|
|
435
|
+
end;
|
|
436
|
+
$$;
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Hook 3 — Send SMS
|
|
440
|
+
|
|
441
|
+
Substitui o envio de SMS do Supabase por provider customizado (Twilio, AWS SNS, Vonage).
|
|
442
|
+
|
|
443
|
+
**Input:**
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"user": { "id": "uuid", "phone": "+5511999999999" },
|
|
447
|
+
"sms": { "otp": "123456" }
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Edge Function com Twilio:**
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
// supabase/functions/send-sms-hook/index.ts
|
|
455
|
+
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
|
456
|
+
|
|
457
|
+
const secret = Deno.env.get('SEND_SMS_HOOK_SECRET')!
|
|
458
|
+
const TWILIO_ACCOUNT_SID = Deno.env.get('TWILIO_ACCOUNT_SID')!
|
|
459
|
+
const TWILIO_AUTH_TOKEN = Deno.env.get('TWILIO_AUTH_TOKEN')!
|
|
460
|
+
const TWILIO_FROM = Deno.env.get('TWILIO_FROM_NUMBER')!
|
|
461
|
+
|
|
462
|
+
Deno.serve(async (req) => {
|
|
463
|
+
const payload = await req.text()
|
|
464
|
+
const headers = Object.fromEntries(req.headers)
|
|
465
|
+
|
|
466
|
+
// verificar assinatura Standard Webhooks
|
|
467
|
+
const wh = new Webhook(secret)
|
|
468
|
+
let event: { user: { phone: string }; sms: { otp: string } }
|
|
469
|
+
try {
|
|
470
|
+
event = wh.verify(payload, headers) as typeof event
|
|
471
|
+
} catch {
|
|
472
|
+
return new Response(
|
|
473
|
+
JSON.stringify({ error: { http_code: 401, message: 'Assinatura inválida' } }),
|
|
474
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const { phone } = event.user
|
|
479
|
+
const { otp } = event.sms
|
|
480
|
+
|
|
481
|
+
// enviar via Twilio
|
|
482
|
+
const resp = await fetch(
|
|
483
|
+
`https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json`,
|
|
484
|
+
{
|
|
485
|
+
method: 'POST',
|
|
486
|
+
headers: {
|
|
487
|
+
Authorization: `Basic ${btoa(`${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}`)}`,
|
|
488
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
489
|
+
},
|
|
490
|
+
body: new URLSearchParams({
|
|
491
|
+
To: phone,
|
|
492
|
+
From: TWILIO_FROM,
|
|
493
|
+
Body: `Seu código de verificação: ${otp}. Válido por 5 minutos.`,
|
|
494
|
+
}),
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if (!resp.ok) {
|
|
499
|
+
const err = await resp.json()
|
|
500
|
+
console.error('Erro Twilio:', err)
|
|
501
|
+
// 503 → Supabase vai retentar
|
|
502
|
+
return new Response(
|
|
503
|
+
JSON.stringify({ error: { http_code: 503, message: 'Falha no envio de SMS' } }),
|
|
504
|
+
{ status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': '30' } }
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return new Response(JSON.stringify({}), {
|
|
509
|
+
headers: { 'Content-Type': 'application/json' },
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Hook 4 — Send Email
|
|
515
|
+
|
|
516
|
+
Substitui emails transacionais do Supabase por provider customizado (Resend, SendGrid, AWS SES).
|
|
517
|
+
|
|
518
|
+
**Input (exemplo para magic link):**
|
|
519
|
+
```json
|
|
520
|
+
{
|
|
521
|
+
"user": { "id": "uuid", "email": "usuario@empresa.com" },
|
|
522
|
+
"email_data": {
|
|
523
|
+
"token": "token-opaque",
|
|
524
|
+
"token_hash": "hash-do-token",
|
|
525
|
+
"redirect_to": "https://app.com/auth/callback",
|
|
526
|
+
"email_action_type": "magic_link",
|
|
527
|
+
"site_url": "https://app.com",
|
|
528
|
+
"token_new": "",
|
|
529
|
+
"token_hash_new": ""
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Tipos de `email_action_type`:** `signup`, `magic_link`, `recovery`, `invite`, `email_change_new`, `email_change_current`.
|
|
535
|
+
|
|
536
|
+
**Edge Function com Resend:**
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
// supabase/functions/send-email-hook/index.ts
|
|
540
|
+
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
|
541
|
+
|
|
542
|
+
const secret = Deno.env.get('SEND_EMAIL_HOOK_SECRET')!
|
|
543
|
+
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!
|
|
544
|
+
|
|
545
|
+
type EmailActionType = 'signup' | 'magic_link' | 'recovery' | 'invite' |
|
|
546
|
+
'email_change_new' | 'email_change_current'
|
|
547
|
+
|
|
548
|
+
function montarConteudo(tipo: EmailActionType, emailData: any): { subject: string; html: string } {
|
|
549
|
+
const link = `${emailData.site_url}/auth/confirm?token_hash=${emailData.token_hash}&type=${tipo}&next=${emailData.redirect_to}`
|
|
550
|
+
|
|
551
|
+
switch (tipo) {
|
|
552
|
+
case 'magic_link':
|
|
553
|
+
return {
|
|
554
|
+
subject: 'Seu link de acesso',
|
|
555
|
+
html: `<p>Clique <a href="${link}">aqui</a> para acessar. Válido por 1 hora.</p>`,
|
|
556
|
+
}
|
|
557
|
+
case 'signup':
|
|
558
|
+
return {
|
|
559
|
+
subject: 'Confirme seu email',
|
|
560
|
+
html: `<p>Bem-vindo! <a href="${link}">Confirme seu email</a> para começar.</p>`,
|
|
561
|
+
}
|
|
562
|
+
case 'recovery':
|
|
563
|
+
return {
|
|
564
|
+
subject: 'Recuperação de senha',
|
|
565
|
+
html: `<p><a href="${link}">Redefina sua senha</a>. Válido por 1 hora.</p>`,
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
return {
|
|
569
|
+
subject: 'Ação necessária',
|
|
570
|
+
html: `<p><a href="${link}">Clique aqui</a> para continuar.</p>`,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
Deno.serve(async (req) => {
|
|
576
|
+
const payload = await req.text()
|
|
577
|
+
const headers = Object.fromEntries(req.headers)
|
|
578
|
+
|
|
579
|
+
const wh = new Webhook(secret)
|
|
580
|
+
let event: { user: { email: string }; email_data: any }
|
|
581
|
+
try {
|
|
582
|
+
event = wh.verify(payload, headers) as typeof event
|
|
583
|
+
} catch {
|
|
584
|
+
return new Response(
|
|
585
|
+
JSON.stringify({ error: { http_code: 401, message: 'Assinatura inválida' } }),
|
|
586
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const { email } = event.user
|
|
591
|
+
const { email_action_type, ...emailData } = event.email_data
|
|
592
|
+
const { subject, html } = montarConteudo(email_action_type, { email_action_type, ...emailData })
|
|
593
|
+
|
|
594
|
+
const resp = await fetch('https://api.resend.com/emails', {
|
|
595
|
+
method: 'POST',
|
|
596
|
+
headers: {
|
|
597
|
+
Authorization: `Bearer ${RESEND_API_KEY}`,
|
|
598
|
+
'Content-Type': 'application/json',
|
|
599
|
+
},
|
|
600
|
+
body: JSON.stringify({
|
|
601
|
+
from: 'no-reply@empresa.com',
|
|
602
|
+
to: [email],
|
|
603
|
+
subject,
|
|
604
|
+
html,
|
|
605
|
+
}),
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
if (!resp.ok) {
|
|
609
|
+
return new Response(
|
|
610
|
+
JSON.stringify({ error: { http_code: 503, message: 'Falha no envio de email' } }),
|
|
611
|
+
{ status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': '60' } }
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return new Response(JSON.stringify({}), {
|
|
616
|
+
headers: { 'Content-Type': 'application/json' },
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Hook 5 — MFA Verification (Teams/Enterprise)
|
|
622
|
+
|
|
623
|
+
Rate-limit customizado para tentativas de verificação MFA. Roda a cada `mfa.verify()`.
|
|
624
|
+
|
|
625
|
+
**Input:**
|
|
626
|
+
```json
|
|
627
|
+
{
|
|
628
|
+
"factor_id": "uuid-do-fator",
|
|
629
|
+
"factor_type": "totp",
|
|
630
|
+
"user_id": "uuid-do-usuario",
|
|
631
|
+
"valid": true
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Output:**
|
|
636
|
+
```json
|
|
637
|
+
{
|
|
638
|
+
"decision": "continue",
|
|
639
|
+
"message": ""
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
```sql
|
|
644
|
+
-- rate-limit: máximo 5 tentativas erradas em 15 minutos
|
|
645
|
+
create table public.mfa_attempt_log (
|
|
646
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
647
|
+
factor_id uuid not null,
|
|
648
|
+
tentativa timestamptz not null default now(),
|
|
649
|
+
sucesso boolean not null
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
create index on public.mfa_attempt_log (user_id, tentativa);
|
|
653
|
+
|
|
654
|
+
create or replace function public.mfa_verification_hook(event jsonb)
|
|
655
|
+
returns jsonb
|
|
656
|
+
language plpgsql
|
|
657
|
+
as $$
|
|
658
|
+
declare
|
|
659
|
+
user_id_val uuid;
|
|
660
|
+
factor_id_val uuid;
|
|
661
|
+
valida bool;
|
|
662
|
+
tentativas_erradas int;
|
|
663
|
+
begin
|
|
664
|
+
user_id_val := (event->>'user_id')::uuid;
|
|
665
|
+
factor_id_val := (event->>'factor_id')::uuid;
|
|
666
|
+
valida := (event->>'valid')::boolean;
|
|
667
|
+
|
|
668
|
+
-- checar tentativas erradas nos últimos 15 minutos
|
|
669
|
+
select count(*) into tentativas_erradas
|
|
670
|
+
from public.mfa_attempt_log
|
|
671
|
+
where user_id = user_id_val
|
|
672
|
+
and factor_id = factor_id_val
|
|
673
|
+
and sucesso = false
|
|
674
|
+
and tentativa > now() - interval '15 minutes';
|
|
675
|
+
|
|
676
|
+
-- logar tentativa atual
|
|
677
|
+
insert into public.mfa_attempt_log (user_id, factor_id, sucesso)
|
|
678
|
+
values (user_id_val, factor_id_val, valida);
|
|
679
|
+
|
|
680
|
+
if tentativas_erradas >= 5 then
|
|
681
|
+
return jsonb_build_object(
|
|
682
|
+
'decision', 'reject',
|
|
683
|
+
'message', 'Muitas tentativas incorretas. Aguarde 15 minutos.'
|
|
684
|
+
);
|
|
685
|
+
end if;
|
|
686
|
+
|
|
687
|
+
return jsonb_build_object('decision', 'continue', 'message', '');
|
|
688
|
+
end;
|
|
689
|
+
$$;
|
|
690
|
+
|
|
691
|
+
grant usage on schema public to supabase_auth_admin;
|
|
692
|
+
grant execute on function public.mfa_verification_hook to supabase_auth_admin;
|
|
693
|
+
revoke execute on function public.mfa_verification_hook from authenticated, anon, public;
|
|
694
|
+
grant all on table public.mfa_attempt_log to supabase_auth_admin;
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## Hook 6 — Password Verification (Teams/Enterprise)
|
|
698
|
+
|
|
699
|
+
Bloquear login após N tentativas erradas de senha.
|
|
700
|
+
|
|
701
|
+
**Input:**
|
|
702
|
+
```json
|
|
703
|
+
{
|
|
704
|
+
"user_id": "uuid-do-usuario",
|
|
705
|
+
"valid": false
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
```sql
|
|
710
|
+
create or replace function public.password_verification_hook(event jsonb)
|
|
711
|
+
returns jsonb
|
|
712
|
+
language plpgsql
|
|
713
|
+
as $$
|
|
714
|
+
declare
|
|
715
|
+
uid uuid;
|
|
716
|
+
valida bool;
|
|
717
|
+
erros_recentes int;
|
|
718
|
+
begin
|
|
719
|
+
uid := (event->>'user_id')::uuid;
|
|
720
|
+
valida := (event->>'valid')::boolean;
|
|
721
|
+
|
|
722
|
+
-- logar tentativa
|
|
723
|
+
insert into public.login_attempt_log (user_id, sucesso)
|
|
724
|
+
values (uid, valida);
|
|
725
|
+
|
|
726
|
+
-- contar erros nos últimos 30 minutos (apenas se tentativa inválida)
|
|
727
|
+
if not valida then
|
|
728
|
+
select count(*) into erros_recentes
|
|
729
|
+
from public.login_attempt_log
|
|
730
|
+
where user_id = uid
|
|
731
|
+
and sucesso = false
|
|
732
|
+
and tentativa > now() - interval '30 minutes';
|
|
733
|
+
|
|
734
|
+
if erros_recentes >= 10 then
|
|
735
|
+
return jsonb_build_object(
|
|
736
|
+
'decision', 'reject',
|
|
737
|
+
'message', 'Conta temporariamente bloqueada. Tente novamente em 30 minutos.'
|
|
738
|
+
);
|
|
739
|
+
end if;
|
|
740
|
+
end if;
|
|
741
|
+
|
|
742
|
+
return jsonb_build_object('decision', 'continue', 'message', '');
|
|
743
|
+
end;
|
|
744
|
+
$$;
|
|
745
|
+
|
|
746
|
+
create table public.login_attempt_log (
|
|
747
|
+
id bigint generated by default as identity primary key,
|
|
748
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
749
|
+
sucesso boolean not null,
|
|
750
|
+
tentativa timestamptz default now()
|
|
751
|
+
);
|
|
752
|
+
create index on public.login_attempt_log (user_id, tentativa);
|
|
753
|
+
|
|
754
|
+
grant usage on schema public to supabase_auth_admin;
|
|
755
|
+
grant execute on function public.password_verification_hook to supabase_auth_admin;
|
|
756
|
+
revoke execute on function public.password_verification_hook from authenticated, anon, public;
|
|
757
|
+
grant all on table public.login_attempt_log to supabase_auth_admin;
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
## Regras absolutas
|
|
761
|
+
|
|
762
|
+
1. **SEMPRE `grant execute` ao `supabase_auth_admin`** — sem este grant, hook Postgres falha silenciosamente; JWT é emitido sem modificações.
|
|
763
|
+
2. **SEMPRE `revoke execute` de `authenticated`, `anon`, `public`** — sem isso, qualquer cliente pode invocar a função diretamente.
|
|
764
|
+
3. **Hooks HTTP devem verificar assinatura Standard Webhooks** — usar `standardwebhooks` com o secret configurado. Nunca processar payload sem verificação.
|
|
765
|
+
4. **Evitar `security definer` desnecessariamente** — prefira grants explícitos; `security definer` amplia o acesso mais do que necessário.
|
|
766
|
+
5. **TODAS as respostas precisam `Content-Type: application/json`** — Supabase Auth rejeita respostas sem este header.
|
|
767
|
+
6. **Hook deve ser rápido (< 10ms idealmente)** — roda a cada login e refresh; query lenta degrada latência de auth de toda a aplicação.
|
|
768
|
+
7. **Hooks MFA/Password Verification são Teams/Enterprise only** — verificar plano antes de implementar.
|
|
769
|
+
|
|
770
|
+
## Anti-patterns
|
|
771
|
+
|
|
772
|
+
### Anti-pattern 1: Esquecer grants ao supabase_auth_admin
|
|
773
|
+
|
|
774
|
+
**Errado:**
|
|
775
|
+
```sql
|
|
776
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
777
|
+
returns jsonb language plpgsql stable as $$
|
|
778
|
+
-- implementação aqui
|
|
779
|
+
$$;
|
|
780
|
+
-- sem GRANT EXECUTE TO supabase_auth_admin
|
|
781
|
+
-- sem REVOKE de anon/authenticated
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Por quê:** Auth hook falha silenciosamente. O JWT é gerado **sem** as modificações do hook — claims customizados não aparecem. Difícil de debugar pois não há erro explícito.
|
|
785
|
+
|
|
786
|
+
**Certo:**
|
|
787
|
+
```sql
|
|
788
|
+
-- SEMPRE após criar a função
|
|
789
|
+
grant usage on schema public to supabase_auth_admin;
|
|
790
|
+
grant execute on function public.custom_access_token_hook to supabase_auth_admin;
|
|
791
|
+
revoke execute on function public.custom_access_token_hook from authenticated, anon, public;
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Anti-pattern 2: Não verificar assinatura do webhook HTTP
|
|
795
|
+
|
|
796
|
+
**Errado:**
|
|
797
|
+
```ts
|
|
798
|
+
Deno.serve(async (req) => {
|
|
799
|
+
const event = await req.json() // ERRADO: aceitar sem verificar assinatura
|
|
800
|
+
// processar event...
|
|
801
|
+
})
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Por quê:** qualquer requisição HTTP pode acionar o hook — atacante pode forjar eventos de auth e manipular JWTs, criar usuários, etc.
|
|
805
|
+
|
|
806
|
+
**Certo:** sempre verificar com `standardwebhooks`:
|
|
807
|
+
```ts
|
|
808
|
+
const wh = new Webhook(secret)
|
|
809
|
+
const event = wh.verify(payload, headers) // lança erro se inválido
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### Anti-pattern 3: Hook com query custosa (JOINs, N+1)
|
|
813
|
+
|
|
814
|
+
**Errado:**
|
|
815
|
+
```sql
|
|
816
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
817
|
+
returns jsonb language plpgsql stable as $$
|
|
818
|
+
declare claims jsonb;
|
|
819
|
+
begin
|
|
820
|
+
claims := event->'claims';
|
|
821
|
+
-- query com múltiplos JOINs — roda em CADA login e refresh
|
|
822
|
+
select jsonb_build_object(
|
|
823
|
+
'org_name', o.name,
|
|
824
|
+
'plan', s.plan,
|
|
825
|
+
'feature_flags', ff.flags
|
|
826
|
+
) into ...
|
|
827
|
+
from auth.users u
|
|
828
|
+
join public.organizations o on u.raw_user_meta_data->>'org_id' = o.id::text
|
|
829
|
+
join public.subscriptions s on o.id = s.org_id
|
|
830
|
+
join public.feature_flags ff on s.plan = ff.plan
|
|
831
|
+
where u.id = (event->>'user_id')::uuid;
|
|
832
|
+
-- ...
|
|
833
|
+
end;
|
|
834
|
+
$$;
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**Por quê:** hook roda em cada login E cada refresh de JWT. Query com JOINs pode adicionar 50-200ms em cada operação de auth — inaceitável em produção.
|
|
838
|
+
|
|
839
|
+
**Certo:** denormalizar dados na tabela de roles/perfis; hook faz query simples em única tabela:
|
|
840
|
+
```sql
|
|
841
|
+
-- tabela denormalizada: user_profile com tudo necessário
|
|
842
|
+
select org_name, plan, feature_flags
|
|
843
|
+
into user_data
|
|
844
|
+
from public.user_profiles
|
|
845
|
+
where user_id = (event->>'user_id')::uuid;
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### Anti-pattern 4: Usar `security definer` desnecessariamente
|
|
849
|
+
|
|
850
|
+
**Errado:**
|
|
851
|
+
```sql
|
|
852
|
+
create or replace function public.before_user_created_hook(event jsonb)
|
|
853
|
+
returns jsonb
|
|
854
|
+
language plpgsql
|
|
855
|
+
security definer -- DESNECESSÁRIO se grants explícitos suficientes
|
|
856
|
+
as $$
|
|
857
|
+
-- ...
|
|
858
|
+
$$;
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**Por quê:** `security definer` faz a função rodar com privilégios do owner (`postgres`) — acesso irrestrito a todos os schemas e tabelas. Risco de path injection e acesso não intencional.
|
|
862
|
+
|
|
863
|
+
**Certo:** grants explícitos ao `supabase_auth_admin` para tabelas específicas + sem `security definer`:
|
|
864
|
+
```sql
|
|
865
|
+
grant select on table public.blocked_email_domains to supabase_auth_admin;
|
|
866
|
+
-- sem security definer na função
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
## Ver também
|
|
870
|
+
|
|
871
|
+
- [supabase-custom-claims-rbac](../supabase-custom-claims-rbac/SKILL.md) — `custom_access_token_hook` completo com RBAC
|
|
872
|
+
- [supabase-edge-functions-auth](../supabase-edge-functions-auth/SKILL.md) — autenticação e segurança em Edge Functions
|
|
873
|
+
- [supabase-mfa](../supabase-mfa/SKILL.md) — MFA TOTP e Phone; MFA Verification Hook para rate-limit
|
|
874
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — políticas RLS que consomem claims do `custom_access_token_hook`
|
|
875
|
+
- [supabase-auth-hook-writer](../../agents/supabase-auth-hook-writer.md) — agente que escreve Auth Hooks com grants corretos e testes
|