@luanpdd/kit-mcp 1.31.0 → 1.32.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/README.md +1 -1
- package/kit/COMPATIBILITY.md +5 -0
- package/kit/agents/supabase-auth-bootstrapper.md +15 -1
- package/kit/agents/supabase-auth-hook-writer.md +418 -0
- package/kit/agents/supabase-mfa-implementer.md +439 -0
- package/kit/agents/supabase-oauth-server-implementer.md +507 -0
- package/kit/agents/supabase-social-auth-implementer.md +451 -0
- package/kit/agents/supabase-sso-saml-architect.md +549 -0
- package/kit/commands/supabase.md +21 -1
- package/kit/file-manifest.json +21 -6
- package/kit/skills/supabase-auth-hardening/SKILL.md +674 -0
- package/kit/skills/supabase-auth-hooks/SKILL.md +875 -0
- package/kit/skills/supabase-auth-methods/SKILL.md +486 -0
- package/kit/skills/supabase-auth-sessions/SKILL.md +579 -0
- package/kit/skills/supabase-auth-ssr/SKILL.md +60 -14
- package/kit/skills/supabase-enterprise-sso-saml/SKILL.md +545 -0
- package/kit/skills/supabase-jwt-signing-keys/SKILL.md +399 -0
- package/kit/skills/supabase-mfa/SKILL.md +488 -0
- package/kit/skills/supabase-oauth-server/SKILL.md +537 -0
- package/kit/skills/supabase-social-oauth/SKILL.md +480 -0
- package/kit/skills/supabase-third-party-auth/SKILL.md +450 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: supabase-auth-ssr
|
|
3
|
-
description: Use ao bootstrap Next.js v16 + Supabase Auth — @supabase/ssr, getAll/setAll APENAS, NUNCA auth-helpers-nextjs, proxy
|
|
3
|
+
description: Use ao bootstrap Next.js v16 + Supabase Auth — @supabase/ssr, getAll/setAll APENAS, NUNCA auth-helpers-nextjs, proxy com getClaims, cache headers e redirects.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Supabase — Auth SSR (Next.js v16+)
|
|
@@ -15,6 +15,7 @@ LLM carrega esta skill quando bootstrap ou auditar autenticação Supabase em Ne
|
|
|
15
15
|
- "middleware.ts auth", "proxy auth"
|
|
16
16
|
- "cookies getAll setAll"
|
|
17
17
|
- "Supabase auth Next.js v16"
|
|
18
|
+
- "getClaims proteger página SSR", "cache headers setAll"
|
|
18
19
|
|
|
19
20
|
## Regras absolutas
|
|
20
21
|
|
|
@@ -26,10 +27,11 @@ LLM carrega esta skill quando bootstrap ou auditar autenticação Supabase em Ne
|
|
|
26
27
|
- **Browser client e Server client são distintos:**
|
|
27
28
|
- Browser (`createBrowserClient`) → para Client Components ("use client")
|
|
28
29
|
- Server (`createServerClient`) → para Server Components, Route Handlers, Server Actions
|
|
29
|
-
- **Middleware (`middleware.ts`) obrigatório** para refresh de sessão SSR. Deve chamar `supabase.auth.
|
|
30
|
-
- **Auth method order** — após `createServerClient` mas **ANTES** de `
|
|
31
|
-
-
|
|
32
|
-
-
|
|
30
|
+
- **Middleware/Proxy (`middleware.ts`) obrigatório** para refresh de sessão SSR. Deve chamar `supabase.auth.getClaims()` em cada request — `getClaims()` valida a assinatura do JWT contra as chaves públicas publicadas e faz o refresh. **NUNCA confie em `getSession()` no servidor** — não revalida o token. `getUser()` continua válido (faz round-trip ao Auth server, garante que a sessão não foi revogada), mas é mais lento; prefira `getClaims()` para proteger páginas.
|
|
31
|
+
- **Auth method order** — após `createServerClient` mas **ANTES** de `getClaims()`, NÃO chamar nada que produza response intermediário. Os cookies precisam fluir corretamente.
|
|
32
|
+
- **Cache headers no `setAll`** — desde `@supabase/ssr` v0.10.0 o `setAll` recebe um 2º argumento com headers de cache (`Cache-Control`, `Expires`, `Pragma`). No proxy, aplique-os ao response — sem isso, um CDN/ISR pode cachear o `Set-Cookie` e vazar a sessão de um usuário para outro.
|
|
33
|
+
- **`NEXT_PUBLIC_*` apenas para a chave pública** — `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (chave nova `sb_publishable_...`) ou `NEXT_PUBLIC_SUPABASE_ANON_KEY` (legada, válida até fim de 2026). **NUNCA** `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY` nem `sb_secret_...` — bypassam RLS e seriam expostos ao cliente (anti-pitfall B6).
|
|
34
|
+
- **Single serverClient factory + inicializar dentro do request handler** — não criar múltiplos clients em layouts (race condition na refresh de token — B13) nem em escopo de módulo (em Vercel Fluid compute o client é reaproveitado entre requests e vaza sessão de outro usuário).
|
|
33
35
|
|
|
34
36
|
## Patterns canônicos
|
|
35
37
|
|
|
@@ -93,14 +95,16 @@ export async function createClient() {
|
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
```tsx
|
|
96
|
-
// PT-BR: uso em Server Component
|
|
98
|
+
// PT-BR: uso em Server Component — getClaims() valida a assinatura do JWT
|
|
99
|
+
// localmente e é a forma canônica de proteger páginas no servidor
|
|
97
100
|
import { createClient } from '@/utils/supabase/server'
|
|
98
101
|
|
|
99
102
|
export default async function Dashboard() {
|
|
100
103
|
const supabase = await createClient()
|
|
101
|
-
const { data
|
|
102
|
-
|
|
103
|
-
return <p>
|
|
104
|
+
const { data } = await supabase.auth.getClaims()
|
|
105
|
+
const claims = data?.claims
|
|
106
|
+
if (!claims) return <p>Não autenticado</p>
|
|
107
|
+
return <p>Olá, {claims.email}</p>
|
|
104
108
|
}
|
|
105
109
|
```
|
|
106
110
|
|
|
@@ -122,7 +126,7 @@ export async function middleware(request: NextRequest) {
|
|
|
122
126
|
getAll() {
|
|
123
127
|
return request.cookies.getAll()
|
|
124
128
|
},
|
|
125
|
-
setAll(cookiesToSet) {
|
|
129
|
+
setAll(cookiesToSet, headers) {
|
|
126
130
|
cookiesToSet.forEach(({ name, value }) =>
|
|
127
131
|
request.cookies.set(name, value)
|
|
128
132
|
)
|
|
@@ -130,14 +134,21 @@ export async function middleware(request: NextRequest) {
|
|
|
130
134
|
cookiesToSet.forEach(({ name, value, options }) =>
|
|
131
135
|
supabaseResponse.cookies.set(name, value, options)
|
|
132
136
|
)
|
|
137
|
+
// PT-BR: aplicar cache headers (@supabase/ssr v0.10.0+) — impede
|
|
138
|
+
// CDN/ISR de cachear o Set-Cookie e vazar sessão entre usuários
|
|
139
|
+
Object.entries(headers ?? {}).forEach(([key, value]) =>
|
|
140
|
+
supabaseResponse.headers.set(key, value)
|
|
141
|
+
)
|
|
133
142
|
},
|
|
134
143
|
},
|
|
135
144
|
}
|
|
136
145
|
)
|
|
137
146
|
|
|
138
|
-
// PT-BR: ATENÇÃO — não execute código entre createServerClient e
|
|
147
|
+
// PT-BR: ATENÇÃO — não execute código entre createServerClient e getClaims()
|
|
139
148
|
// (qualquer cookie set/get fora desse path quebra refresh silencioso)
|
|
140
|
-
|
|
149
|
+
// getClaims() valida a assinatura do JWT e faz o refresh da sessão
|
|
150
|
+
const { data } = await supabase.auth.getClaims()
|
|
151
|
+
const user = data?.claims ?? null
|
|
141
152
|
|
|
142
153
|
// PT-BR: redirect para /login se sem user
|
|
143
154
|
if (!user && !request.nextUrl.pathname.startsWith('/login')) {
|
|
@@ -245,15 +256,50 @@ const { user } = await supabase2.auth.getUser()
|
|
|
245
256
|
|
|
246
257
|
**Por quê:** múltiplos `createServerClient` na mesma request podem corromper cookies de refresh de token. Issue [supabase/ssr#68](https://github.com/supabase/ssr/issues/68) — race condition documentada.
|
|
247
258
|
|
|
248
|
-
**Certo:** middleware faz o refresh **uma vez por request**. Layouts apenas leem
|
|
259
|
+
**Certo:** middleware faz o refresh **uma vez por request**. Layouts apenas leem os claims via `getClaims()`:
|
|
249
260
|
```tsx
|
|
250
261
|
// app/layout.tsx — middleware já fez o refresh
|
|
251
262
|
const supabase = await createClient()
|
|
252
|
-
const { data
|
|
263
|
+
const { data } = await supabase.auth.getClaims()
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Anti-pattern 5: Confiar em `getSession()` no servidor
|
|
267
|
+
|
|
268
|
+
**Errado:**
|
|
269
|
+
```ts
|
|
270
|
+
// middleware.ts ou Server Component
|
|
271
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
272
|
+
if (!session) redirect('/login') // ⚠ inseguro no servidor
|
|
253
273
|
```
|
|
254
274
|
|
|
275
|
+
**Por quê:** `getSession()` lê a sessão direto dos cookies — que podem ser forjados — e **não revalida** o token. No servidor, use `getClaims()` (valida a assinatura do JWT contra as chaves públicas do projeto) ou `getUser()` (round-trip ao Auth server). `getSession()` é aceitável apenas no cliente.
|
|
276
|
+
|
|
277
|
+
**Certo:**
|
|
278
|
+
```ts
|
|
279
|
+
const { data } = await supabase.auth.getClaims()
|
|
280
|
+
if (!data?.claims) redirect('/login')
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Anti-pattern 6: Cliente Supabase em escopo de módulo (vazamento em Fluid compute)
|
|
284
|
+
|
|
285
|
+
**Errado:**
|
|
286
|
+
```ts
|
|
287
|
+
// ⚠ escopo de módulo — reaproveitado entre requests
|
|
288
|
+
const supabase = createServerClient(/* ... */)
|
|
289
|
+
export async function handler(req) { /* usa supabase compartilhado */ }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Por quê:** em Vercel Fluid compute (e instâncias serverless reaproveitadas) o client persiste entre requests de usuários diferentes — a sessão de um vaza para outro.
|
|
293
|
+
|
|
294
|
+
**Certo:** crie o client **dentro** do request handler, sempre via a factory `createClient()`.
|
|
295
|
+
|
|
255
296
|
## Ver também
|
|
256
297
|
|
|
298
|
+
- [supabase-auth-methods](../supabase-auth-methods/SKILL.md) — métodos de sign-in/sign-up (password, magic link, OTP, anonymous, Web3)
|
|
299
|
+
- [supabase-social-oauth](../supabase-social-oauth/SKILL.md) — social login + rota callback PKCE `/auth/callback`
|
|
300
|
+
- [supabase-auth-sessions](../supabase-auth-sessions/SKILL.md) — fluxos implicit vs PKCE, refresh tokens
|
|
301
|
+
- [supabase-jwt-signing-keys](../supabase-jwt-signing-keys/SKILL.md) — `getClaims()`, JWKS e signing keys assimétricas
|
|
302
|
+
- [supabase-auth-hardening](../supabase-auth-hardening/SKILL.md) — redirect URLs, custom SMTP, rate limits, CAPTCHA
|
|
257
303
|
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — RLS aplicado quando user autenticado consulta tabelas
|
|
258
304
|
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions usando service_role server-side
|
|
259
305
|
- [supabase-realtime](../supabase-realtime/SKILL.md) — Realtime exige usuário autenticado para canais privados
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-enterprise-sso-saml
|
|
3
|
+
description: Use ao configurar Enterprise SSO via SAML 2.0 no Supabase — Okta, Azure AD, Google Workspace, signInWithSSO, attribute mapping e RLS multi-tenant.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Enterprise SSO (SAML 2.0)
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando configurar **Single Sign-On empresarial via SAML 2.0** no Supabase, integrando com provedores de identidade (IdP) como Okta, Azure AD/Entra ID, Google Workspace ou PingIdentity.
|
|
11
|
+
|
|
12
|
+
Trigger phrases:
|
|
13
|
+
|
|
14
|
+
- "SAML SSO Supabase", "signInWithSSO"
|
|
15
|
+
- "enterprise single sign-on", "Okta Supabase"
|
|
16
|
+
- "Azure AD SAML", "Entra ID Supabase"
|
|
17
|
+
- "attribute mapping SAML", "multi-tenant SSO"
|
|
18
|
+
- "identity provider Supabase", "SP-initiated flow"
|
|
19
|
+
- "supabase sso add", "sso_provider_id RLS"
|
|
20
|
+
|
|
21
|
+
## Princípio canônico
|
|
22
|
+
|
|
23
|
+
SSO via SAML 2.0 permite que empresas clientes (tenants) façam login com suas credenciais corporativas existentes, sem criar senhas no Supabase. O Supabase atua como **Service Provider (SP)** e o sistema da empresa (Okta, Azure AD, etc.) é o **Identity Provider (IdP)**.
|
|
24
|
+
|
|
25
|
+
**Disponibilidade:** SAML SSO exige plano **Pro ou superior** do Supabase.
|
|
26
|
+
|
|
27
|
+
**Providers SAML suportados (testados):**
|
|
28
|
+
|
|
29
|
+
| Provider | Observação |
|
|
30
|
+
|----------|------------|
|
|
31
|
+
| Okta | Provider mais comum em B2B enterprise |
|
|
32
|
+
| Azure AD / Entra ID | Microsoft identity — integração via app enterprise |
|
|
33
|
+
| Google Workspace | Apenas org-level SSO (não contas pessoais Google) |
|
|
34
|
+
| PingIdentity | Usado em grandes enterprises financeiras |
|
|
35
|
+
| OneLogin | Alternativa frequente em médias empresas |
|
|
36
|
+
| Auth0 (como IdP) | Quando o tenant usa Auth0 para SSO interno |
|
|
37
|
+
|
|
38
|
+
**Quando usar SSO SAML:**
|
|
39
|
+
|
|
40
|
+
- ✅ B2B SaaS com clientes enterprise que exigem login corporativo
|
|
41
|
+
- ✅ Compliance que exige autenticação via IdP central da empresa
|
|
42
|
+
- ✅ Migração de usuários — empresa já tem Okta/Azure com todos os colaboradores
|
|
43
|
+
- ✅ Multi-tenant onde cada tenant tem seu próprio IdP
|
|
44
|
+
|
|
45
|
+
**Quando NÃO usar SSO SAML:**
|
|
46
|
+
|
|
47
|
+
- ❌ Aplicação B2C (usuários comuns) — use OAuth social ou magic link
|
|
48
|
+
- ❌ Plano Free do Supabase — SAML exige Pro+
|
|
49
|
+
- ❌ Poucos usuários que podem usar email/senha — overhead de configuração não compensa
|
|
50
|
+
|
|
51
|
+
## Terminologia SAML
|
|
52
|
+
|
|
53
|
+
| Termo | Significado |
|
|
54
|
+
|-------|-------------|
|
|
55
|
+
| **Identity Provider (IdP)** | Sistema que autentica o usuário (Okta, Azure AD) |
|
|
56
|
+
| **Service Provider (SP)** | Seu aplicativo (Supabase) — confia no IdP |
|
|
57
|
+
| **EntityID** | Identificador único do SP/IdP (geralmente uma URL) |
|
|
58
|
+
| **NameID** | Identificador do usuário no SAML assertion (email ou UUID) |
|
|
59
|
+
| **Assertion** | Documento XML assinado que o IdP envia ao SP após autenticação |
|
|
60
|
+
| **Metadata** | XML descrevendo endpoints, certificados e configurações do SP/IdP |
|
|
61
|
+
| **Certificate** | Certificado X.509 para assinar/verificar assertions |
|
|
62
|
+
| **ACS URL** | Assertion Consumer Service — endpoint do SP que recebe o assertion |
|
|
63
|
+
| **Binding** | Método de transporte (HTTP-POST ou HTTP-Redirect) |
|
|
64
|
+
| **RelayState** | Parâmetro opaco passado pelo SP ao IdP para manter estado (ex: URL de destino) |
|
|
65
|
+
|
|
66
|
+
## URLs SAML do Projeto Supabase
|
|
67
|
+
|
|
68
|
+
O Supabase expõe endpoints SAML padrão para cada projeto:
|
|
69
|
+
|
|
70
|
+
| Endpoint | URL |
|
|
71
|
+
|----------|-----|
|
|
72
|
+
| **Metadata (EntityID)** | `https://<project-ref>.supabase.co/auth/v1/sso/saml/metadata` |
|
|
73
|
+
| **ACS URL** | `https://<project-ref>.supabase.co/auth/v1/sso/saml/acs` |
|
|
74
|
+
|
|
75
|
+
**NameID:** configurar no IdP para enviar `emailAddress` (formato `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) ou `persistent` (UUID opaco). Prefira `persistent` em ambientes onde o email pode mudar.
|
|
76
|
+
|
|
77
|
+
**Configurar no IdP (exemplo Okta):**
|
|
78
|
+
|
|
79
|
+
1. No Okta Admin Console → Applications → Create App Integration → SAML 2.0
|
|
80
|
+
2. **Single Sign-On URL:** `https://<project-ref>.supabase.co/auth/v1/sso/saml/acs`
|
|
81
|
+
3. **Audience URI (SP Entity ID):** `https://<project-ref>.supabase.co/auth/v1/sso/saml/metadata`
|
|
82
|
+
4. **Name ID format:** `EmailAddress`
|
|
83
|
+
5. Adicionar attribute mappings (ver seção abaixo)
|
|
84
|
+
6. Baixar metadata XML do Okta para usar no `supabase sso add`
|
|
85
|
+
|
|
86
|
+
## Gerenciar Conexões SSO via Supabase CLI
|
|
87
|
+
|
|
88
|
+
**Requisito:** Supabase CLI v1.46.4 ou superior.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# verificar versão
|
|
92
|
+
supabase --version # deve ser >= 1.46.4
|
|
93
|
+
|
|
94
|
+
# adicionar conexão SSO via metadata URL (Okta/Azure expõem URL de metadata)
|
|
95
|
+
supabase sso add \
|
|
96
|
+
--project-ref <project-ref> \
|
|
97
|
+
--type saml \
|
|
98
|
+
--metadata-url https://okta.example.com/app/xxx/sso/saml/metadata \
|
|
99
|
+
--domains empresa.com,empresa.com.br
|
|
100
|
+
|
|
101
|
+
# adicionar via arquivo XML (quando o IdP não expõe URL pública de metadata)
|
|
102
|
+
supabase sso add \
|
|
103
|
+
--project-ref <project-ref> \
|
|
104
|
+
--type saml \
|
|
105
|
+
--metadata-file ./idp-metadata.xml \
|
|
106
|
+
--domains empresa.com
|
|
107
|
+
|
|
108
|
+
# adicionar com attribute mapping customizado
|
|
109
|
+
supabase sso add \
|
|
110
|
+
--project-ref <project-ref> \
|
|
111
|
+
--type saml \
|
|
112
|
+
--metadata-url https://okta.example.com/.../metadata \
|
|
113
|
+
--domains empresa.com \
|
|
114
|
+
--attribute-mapping-file ./attribute-mapping.json
|
|
115
|
+
|
|
116
|
+
# listar todas as conexões SSO do projeto
|
|
117
|
+
supabase sso list --project-ref <project-ref>
|
|
118
|
+
|
|
119
|
+
# ver detalhes de uma conexão (inclui provider_id)
|
|
120
|
+
supabase sso show --project-ref <project-ref> --sso-provider-id <uuid>
|
|
121
|
+
|
|
122
|
+
# atualizar domains ou metadata de uma conexão
|
|
123
|
+
supabase sso update \
|
|
124
|
+
--project-ref <project-ref> \
|
|
125
|
+
--sso-provider-id <uuid> \
|
|
126
|
+
--domains empresa.com,novodominio.com
|
|
127
|
+
|
|
128
|
+
# remover conexão SSO
|
|
129
|
+
supabase sso remove \
|
|
130
|
+
--project-ref <project-ref> \
|
|
131
|
+
--sso-provider-id <uuid>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Fluxo SP-Initiated (padrão)
|
|
135
|
+
|
|
136
|
+
O fluxo padrão é iniciado pelo SP (seu app) quando o usuário tenta acessar:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { createClient } from '@supabase/supabase-js'
|
|
140
|
+
|
|
141
|
+
const supabase = createClient(
|
|
142
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
143
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// SP-initiated: identificar pelo domínio do email do usuário
|
|
147
|
+
const iniciarSSO = async (email: string) => {
|
|
148
|
+
const dominio = email.split('@')[1]
|
|
149
|
+
|
|
150
|
+
const { data, error } = await supabase.auth.signInWithSSO({
|
|
151
|
+
domain: dominio,
|
|
152
|
+
options: {
|
|
153
|
+
redirectTo: `${window.location.origin}/auth/callback`,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (error) {
|
|
158
|
+
console.error('Erro ao iniciar SSO:', error.message)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (data?.url) {
|
|
163
|
+
// redirecionar para o IdP
|
|
164
|
+
window.location.href = data.url
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Alternativa: identificar pelo sso_provider_id (para multi-tenant com seleção explícita)
|
|
169
|
+
const iniciarSSOPorProvider = async (ssoProviderId: string) => {
|
|
170
|
+
const { data, error } = await supabase.auth.signInWithSSO({
|
|
171
|
+
providerId: ssoProviderId,
|
|
172
|
+
options: {
|
|
173
|
+
redirectTo: `${window.location.origin}/auth/callback`,
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if (data?.url) window.location.href = data.url
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Callback — trocar code por sessão:**
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// app/auth/callback/route.ts (Next.js Route Handler)
|
|
185
|
+
import { createServerClient } from '@supabase/ssr'
|
|
186
|
+
import { cookies } from 'next/headers'
|
|
187
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
188
|
+
|
|
189
|
+
export async function GET(request: NextRequest) {
|
|
190
|
+
const { searchParams } = new URL(request.url)
|
|
191
|
+
const code = searchParams.get('code')
|
|
192
|
+
|
|
193
|
+
if (code) {
|
|
194
|
+
const cookieStore = await cookies()
|
|
195
|
+
const supabase = createServerClient(
|
|
196
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
197
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
|
198
|
+
{
|
|
199
|
+
cookies: {
|
|
200
|
+
getAll: () => cookieStore.getAll(),
|
|
201
|
+
setAll: (cookiesToSet) => {
|
|
202
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
203
|
+
cookieStore.set(name, value, options)
|
|
204
|
+
)
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
|
211
|
+
if (!error) {
|
|
212
|
+
return NextResponse.redirect(new URL('/dashboard', request.url))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return NextResponse.redirect(new URL('/auth/error', request.url))
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Attribute Mappings
|
|
221
|
+
|
|
222
|
+
Attribute mappings definem como atributos SAML do IdP são mapeados para campos do usuário Supabase. Os dados mapeados ficam em `auth.identities.identity_data` e `auth.users.raw_user_meta_data`.
|
|
223
|
+
|
|
224
|
+
**Formato do arquivo JSON de mapping:**
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"keys": {
|
|
229
|
+
"email": {
|
|
230
|
+
"name": "mail",
|
|
231
|
+
"names": ["email", "mail", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
|
|
232
|
+
},
|
|
233
|
+
"email_verified": {
|
|
234
|
+
"default": true
|
|
235
|
+
},
|
|
236
|
+
"first_name": {
|
|
237
|
+
"name": "givenName",
|
|
238
|
+
"names": ["firstName", "givenName", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"]
|
|
239
|
+
},
|
|
240
|
+
"last_name": {
|
|
241
|
+
"name": "sn",
|
|
242
|
+
"names": ["lastName", "sn", "surname", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"]
|
|
243
|
+
},
|
|
244
|
+
"department": {
|
|
245
|
+
"name": "department",
|
|
246
|
+
"array": false
|
|
247
|
+
},
|
|
248
|
+
"groups": {
|
|
249
|
+
"name": "groups",
|
|
250
|
+
"array": true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Campos do mapping:**
|
|
257
|
+
|
|
258
|
+
| Campo | Tipo | Descrição |
|
|
259
|
+
|-------|------|-----------|
|
|
260
|
+
| `name` | string | Nome principal do atributo SAML |
|
|
261
|
+
| `names` | string[] | Aliases (Supabase tenta cada um até encontrar) |
|
|
262
|
+
| `array` | boolean | Se o atributo é multi-valor (ex: grupos) |
|
|
263
|
+
| `default` | any | Valor padrão se o atributo não vier no assertion |
|
|
264
|
+
|
|
265
|
+
**Acessar dados mapeados:**
|
|
266
|
+
|
|
267
|
+
```sql
|
|
268
|
+
-- em RLS policy ou função
|
|
269
|
+
select raw_user_meta_data->>'department' from auth.users where id = auth.uid();
|
|
270
|
+
select raw_user_meta_data->'groups' from auth.users where id = auth.uid();
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
// no cliente
|
|
275
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
276
|
+
const department = user?.user_metadata?.department
|
|
277
|
+
const groups = user?.user_metadata?.groups // array se configurado como array: true
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Contas SSO — Diferenças Importantes
|
|
281
|
+
|
|
282
|
+
Contas criadas via SSO têm comportamento diferente de contas email/senha:
|
|
283
|
+
|
|
284
|
+
| Aspecto | Conta Email/Senha | Conta SSO |
|
|
285
|
+
|---------|-------------------|-----------|
|
|
286
|
+
| Auto-linking | Supabase pode vincular por email | **Sem auto-linking** — sempre cria nova identidade |
|
|
287
|
+
| Unicidade de email | Emails são únicos por padrão | **Emails NÃO são únicos** — dois IdPs podem ter mesmo email |
|
|
288
|
+
| Identificador seguro | Email | **UUID (`auth.uid()`)** |
|
|
289
|
+
| Sessão máxima | Configurável via JWT TTL | Pode ter duração máxima definida pelo IdP |
|
|
290
|
+
| Senha | Usuário tem senha | **Sem senha** — autenticação apenas via IdP |
|
|
291
|
+
|
|
292
|
+
**Crítico:** nunca usar email como identificador primário em sistemas com SSO. Dois tenants (empresas) diferentes podem ter colaboradores com mesmo email se houver overlap de domínio — situação rara mas válida (ex: empresa A e empresa B ambas têm `joao@empresa.com` em seus diretórios).
|
|
293
|
+
|
|
294
|
+
## RLS para Multi-tenant SSO
|
|
295
|
+
|
|
296
|
+
O `sso_provider_id` no JWT identifica de qual conexão SSO o usuário veio — use como chave de isolamento de tenant:
|
|
297
|
+
|
|
298
|
+
```sql
|
|
299
|
+
-- extrair sso_provider_id do JWT
|
|
300
|
+
-- retorna o UUID do IdP (conexão SSO configurada) ou null se não for SSO
|
|
301
|
+
select auth.jwt()#>>'{amr,0,provider}' as sso_provider_id;
|
|
302
|
+
|
|
303
|
+
-- tabela de configurações por tenant SSO
|
|
304
|
+
create table public.organization_settings (
|
|
305
|
+
id uuid default gen_random_uuid() primary key,
|
|
306
|
+
sso_provider_id uuid not null unique, -- UUID do provider SSO
|
|
307
|
+
organization_name text not null,
|
|
308
|
+
plan text not null default 'pro',
|
|
309
|
+
max_users int not null default 100,
|
|
310
|
+
created_at timestamptz default now()
|
|
311
|
+
);
|
|
312
|
+
comment on table public.organization_settings
|
|
313
|
+
is 'Configurações por tenant SSO — isolamento via sso_provider_id.';
|
|
314
|
+
|
|
315
|
+
-- habilitar RLS
|
|
316
|
+
alter table public.organization_settings enable row level security;
|
|
317
|
+
|
|
318
|
+
-- política RESTRICTIVE — escopo de tenant SSO
|
|
319
|
+
create policy "Tenant SSO só vê sua própria organização" on public.organization_settings
|
|
320
|
+
as restrictive
|
|
321
|
+
for select
|
|
322
|
+
to authenticated
|
|
323
|
+
using (
|
|
324
|
+
sso_provider_id = (auth.jwt()#>>'{amr,0,provider}')::uuid
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
-- política permissiva para leitura
|
|
328
|
+
create policy "Usuários autenticados podem ler config da org" on public.organization_settings
|
|
329
|
+
as permissive
|
|
330
|
+
for select
|
|
331
|
+
to authenticated
|
|
332
|
+
using (true);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Tabela de recursos com escopo de tenant:**
|
|
336
|
+
|
|
337
|
+
```sql
|
|
338
|
+
create table public.projetos (
|
|
339
|
+
id uuid default gen_random_uuid() primary key,
|
|
340
|
+
sso_provider_id uuid not null, -- tenant owner
|
|
341
|
+
nome text not null,
|
|
342
|
+
dados jsonb,
|
|
343
|
+
criado_em timestamptz default now()
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
alter table public.projetos enable row level security;
|
|
347
|
+
|
|
348
|
+
-- escopo rígido por tenant SSO
|
|
349
|
+
create policy "Usuário vê apenas projetos do seu tenant" on public.projetos
|
|
350
|
+
as restrictive
|
|
351
|
+
for all
|
|
352
|
+
to authenticated
|
|
353
|
+
using (
|
|
354
|
+
sso_provider_id = (auth.jwt()#>>'{amr,0,provider}')::uuid
|
|
355
|
+
);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Helper function para uso em múltiplas policies:**
|
|
359
|
+
|
|
360
|
+
```sql
|
|
361
|
+
create or replace function public.sso_provider_id() returns uuid
|
|
362
|
+
language sql stable security definer set search_path = ''
|
|
363
|
+
as $$
|
|
364
|
+
select (auth.jwt()#>>'{amr,0,provider}')::uuid
|
|
365
|
+
$$;
|
|
366
|
+
|
|
367
|
+
-- uso simplificado em policies
|
|
368
|
+
create policy "Tenant isolado" on public.projetos
|
|
369
|
+
as restrictive for all to authenticated
|
|
370
|
+
using ((select public.sso_provider_id()) = sso_provider_id);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## IdP-Initiated Flow — Caveat PKCE
|
|
374
|
+
|
|
375
|
+
**IdP-initiated flow** é quando o usuário clica em um ícone no portal do IdP (ex: Okta App Dashboard) para acessar o app — sem que o SP inicie o fluxo.
|
|
376
|
+
|
|
377
|
+
**Problema:** IdP-initiated é **incompatível com PKCE** porque não há `state` do SP para validar o `code_verifier`.
|
|
378
|
+
|
|
379
|
+
**Solução — Padrão "Bookmark App":**
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// 1. Criar um endpoint que inicia o SP-initiated flow
|
|
383
|
+
// app/auth/sso-bookmark/route.ts
|
|
384
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
385
|
+
import { createServerClient } from '@supabase/ssr'
|
|
386
|
+
import { cookies } from 'next/headers'
|
|
387
|
+
|
|
388
|
+
export async function GET(request: NextRequest) {
|
|
389
|
+
const { searchParams } = new URL(request.url)
|
|
390
|
+
const providerId = searchParams.get('provider_id')
|
|
391
|
+
const tenant = searchParams.get('tenant') // para multi-tenant
|
|
392
|
+
|
|
393
|
+
if (!providerId && !tenant) {
|
|
394
|
+
return NextResponse.redirect(new URL('/auth/error', request.url))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const cookieStore = await cookies()
|
|
398
|
+
const supabase = createServerClient(
|
|
399
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
400
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
|
401
|
+
{ cookies: { getAll: () => cookieStore.getAll(), setAll: () => {} } }
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
// iniciar SP-initiated (não IdP-initiated)
|
|
405
|
+
const { data, error } = await supabase.auth.signInWithSSO({
|
|
406
|
+
...(providerId ? { providerId } : { domain: `${tenant}.empresa.com` }),
|
|
407
|
+
options: {
|
|
408
|
+
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
if (data?.url) return NextResponse.redirect(data.url)
|
|
413
|
+
return NextResponse.redirect(new URL('/auth/error', request.url))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 2. No Okta/Azure: configurar o ícone do app para apontar para:
|
|
417
|
+
// https://seu-app.com/auth/sso-bookmark?provider_id=<uuid-do-provider>
|
|
418
|
+
// (não para o ACS URL diretamente)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Multi-Subdomínio com SSO
|
|
422
|
+
|
|
423
|
+
Para apps com subdomínio por tenant (`empresa1.app.com`, `empresa2.app.com`):
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
// detectar tenant pelo subdomínio e redirecionar com callback correto
|
|
427
|
+
const iniciarSSOMultiSubdomain = async (email: string) => {
|
|
428
|
+
const dominio = email.split('@')[1]
|
|
429
|
+
const subdominio = window.location.hostname.split('.')[0] // ex: 'empresa1'
|
|
430
|
+
|
|
431
|
+
const { data, error } = await supabase.auth.signInWithSSO({
|
|
432
|
+
domain: dominio,
|
|
433
|
+
options: {
|
|
434
|
+
// callback no subdomínio correto
|
|
435
|
+
redirectTo: `https://${subdominio}.app.com/auth/callback`,
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
if (data?.url) window.location.href = data.url
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Regras absolutas
|
|
444
|
+
|
|
445
|
+
1. **SSO exige Supabase CLI v1.46.4+** — versões anteriores não têm o subcomando `sso`.
|
|
446
|
+
2. **Usar UUID (`auth.uid()`), NUNCA email, como identificador de usuário** — emails não são únicos em ambientes multi-IdP SSO.
|
|
447
|
+
3. **IdP-initiated é incompatível com PKCE** — implementar padrão "bookmark app" (endpoint que inicia SP-initiated).
|
|
448
|
+
4. **RLS RESTRICTIVE para escopo de tenant** — isolamento via `sso_provider_id` deve ser política RESTRICTIVE.
|
|
449
|
+
5. **Sem auto-linking SSO** — nunca assumir que dois registros com mesmo email são o mesmo usuário.
|
|
450
|
+
6. **Usar `@supabase/ssr`, nunca `auth-helpers-nextjs`** — pacote legado descontinuado.
|
|
451
|
+
7. **Validar JWT no servidor com `getClaims()`** — nunca confiar apenas no cliente para verificar identidade SSO.
|
|
452
|
+
|
|
453
|
+
## Anti-patterns
|
|
454
|
+
|
|
455
|
+
### Anti-pattern 1: Usar email como chave de usuário SSO
|
|
456
|
+
|
|
457
|
+
**Errado:**
|
|
458
|
+
```sql
|
|
459
|
+
-- assumindo que email é único entre tenants SSO
|
|
460
|
+
create table public.perfis (
|
|
461
|
+
email text primary key, -- ERRADO para SSO
|
|
462
|
+
dados jsonb
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
create policy "Usuário vê próprio perfil" on public.perfis
|
|
466
|
+
as permissive for select to authenticated
|
|
467
|
+
using (email = (auth.jwt()->>'email')); -- ERRADO
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Por quê:** se dois IdPs diferentes tiverem um usuário com `joao@empresa.com`, haverá conflito. UUID é o único identificador garantidamente único.
|
|
471
|
+
|
|
472
|
+
**Certo:**
|
|
473
|
+
```sql
|
|
474
|
+
create table public.perfis (
|
|
475
|
+
user_id uuid primary key references auth.users(id) on delete cascade,
|
|
476
|
+
dados jsonb
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
create policy "Usuário vê próprio perfil" on public.perfis
|
|
480
|
+
as permissive for select to authenticated
|
|
481
|
+
using (user_id = auth.uid()); -- UUID sempre único
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Anti-pattern 2: Assumir auto-linking de contas por email
|
|
485
|
+
|
|
486
|
+
**Errado:**
|
|
487
|
+
```ts
|
|
488
|
+
// assumindo que se o email já existe, é o mesmo usuário
|
|
489
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
490
|
+
const perfil = await supabase.from('perfis')
|
|
491
|
+
.select()
|
|
492
|
+
.eq('email', user.email)
|
|
493
|
+
.single()
|
|
494
|
+
// ERRADO: pode retornar perfil de outro usuário SSO com mesmo email
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Por quê:** Supabase **não faz auto-linking** por email em SSO. Cada `signInWithSSO` cria uma identidade nova em `auth.identities`, mesmo que o email já exista.
|
|
498
|
+
|
|
499
|
+
**Certo:** usar `auth.uid()` como chave de join, nunca email.
|
|
500
|
+
|
|
501
|
+
### Anti-pattern 3: Esperar IdP-initiated funcionar com PKCE
|
|
502
|
+
|
|
503
|
+
**Errado:**
|
|
504
|
+
```
|
|
505
|
+
// configurando no Okta:
|
|
506
|
+
// Ícone do app → aponta para ACS URL diretamente
|
|
507
|
+
// ACS URL: https://proj.supabase.co/auth/v1/sso/saml/acs
|
|
508
|
+
// Resultado: erro PKCE validation failed
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Por quê:** quando o IdP inicia, não há `code_verifier` do SP. O PKCE (Proof Key for Code Exchange) exige que o SP inicie o fluxo.
|
|
512
|
+
|
|
513
|
+
**Certo:** configurar o ícone do app no IdP para apontar ao "bookmark app" (endpoint SP-initiated), não ao ACS URL.
|
|
514
|
+
|
|
515
|
+
### Anti-pattern 4: Conexão SSO sem validar domínios
|
|
516
|
+
|
|
517
|
+
**Errado:**
|
|
518
|
+
```bash
|
|
519
|
+
# adicionar SSO sem especificar domains
|
|
520
|
+
supabase sso add \
|
|
521
|
+
--project-ref abc123 \
|
|
522
|
+
--type saml \
|
|
523
|
+
--metadata-url https://idp.exemplo.com/metadata
|
|
524
|
+
# sem --domains → qualquer um pode tentar signInWithSSO com qualquer domínio
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Por quê:** sem restrição de domínio, `signInWithSSO({ domain: 'qualquer.com' })` pode ser abusado para descoberta de providers.
|
|
528
|
+
|
|
529
|
+
**Certo:** sempre especificar `--domains` com os domínios exatos de email do tenant:
|
|
530
|
+
```bash
|
|
531
|
+
supabase sso add \
|
|
532
|
+
--project-ref abc123 \
|
|
533
|
+
--type saml \
|
|
534
|
+
--metadata-url https://idp.exemplo.com/metadata \
|
|
535
|
+
--domains empresa.com.br,empresa.com
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Ver também
|
|
539
|
+
|
|
540
|
+
- [supabase-auth-methods](../supabase-auth-methods/SKILL.md) — outros providers de autenticação (OAuth, magic link, email)
|
|
541
|
+
- [multi-tenant-rls-hierarchy](../multi-tenant-rls-hierarchy/SKILL.md) — RLS hierárquica para multi-tenant complexo
|
|
542
|
+
- [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — padrão `@supabase/ssr` com Next.js e cookies
|
|
543
|
+
- [supabase-custom-claims-rbac](../supabase-custom-claims-rbac/SKILL.md) — RBAC via custom claims combinado com SSO
|
|
544
|
+
- [supabase-mfa](../supabase-mfa/SKILL.md) — MFA adicional para usuários SSO (TOTP sobre aal2)
|
|
545
|
+
- [supabase-sso-saml-architect](../../agents/supabase-sso-saml-architect.md) — agente que configura SSO SAML end-to-end
|