Login social: quatro provedores (opt-in, um por um)
Um segundo caminho de entrada, ao lado da senha: os botões Entrar com… abaixo do formulário de login dos três painéis. Cada provedor nasce desligado, é ligado individualmente, e ligado faz uma coisa só — autenticar quem já tem conta.
| Provedor | Driver do Socialite | URI de redirecionamento | Como o kit confirma o e-mail verificado |
|---|---|---|---|
google |
/auth/google/callback |
campo email_verified no payload |
|
| GitHub | github |
/auth/github/callback |
o Socialite já entrega só o e-mail primary + verified, ou nada — a presença é a prova |
linkedin-openid |
/auth/linkedin-openid/callback |
campo email_verified do userinfo OpenID |
|
| X (antigo Twitter) | x |
/auth/x/callback |
o X só devolve confirmed_email — a presença é a prova |
Facebook e Discord ficaram de fora, e não por esquecimento. A seção Facebook e Discord: por que não estão aqui explica o que faltaria para incluí-los.
O que o login social faz, e o que ele deliberadamente não faz
Vale para os quatro provedores, sem exceção:
| Autentica quem já tem conta com o e-mail que o provedor devolve | ✅ sempre, quando ligado |
| Cria conta para quem não tem | ❌ só com o registro aberto ligado, que nasce desligado |
| Aceita e-mail não verificado no provedor | ❌ nunca — recusa e registra o motivo |
| Contorna o segundo fator | ❌ nunca — quem tem 2FA confirmado cai no desafio igual |
Guarda token de acesso ou refresh_token |
❌ nada é gravado |
Guarda a identidade no provedor (sub) |
✅ em vinculos_sociais — é por ela que a conta é reconhecida da segunda vez em diante (detalhes) |
Cria coluna nova em users |
✅ uma, origem — só diz por qual porta a conta entrou (google, github, convite, registro, interno), exibida na lista de usuários e no dashboard. Não é vínculo: nada de id do provedor nem token; o vínculo continua sendo o e-mail verificado |
| Marca a conta criada como e-mail verificado | ✅ sim — o provedor já provou, e pedir de novo seria a mesma prova duas vezes |
A linha que mais importa é a segunda, e ela não é timidez: o convite é a única porta de entrada
do kit. O exemplo que a documentação do Laravel Socialite dá para o callback é
User::updateOrCreate() — copiado para cá, ele transformaria qualquer pessoa com uma conta em
qualquer um dos provedores em usuária do sistema, contornando convite, verificação e atribuição
de papel. Isso é furo de autorização, não conveniência. Se você quer cadastro por login social,
ligue o registro aberto: o kit passa a criar a conta e a levar a pessoa para a tela do perfil dela,
onde ela completa o que falta.
E lembre do resto do kit: conta sem papel não abre painel nenhum (User::canAccessPanel()).
Quem entra por login social precisa de papel como qualquer outra pessoa — a conta criada pelo
registro aberto recebe o papel único dele, pela mesma porta do formulário.
A conta criada pelo provedor não tem senha que a pessoa conheça (nasce com uma aleatória), e
três coisas pedem a senha atual: trocar a senha, ligar o 2FA e desbloquear a sessão. Por isso o
perfil (/app/meu-perfil, e o dos outros dois painéis) tem o bloco Definir senha por e-mail:
ele envia o mesmo link do “Esqueceu a senha?”, encerra a sessão — a página que define a senha só
abre para quem está fora — e, com a senha definida, os três passam a funcionar. Medido numa
instalação real: era o primeiro tropeço de quem entrava pelo Google. E quem escolhe viver sem
senha local não fica preso na tela de bloqueio de sessão: ela oferece os mesmos botões do
login, e a volta do provedor destrava.
Ligando um provedor, em quatro passos
O roteiro é o mesmo para os quatro; só muda onde se cria o app OAuth. Você pode fazer tudo pelo
.env ou pela tela /admin/configuracoes-da-aplicacao → aba Login — mas saiba quem manda: o
banco vence o .env em tempo de execução, e o .env só semeia (ver
Quem manda: o banco ou o .env?). O passo 3 é onde isso pesa.
1. Crie o app OAuth no provedor e cadastre a URI de redirecionamento — que é o seu APP_URL
mais o caminho da tabela acima:
| Provedor | Onde criar | O que pedir lá |
|---|---|---|
| console.cloud.google.com → APIs e serviços → Credenciais → ID do cliente OAuth, tipo Aplicativo da Web | nada além do padrão | |
| GitHub | github.com/settings/developers → OAuth Apps → New OAuth App | nada a marcar; o kit pede o escopo user:email no código, e é ele que permite confirmar a verificação |
| linkedin.com/developers → Create app → aba Products | habilite o produto Sign In with LinkedIn using OpenID Connect. Sem ele o provedor não devolve email_verified, e o kit recusa todo login |
|
| X | developer.x.com → Projects & Apps → User authentication settings | tipo Web App, OAuth 2.0, e os escopos users.read e users.email |
Exemplo de URI a cadastrar:
https://seu-dominio.com.br/auth/github/callbackhttp://localhost:8000/auth/github/callback # para desenvolvimentoEsse caminho não é escolha: ele está em config/services.php como caminho relativo, de
propósito, para acompanhar o APP_URL de cada ambiente sem uma variável a mais para esquecer.
2. Escreva as três chaves do provedor no .env:
# GoogleKIT_SOCIALITE_GOOGLE=trueGOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.comGOOGLE_CLIENT_SECRET=GOCSPX-seu-segredo
# GitHubKIT_SOCIALITE_GITHUB=trueGITHUB_CLIENT_ID=Iv1.abc123GITHUB_CLIENT_SECRET=seu-segredo
# LinkedIn (driver linkedin-openid)KIT_SOCIALITE_LINKEDIN=trueLINKEDIN_CLIENT_ID=86abc123LINKEDIN_CLIENT_SECRET=seu-segredo
# X (antigo Twitter)KIT_SOCIALITE_X=trueX_CLIENT_ID=seu-client-idX_CLIENT_SECRET=seu-segredo3. Leve as chaves ao banco. Numa instalação nova o migrate faz isso sozinho — a
migration de settings semeia cada propriedade do config(), que vem do .env. Num kit já
instalado, o .env sozinho não liga nada, e config:clear não muda isso: a tabela
settings já tem a linha (false, credencial vazia) e ela vence em todo request. Aí são dois
caminhos: gravar pela tela /admin/configuracoes-da-aplicacao → Login, ou
php artisan kit:install --force com o .env já preenchido — que recria o banco (APAGA os
dados; inócuo só no minuto seguinte à instalação). Medido numa instalação real: o .env com as
três chaves do Google, config:clear, e nenhum botão — até a migration reler o .env.
4. Confirme que o botão apareceu. Se não apareceu, é uma das duas condições abaixo.
Pela tela, em vez do
.env: em/admin/configuracoes-da-aplicacao→ Login há uma seção por provedor. Ligar o interruptor abre os campos de Client ID e Client Secret daquele provedor — e só dele. O Client Secret é guardado cifrado, nunca é exibido de volta e não aparece no código-fonte da página; deixar o campo em branco mantém o que estava gravado.
As telas
Vínculo com o provedor: a primeira vez, e as seguintes
A pergunta que motivou esta seção: “eu poderia criar uma conta no Google com o e-mail de outra pessoa e entrar na conta dela?” Não — e vale entender por quê antes de ler o que o kit faz a mais.
O kit só aceita e-mail que o provedor declara verificado (tabela acima). Google, GitHub, LinkedIn e X só marcam um e-mail como verificado depois de mandar um código ou link para aquela caixa postal. Então quem consegue uma identidade “verificada” com o e-mail de outra pessoa já controla a caixa postal dela — e quem controla a caixa postal já entra pelo “Esqueceu a senha?” do próprio kit. O login social não abre uma porta que não existisse: ele aceita a mesma prova. É o modelo que o Auth0 chama de trusted providers.
Sobram dois riscos residuais, e são eles que o vínculo endereça: um endereço reciclado pelo provedor de correio (o novo dono verifica o endereço no Google e chegaria à conta do antigo — mas também resetaria a senha dele), e um bug ou comprometimento do provedor OAuth.
O vínculo. Toda entrada social grava, na tabela vinculos_sociais, a identidade da pessoa
no provedor — o sub, o id da conta lá, estável mesmo quando o e-mail muda — junto da conta do
kit. Nada de token: é reconhecimento, não credencial. Da segunda entrada em diante a conta é
reconhecida pelo vínculo, antes de olhar o e-mail: uma troca de e-mail no provedor, ou um
endereço reciclado, não leva a outra conta.
A primeira vez. Quando um provedor aparece pela primeira vez numa conta que já existia, o
que acontece depende de um interruptor — KIT_SOCIALITE_VINCULO_CONFIRMAR no .env, ou a tela
/admin/configuracoes-da-aplicacao → Login → “Exigir confirmação por e-mail…”:
Modo padrão (false) |
Modo estrito (true) |
|
|---|---|---|
| Conta existe, primeira vez deste provedor | vincula, entra, e envia o e-mail “sua conta foi acessada pelo Google pela primeira vez — não foi você? troque a senha e avise quem administra” | não entra; envia o e-mail “confirme a entrada pelo Google” com um link assinado de 30 minutos; ao abri-lo, o vínculo nasce e a sessão começa |
| Conta existe, já vinculada | entra pelo vínculo, sem e-mail | idem |
| Conta não existe, registro aberto ligado | cria pela porta do registro aberto e já nasce vinculada — não há conta anterior a proteger | idem |
| Conta não existe, registro fechado | recusa (“o acesso é por convite”) | idem |
O aviso do modo padrão é detecção: torna o risco residual visível para a própria pessoa. O modo estrito é prevenção: exige a prova (a caixa postal) no exato momento em que ela importa. O link de confirmação vale só para aquela conta e aquela identidade, é assinado, expira, e se a identidade já for de outra conta a confirmação recusa — uma identidade de provedor pertence a uma conta só.
Os dois e-mails vão pela fila (
ShouldQueue, como o convite). Sem um worker rodando nada sai —composer devsobe um. Na validação real isso foi o primeiro tropeço: o aviso “link enviado” apareceu, e o e-mail ficou na tabelajobsaté oqueue:work.
Cadastro pelo provedor, a partir da tela de registro. /app/register mostra os mesmos botões
(com o registro aberto ligado), e o clique carrega o contexto da tela até a volta do OAuth: com a
multi-organização, /app/register?org=acme cria a conta na acme, com o papel do registro
aberto nela — a mesma porta do formulário, com as mesmas recusas (organização inexistente ou
fechada). A partir do link de um convite (?token=), entrar pelo provedor cria a conta pelo
convite: ela nasce com a organização e o papel do convite, e o convite é consumido — desde que o
e-mail verificado do provedor seja o e-mail convidado; se for outro, recusa e o convite fica intacto.
Sem senha em nenhum dos casos: o provedor provou o e-mail. Conta nova sem ?org= na
multi-organização continua recusada.
Conta existente entra normalmente em qualquer modo, mas não consome convite nesse caminho: o ?token= viaja numa rota GET pública sem
CSRF, e com SSO silencioso do provedor o aceite aconteceria sem clique da pessoa. Quem já tem conta
aceita o convite na tela autenticada Convites recebidos, que exige o dono e pede confirmação.
Auditoria e decisão em
wikis/specs/feat/travas-de-escalada-de-papeis/ (F-03 e F-04).
Decisões e casos: wikis/specs/feat/cadastro-social-por-convite-e-organizacao/.
Decisões e casos: wikis/specs/feat/vinculo-de-provedor-social/.
O botão só aparece com TUDO preenchido — e por provedor
São três condições, em conjunção, e elas falham por motivos diferentes:
- o interruptor daquele provedor ligado — desligado é escolha de quem instalou;
- o painel atual (
app,adminouinfra) autorizado para aquele provedor — lista vazia significa todos, preservando instalações anteriores; - o
client_id, oclient_secrete oredirecttodos preenchidos — credencial vazia é descuido de quem configurou.
Interruptor ligado com o client_secret vazio mantém o botão fora do ar, e é de propósito: botão
que leva a um OAuth inexistente é uma promessa que a tela não pode cumprir.
E o desligamento derruba a ROTA, não só o botão. Com um provedor indisponível,
/auth/{provedor}/redirect e /auth/{provedor}/callback respondem 404 — e só os dele, os
outros seguem no ar. Esconder o botão não seria barreira nenhuma: a URL é fixa, pública e conhecida.
Provedor fora da lista responde 404 sem nem chegar ao controller. /auth/facebook/callback,
/auth/discord/callback ou qualquer outro segmento devolvem 404 porque o parâmetro da rota é
tipado como App\Support\ProvedorSocial — a lista branca é o próprio enum, e o roteador a consulta.
Cada interruptor também falha fechado: false, 0, off, no, vazio e qualquer valor
irreconhecível o mantêm desligado. Só true e 1 ligam.
Cada provedor escolhe seus painéis
Na tela /admin/configuracoes-da-aplicacao → Login, o campo Painéis permitidos de cada provedor
controla separadamente onde seu botão e suas rotas ficam disponíveis: /app, /admin e /infra.
Lista vazia significa todos os painéis, preservando o comportamento das instalações anteriores.
A barreira vale no servidor, não apenas na interface. Alterar a URL manualmente para iniciar ou
concluir o OAuth por um painel não autorizado retorna 404. O painel de origem é mantido na
sessão durante a ida ao provedor, inclusive quando o /app usa multi-tenancy.
O destino respeita o painel de origem
Quem inicia o login social em /admin/login volta ao /admin; quem inicia no /infra/login volta
ao /infra; e quem inicia no /app/login volta ao /app — ou à organização correta quando a
multi-tenancy está ligada. Uma recusa também retorna à tela de login de origem.
A autenticação não ignora autorização: depois do callback, User::canAccessPanel() continua sendo
a barreira final. Ter um provedor habilitado em um painel não concede acesso a esse painel.
O rodapé da tela de login
A mesma configuração traz um rodapé em Markdown (negrito, itálico, link; HTML cru é descartado) na base da tela de login dos três painéis:
KIT_LOGIN_RODAPE="Acme · Todos os direitos reservados"Vazio (ou só espaços) = sem rodapé, sem faixa vazia.
É texto, não HTML, e o valor sai escapado. A tela de login é pública e não autenticada: HTML cru vindo de um campo editável ali seria XSS armazenado com o pior alcance possível — a tela por onde todo mundo entra. Se você precisar de link no rodapé, o caminho é um campo estruturado (texto + URL, com validação), não um campo de HTML solto.
E-mail não verificado: por que recusamos, e como cada provedor prova
O vínculo com a conta do kit é feito pelo e-mail, comparado sem diferenciar caixa nem espaços nas bordas. Isso é simples e não custa coluna nova — mas tem um risco conhecido: se o provedor devolvesse um e-mail não verificado, bastaria criar uma conta naquele provedor com o e-mail de outra pessoa para entrar na conta dela. Com o registro do kit fechado — o default — esse é justamente o caminho principal, não um caso de borda.
Então o kit exige prova positiva em todos os provedores. Ausente, falsa, ou com um valor que
não seja claramente verdadeiro ⇒ recusa, com aviso na tela e o motivo email_nao_verificado no
log. Falha fechado, sempre.
O que muda de provedor para provedor é onde a prova está, e a diferença é grande:
-
Google — informa
email_verifiedno payload (e um aliasverified_email). O kit lê e exige verdadeiro. -
LinkedIn — o userinfo do OpenID Connect traz
email_verified. É por isso que o kit usa o driverlinkedin-openide não olinkedinlegado: o legado não informa verificação nenhuma, e os escopos dele foram descontinuados pela própria LinkedIn. -
X — não tem campo de verificação, porque não precisa: o X só devolve
confirmed_email, ou seja, um endereço que ele já confirmou. A presença do e-mail é a prova. Se o X não devolver e-mail (app sem o escopousers.email, ou conta sem endereço confirmado), o kit recusa com o motivoemail_ausente. -
GitHub — a presença também, e por um caminho que vale entender antes de mexer nos escopos. O Socialite consulta
/user/emails, escolhe a entradaprimaryeverified, e sobrescreve o e-mail com ela — ou com nada, se a consulta falhar ou se nenhuma entrada casar. Não há campo de verificação no payload, e não precisa: e-mail preenchido já significaprimary+verified, e e-mail vazio cai na recusa poremail_ausente.Isso depende de uma condição: o escopo
user:email. Ele é o default do driver, e a chavescopesdeconfig/services.phpsoma aos defaults em vez de substituí-los, então acrescentar escopos é seguro. O que quebraria a garantia é umsetScopes()no código do kit removendouser:email— aí o GitHub passaria a entregar o e-mail do perfil público, não verificado, em silêncio. Há caso de teste guardando exatamente isso; não desligue esse escopo.Versões anteriores desta seção diziam que o kit refazia a consulta a
/user/emailspor conta própria. Ele fazia, e era redundante: a leitura do código do Socialite que justificava a chamada estava errada. A chamada foi removida — o kit não chama API de provedor nenhuma.
Consequência a conhecer, em todos: se a pessoa trocar o e-mail na conta do provedor, o vínculo se perde e ela volta a entrar por senha.
Limitação conhecida: o destino é sempre o painel /app
Resolvida na v0.29.0: o destino agora respeita o painel de origem, como descrito acima.
Facebook e Discord: por que não estão aqui
O requisito original pedia os dois. Nenhum entrou, e cada um por um motivo diferente.
Facebook — não há como confirmar o e-mail. O Socialite tem o driver, e ele funciona; o que não
existe é um campo que afirme que aquele endereço foi confirmado. O verified que o provider
pede é de nível de conta, legado, e ausente na versão da Graph API que ele usa; o caminho
OIDC/Limited Login devolve claims sem email_verified. Aceitar o Facebook faria o nível de garantia
do seu login depender de qual botão a pessoa clicou — e o botão mais fraco seria o vetor. Se
você aceitar esse risco conscientemente, o que falta é: um caso em App\Support\ProvedorSocial com
o ramo de verificação declarando a premissa, o bloco em config/services.php (chave facebook) e
em config/kit.php, e as três propriedades no Settings. Leia o ADR-05 antes — ele lista as
alternativas que foram consideradas e por que cada uma é pior.
Discord — não é driver do Socialite. A documentação oficial suporta Facebook, X, LinkedIn,
Google, GitHub, GitLab, Bitbucket e Slack; o resto vem do catálogo comunitário
socialiteproviders.com. Incluí-lo exige
composer require socialiteproviders/discord e o registro de um listener de SocialiteWasCalled
— uma dependência nova e um segundo mecanismo de extensão. O kit não adiciona dependência por
conta própria; se você quiser, o caminho é esse mais um caso no enum (o Discord expõe um campo
verified no payload, então a barreira tem onde encostar).
Onde ficam os registros
Tudo no channel autenticacao (storage/logs/autenticacao-*.log), no mesmo formato do resto
do kit — [Classe@Método] mensagem | chave: valor, com e-mail mascarado, o provedor em todas
as linhas e um motivo legível em cada recusa:
motivo |
O que aconteceu |
|---|---|
falha_no_provedor |
state de CSRF inválido, rede fora, ou credencial recusada pelo provedor |
email_ausente |
o provedor não devolveu e-mail (no X, é o caso de escopo faltando) |
email_nao_verificado |
o e-mail não está verificado no provedor |
conta_inexistente_registro_fechado |
não há conta e o registro aberto está desligado |
conta_criada_por_login_social |
conta nova criada (registro aberto ligado) |
Nenhum client_secret aparece — não em log, não em tela, não em mensagem de erro, e não no
HTML da tela de configuração. E as mensagens devolvidas ao visitante são propositalmente genéricas:
dizer qual barreira reprovou é entregar informação de reconhecimento a quem estiver sondando. O
motivo fica no log, para você.
O login social também entra na trilha de acesso do painel /infra (quem entrou, quando, de
onde), como qualquer outro login — sem configuração nenhuma.
O segredo do Google ficou em claro na trilha de auditoria até a v0.19.3
Se você configurou o GOOGLE_CLIENT_SECRET pela tela /admin/configuracoes-da-aplicacao em alguma
versão entre a 0.19.2 e a 0.19.3, rotacione esse segredo no console do Google.
O motivo: a máscara de segredo da trilha de auditoria decide o que esconder consultando a lista
ConfiguracoesDoKit::encrypted(), e o client_secret do Google estava fora dessa lista. Então
cada gravação pela tela escreveu o valor em claro nas colunas old_values/new_values da
tabela audits — e a tela de auditoria exibe essas colunas para leitura.
O que esta versão faz por você:
- corrige a lista, o que fecha o vazamento daqui para a frente nos quatro segredos de provedor e na senha de SMTP, de uma vez (uma lista, três consumidores: o decifrador da leitura, o cifrador da gravação e a máscara da trilha);
- mascara o que já está gravado, numa migration que substitui o valor pela mesma máscara que a trilha usa hoje. A linha da trilha é preservada — quem alterou, quando e de onde continua registrado; sai só o valor que nunca deveria ter entrado;
- avisa no log (channel
configuracoes) quantas linhas foram mascaradas, com a instrução de rotacionar.
Mascarar a trilha não desfaz o fato de o valor ter estado legível. Por isso a rotação é sua, e é o único passo que o kit não pode fazer no seu lugar.
Acrescentando o próximo provedor
O kit tem abstração de provedor agora, e ela é um enum: App\Support\ProvedorSocial. A decisão
foi tomada com quatro casos na mão, não com um — o que revelou que o eixo a abstrair não era o
redirect nem o botão (idênticos em todos), mas a verificação de e-mail (radicalmente diferente
em cada um).
O roteiro do quinto provedor, e é curto de propósito:
- um caso novo no enum, com o
value= nome do driver do Socialite (esse mesmo valor é o segmento da URL e a chave nos dois arquivos de config), mais os ramos derotulo(),icone()eemailVerificado(); - um bloco em
config/services.phpe um emconfig/kit.php→login, e as chaves no.env.example; - três propriedades em
App\Settings\ConfiguracoesDoKit, a linha de cada uma emmapaDeConfiguracao(), oclient_secretemencrypted(), e o paradd/addEncryptednuma migration nova emdatabase/settings/; - uma partial de SVG em
resources/views/filament/auth/icones/.
Nenhum arquivo de lógica muda: as rotas, o controller, o blade dos botões e a aba Login da tela
de Settings percorrem ProvedorSocial::cases(). E o match exaustivo de emailVerificado()
cobra o ramo novo — esquecê-lo não compila em análise estática.
Se o provedor não permitir confirmar a verificação do e-mail, essa é uma decisão de arquitetura,
não um ?? true. Foi o que tirou o Facebook da lista. Registre a escolha antes de escrever o
ramo.
O raciocínio completo, com as alternativas recusadas e o
file:linedovendor/de cada afirmação sobre o Socialite, está emwikis/specs/feat/mais-provedores-sociais/mais-provedores-sociais/. A decisão anterior — a de não abstrair, com um provedor só — está emwikis/specs/feat/login-social-google/login-social-google/, ADR-10.