A página de docs de todo provedor termina no mesmo lugar: um botão, um usuário, pronto. As perguntas começam na semana seguinte. Um convidado que jogou por uma hora quer manter o progresso, então entra com o Google. Alguém cria conta com senha e depois clica no botão do Google com o mesmo e-mail. Um celular se perde e a sessão dele precisa morrer.
Este post é a semana depois da página de docs. Uma conta, várias formas de entrar, num backend Parse hospedado no Back4app, sem servidor de auth próprio: o navegador fala com o backend, o backend fala com o Google. Tudo foi executado em 20 e 23 de outubro de 2026, contra a plataforma ao vivo, e os códigos e textos abaixo são citados das respostas dela.
Teste, faça o deploy, leia: a demo roda em socialloginstarter-6om29ktc.b4a.run com um botão do Google de verdade, login como convidado e e-mail/senha. Contas criadas lá podem ser apagadas a qualquer momento.
O repositório é github.com/templates-back4app/social-login-starter, MIT.
O que “vincular” significa num backend Parse
Um usuário Parse tem uma linha em _User, e essa linha pode carregar várias credenciais ao mesmo tempo: um username e uma senha, e um objeto authData com uma chave por provedor. Entrar com qualquer uma delas resolve para a mesma linha.
Não há tabela de “identidades” separada nem etapa de merge. Vincular é um PUT no seu próprio usuário com uma nova chave em authData, e desvincular é o mesmo PUT com essa chave definida como null.
Esse é o modelo inteiro, e a tabela abaixo é o protocolo inteiro. As linhas de convidado, senha, desvínculo e logout foram exercitadas com chamadas REST cruas contra o backend ao vivo; as duas linhas do Google descrevem o que o Parse Server faz com um token válido, o que esta rodada só verificou pelo lado da recusa, com tokens forjados.
| Ação na interface | O que o navegador envia | O que o Parse Server faz |
|---|---|---|
| Continuar como convidado | POST /users {authData:{anonymous:{id}}} | cria um usuário (201) ou loga o mesmo convidado de novo (200); nova sessão nos dois casos |
| Entrar com Google | POST /users {authData:{google:{id, id_token}}} | verifica o ID token junto ao Google, depois cria ou loga (caminho documentado; tokens forjados recusados, veja a tabela de segurança) |
| Criar conta / Entrar | POST /users {username,password,email} / POST /login | o caminho clássico |
| Vincular e-mail/senha | PUT /users/<id> {username,password,email} com a sessão | define a senha, responde com um novo session token e revoga os antigos, mantém o id de convidado |
| Vincular Google | PUT /users/<id> {authData:{google:{…}}} | verifica o token, vincula; um id já usado em outra conta responde 208 (documentado; não executado com uma conta real) |
| Desvincular Google ou id de convidado | PUT /users/<id> {authData:{google:null}} | remove o provedor; a sessão continua válida; remover a última forma de entrar é permitido pelo Parse Server e recusado pelo hook do repositório |
| Sair | POST /logout | encerra só esta sessão |
| Sair de todos os dispositivos | Cloud function signOutEverywhere | apaga toda outra linha de _Session do chamador (não é nativo) |

A coisa toda em três requisições
Antes dos detalhes, aqui está o fluxo de vínculo como o navegador o envia, com o Parse JS SDK. Cada chamada é uma requisição REST, e nada mais é necessário para ir de um convidado a uma conta com três formas de entrar.
// 1. Start as a guest: POST /users with an anonymous id the SDK generates and keeps
const user = await Parse.AnonymousUtils.logIn();
// 2. Link e-mail and password: PUT /users/<id> with the current session.
// The answer carries a NEW session token; every previous session of this user is revoked.
user.set('username', 'jane.doe.acme');
user.setPassword('a-long-passphrase');
user.setEmail('[email protected]');
await user.save();
// 3. Link Google: the Google Identity Services button hands you an ID token; PUT it under authData.
// Parse Server verifies the signature and audience against the client id you configured.
await user.save({ authData: { ...(user.get('authData') || {}), google: { id: payload.sub, id_token: credential } } });
// Unlink: the same PUT with null. The hook refuses to remove the last method.
await user._unlinkFrom('anonymous');
O passo 3 usa save() puro em vez do linkWith() do SDK, e o passo 2 usa set('username') em vez de setUsername(), por um motivo que as próximas seções medem: os dois helpers descartam o id de convidado em silêncio. Tudo depois deste ponto é o que acontece ao redor dessas chamadas: colisões, sessões, as regras que faltam no servidor e os números.
Para quem é este post
Você tem um provedor funcionando a partir de uma página de docs e precisa do resto do ciclo de vida: um segundo método na mesma conta, colisões, o que expira quando e como encerrar sessões. Você aceita que o backend seja o dono da identidade e não quer rodar um servidor OAuth.
Duas coisas ficam fora do escopo de propósito. O app não tenta manter “um usuário por pessoa” entre provedores: uma conta Google e uma conta com senha com o mesmo e-mail são dois usuários, a menos que o usuário logado as vincule, e o post explica por quê mais abaixo.
Roles, edição de perfil e SSO pertencem a outros posts; o post sobre cadastro e recuperação de senha cobre o lado do e-mail.
Os provedores, e quais deles este post executa
A dashboard do Back4app lista cinco provedores sociais em Authentication: Apple, Google, Facebook, X e VK. Os nomes no Parse são apple, google, facebook, twitter e vkontakte. A demo roda o Google, mais os dois provedores que não precisam de console nenhum: convidados anônimos e e-mail/senha.
| Provedor | Neste post | Por quê |
|---|---|---|
| ao vivo na demo | um console gratuito, um Web client id, sem revisão para os escopos básicos | |
| Convidado anônimo | ao vivo na demo | nativo do Parse Server; o navegador guarda um id aleatório |
| E-mail/senha | ao vivo na demo | nativo; o post anterior cobre os e-mails de redefinição |
| GitHub | não disponível | não é oferecido pelo Back4app; authData.github respondeu HTTP 500 “Internal server error” no backend ao vivo |
| Apple | documentado, não executado | precisa de uma conta de desenvolvedor paga e de um domínio verificado |
| documentado, não executado | login público exige App Review; o modo de desenvolvimento permite só usuários de teste (página de App Review da Meta, lida em outubro de 2026) | |
| VK | documentado, não executado | o adapter respondeu “Vk only works with enableInsecureAuth: true” a uma sondagem, então exige uma configuração de servidor que não perseguimos |
A linha do GitHub merece uma frase, porque a rodada de viabilidade de 20 de outubro descobriu isso do jeito difícil. Um cliente que envia authData.github não recebe um erro limpo de “provedor não suportado”; o adapter do Parse Server falha antes, e a resposta é um 500.
O hook beforeSave do repositório mantém uma lista de permitidos com os cinco provedores mais anonymous, de modo que um provedor ligado na dashboard por acidente ainda não consegue criar um usuário até que a lista seja alterada de propósito.
A página, o hook e as duas funções
O cliente é uma página HTML, uma folha de estilo e 206 linhas de JavaScript, servidas por um app Express de 65 linhas cujas únicas tarefas são arquivos estáticos, um /config.js com as chaves públicas e um /healthz. O Parse JS SDK e o Google Identity Services são carregados das suas CDNs.
O botão do Google entrega à página um ID token, um JWT assinado pelo Google. A página decodifica o payload para mostrar o e-mail e então chama Parse.User.logInWith('google', { authData: { id, id_token } }). Isso é um POST /users; o servidor busca as chaves públicas do Google, confere a assinatura e a audience contra o client id da dashboard, e só então toca no banco de dados.
O arquivo de Cloud Code tem um hook e duas funções, e cada um existe por um motivo que o servidor não cobre:
beforeSave(_User)recusa mudanças de identidade por qualquer um que não seja o dono ou a master key, mesmo quando a ACL da linha foi alargada por engano; mantém a lista de provedores permitidos; e mantém um booleanohasPassword. O Parse nunca diz a um cliente se um usuário tem senha, nem com a master key, mas o hook roda antes do hashing e vê o texto puro, então consegue definir a flag. Um cliente que enviahasPassword: truetem o valor sobrescrito.linkInfodevolve o que o painel precisa e o SDK não consegue ver: a flag acima, ocreatedAte oexpiresAtda sessão atual, e quantas sessões a conta tem.signOutEverywhereapaga toda linha de_Sessiondo chamador exceto a que fez a chamada, com a master key, e devolve a contagem.
O ciclo de vida, um resultado registrado por caso
Cada linha abaixo é um teste em test/lifecycle.test.mjs, executado contra o backend ao vivo em 23 de outubro de 2026, como chamadas REST cruas com a REST key. Nada na tabela é previsão; a suíte imprime os códigos e os textos, e são estes.
| Caso | O que o backend ao vivo fez |
|---|---|
| Novo convidado, depois o mesmo convidado de novo | 201 com um usuário novo, depois 200 com o mesmo objectId e um novo session token |
| Convidado vincula username, e-mail e senha | 200; a resposta trouxe um novo session token; o token antigo passou a responder 209 “Invalid session token”; authData.anonymous mantido |
| Uma segunda sessão de convidado aberta durante o vínculo | revogada também: 209 |
| Entrar com a senha vinculada | 200, mesmo objectId |
| Outro usuário pega o mesmo username | 400, código 202 “Account already exists for this username.” |
| Outro usuário pega o mesmo e-mail | 400, código 203 “Account already exists for this email address.” |
| Desvincular o id de convidado de um usuário com senha | 200; a sessão continuou válida; o id de convidado antigo então criou um usuário novo |
| Desvincular o último método de um convidado sem senha | 200 do Parse Server sozinho em 23 de outubro, conta órfã; 400, código 119 “Cannot remove the last sign-in method” com o hook no ar em 27 de outubro |
| Sair no dispositivo 2 | dispositivo 1 continua válido, dispositivo 2 responde 209 |
| Trocar a senha por uma sessão | 200 com um token novo; o token antigo do chamador e toda outra sessão respondem 209 |
| Sair de todos os dispositivos | a função revogou as outras e o chamador continuou funcionando |

Três dessas linhas mudam como você escreve o cliente.
Vincular uma senha substitui a sessão. A resposta ao PUT traz um token novo, e toda sessão anterior daquele usuário some, outros dispositivos incluídos. O SDK troca o token na aba atual; outra aba aberta descobre isso na próxima requisição, quando recebe um 209.
Na captura acima, o createdAt da sessão passou de 13:17:13 para 13:18:03 na mesma conta. A página mostra esse timestamp exatamente por esse motivo.
Colisões são trabalho do backend, e ele diz isso em palavras. A demo mostra o texto do próprio backend ao lado do código. Na captura abaixo, um segundo convidado tentou vincular o e-mail que a primeira conta já possui.

O Parse Server permite deixar uma conta órfã, então o hook precisa impedir. Um convidado sem senha que desvincula o id de convidado fica com uma sessão funcionando e uma conta que nenhuma credencial alcança de novo; registramos HTTP 200 para essa requisição em 23 de outubro.
A primeira versão deste post deixou a proteção para a interface, que desabilita o último botão de desvínculo. Uma regra de interface não é regra quando o mesmo post mostra chamadas REST diretas.
O hook agora recusa uma atualização sem master key que deixaria a conta sem provedor e sem senha: a mesma requisição responde 400, código 119, Cannot remove the last sign-in method: link another one first, e a sessão e a conta ficam intactas. Vincule uma senha antes e o desvínculo passa.
O teste que antes documentava a conta órfã agora afirma a recusa, localmente e no backend ao vivo.
O Parse cumpre toda promessa sobre quem pode alterar uma linha. Se a linha continua alcançável é uma regra que você acrescenta, e o lugar dela é no servidor.
Por que não fundir contas pelo e-mail
Um login com Google cujo e-mail coincide com uma conta com senha existente é um usuário novo no Parse, e este post mantém assim. Fundir pelo e-mail significaria confiar que quem controla aquela conta Google também controla a conta com senha, e a única prova disso é entrar nas duas.
É isso que vincular a partir de uma sessão logada é: o usuário prova a primeira conta por estar nela, depois prova a segunda ao entrar nela.
A consequência prática é uma linha de interface. Quando um cadastro com senha falha com 203 porque um usuário do Google já possui aquele e-mail, diga ao usuário para entrar com o Google e vincular uma senha pelo painel. A demo mostra o texto do backend; seu app pode mostrar a frase melhor.
Sessões: dois relógios, e o que as encerra
Um session token neste backend foi emitido com expiração um ano depois da criação: o expiresAt no registro armazenado é createdAt mais 365 dias, em toda sessão que criamos.
O ID token do Google é um relógio separado. Ele é consumido uma vez, no login ou no vínculo, e o backend o guarda em authData mas nunca mais precisa dele para atender uma requisição. A expiração desse token não desloga ninguém.
O que encerra uma sessão, pela tabela acima: POST /logout para a sessão que chama; uma senha definida ou trocada por uma sessão, para todas elas; e signOutEverywhere para todas menos a do chamador. Nada mais no ciclo de vida tocou em _Session. Desvincular um provedor não tocou.
Testes de segurança, como requisições cruas
Um cliente no navegador tem o Application ID e a JavaScript key, e os dois são públicos. Os testes em test/security.test.mjs enviam o que um cliente hostil enviaria, direto para a REST API com essas chaves, e o backend ao vivo respondeu:
| Tentativa | Resposta do backend ao vivo |
|---|---|
GET /users/me sem sessão | 400, código 209 |
GET /users sem sessão | 404, código 101 “Permission denied, user needs to be authenticated.” |
GET /users com sessão | 200, e a lista contém só a linha do próprio chamador |
GET /users/<other> com sessão | código 101, como se a linha não existisse |
PUT /users/<other> {username} com a sessão de outro usuário | 400, código 206 “Insufficient auth.” |
| o mesmo, depois que a ACL da vítima foi alargada para escrita pública | recusado pelo hook: 119 “Only the account owner can change its sign-in methods.” |
PUT no próprio usuário com hasPassword: true | 200, e a flag continua false |
authData.google com um token que não é JWT | 404, código 101 “provided token does not decode as JWT” |
authData.google sem id_token | 404, código 101 “id token is invalid for this user.” |
authData.google com um JWT RS256 autoassinado | 404, código 101 “secret or public key must be provided”, depois que o servidor buscou as chaves do Google e não encontrou um key id correspondente |
authData.github | 500, código 1 “Internal server error.” |
| qualquer Cloud function sem sessão | 400, código 209 |
A linha do token autoassinado é a que prova o caminho de verificação: o servidor foi ao endpoint de certificados do Google, procurou o key id do header forjado e recusou quando não havia nenhum. Um token forjado com um key id real do Google falharia na verificação de assinatura.
Duas configurações tornam o resto da tabela verdadeiro, e npm run setup aplica as duas por uma Cloud function, para que nenhum script precise da master key.
Usuários novos recebem uma ACL só do dono, e as permissões por classe de _User exigem sessão para find, get, update e delete e escondem email, authData, hasPassword e emailVerified de todos menos o dono.
Os campos protegidos importam mesmo que você nunca alargue uma ACL: sem eles, um usuário logado listando _User veria os e-mails dos outros usuários.
Latência, medida
npm run measure cria seus próprios usuários, roda 20 aquecimentos e 100 requisições cronometradas por cenário, e apaga o que criou. O cliente foi uma máquina fora dos EUA; os arquivos estão em docs/evidence/.
| Cenário (backend ao vivo, 23 de outubro de 2026) | p50 | p95 | mín | máx | erros |
|---|---|---|---|---|---|
Novo login como convidado (POST /users, id anônimo novo) | 160 ms | 430 ms | 150 ms | 457 ms | 0 |
Convidado que volta (POST /users, id anônimo conhecido) | 164 ms | 248 ms | 144 ms | 267 ms | 0 |
Login por e-mail (POST /login) | 228 ms | 288 ms | 215 ms | 313 ms | 0 |
Criar um usuário custa mais na cauda do que logar um: o p95 da primeira linha é o único outlier do conjunto. A linha do e-mail é cerca de 60 ms mais lenta que as linhas de convidado; a rodada local abaixo aponta o bcrypt como motivo, embora a rodada ao vivo não consiga separá-lo do resto do trabalho do servidor.
O caminho do Google está ausente dessa tabela de propósito. Ele precisa de um token assinado pelo Google para uma conta real, que só uma sessão de navegador consegue produzir, e um script não consegue entrar no Google. O roteiro manual do fim o mede com o painel Network do próprio navegador, cinco execuções, e o CSV tem uma linha reservada para o resultado.
Para dar escala, os mesmos três cenários num parse-server local com um PostgreSQL no mesmo processo levaram 1,7 ms, 1,8 ms e 42,6 ms no p50. Ali, sem rede, os 40 ms entre as linhas de convidado e a linha do e-mail são o hash da senha; a diferença entre local e ao vivo é a rede, e é por isso que os números ao vivo são os que importam.
O que encontramos enquanto construíamos
- O primeiro deploy de Cloud Code num backend novo pode reportar sucesso e não enviar nada. A prova é uma requisição, não o diálogo:
POST /functions/linkInfocom a REST key responde 209 quando o código está no ar e 141 “Invalid function” quando não está. - O Cloud Code roda em Node 19.9 e o Containers faz o build em Node 22. Nada neste repositório precisa de um runtime mais novo, mas um hook que usasse uma API do Node 20 só falharia depois do deploy.
- A master key nunca saiu da dashboard. Setup, limpeza dos testes e o teste que alarga a ACL são Cloud functions protegidas por
SETUP_ENABLEDemcloud/config.js. Enquanto ele está em true, qualquer um com o app id e uma chave de cliente pode chamá-las, então o último passo antes dos usuários reais é defini-lo como false, fazer o upload, fazer o deploy e provar com o curl do README.
Três coisas menores custaram tempo e pertencem a uma lista de troubleshooting, não à história:
- O botão do Google segue o idioma do navegador: numa máquina alemã ele dizia Über Google anmelden.
hl=enna URL do script elocale: 'en'emrenderButtono fixam. - Um botão do Google renderizado dentro de uma seção oculta tem 0 px de largura, porque o Google Identity Services o dimensiona no momento da renderização. A página renderiza os botões de novo sempre que uma view fica visível.
- Um app criado com a origem Upload files do Containers não consegue fazer o próprio redeploy: uma troca de plano disparou um redeploy que falhou com This deployment has no uploaded code, e a borda respondeu 502 até que Action → Redeploy latest code o reconstruiu a partir do código que a página Source Code ainda guardava.
Como fazer o deploy
-
Backend. Crie um app na dashboard do Back4app e copie o Application ID, a JavaScript key e a REST key para o
.env. DeixeMASTER_KEYvazio; nada aqui precisa dele. -
Google. No console do Google Cloud, crie um cliente OAuth do tipo Web application, adicione suas origens em Authorized JavaScript origins (esquema e host exatos, sem path) e cole o client id em Back4app → Authentication → Google e no
.envcomoGOOGLE_CLIENT_ID. -
Cloud Code. Dashboard → Cloud Code → faça o upload de
cloud/main.jsecloud/config.js, confirme o badge Files pending deploy (2), Deploy. Verifique com a requisição acima. -
Schema.
npm run setupadiciona a colunahasPassworde as permissões por classe pela funçãosetupSchemae imprime a sondagem:GET /userssem sessão respondendo 101. -
Testes e tempos.
TEST_SERVER_URL=https://parseapi.back4app.com npm test, depoisnpm run measure. Os dois criam seus próprios usuários e os apagam. -
Containers. Faça o deploy de um web app a partir do repositório com o Node.js Buildpack, porta 8080, health check
/healthze exatamente quatro variáveis de ambiente:APP_ID,JS_KEY,SERVER_URLeGOOGLE_CLIENT_ID. O servidor imprime toda variável que conhece em/config.js, que todo visitante pode ler, então nunca adicione uma chave que não seja pública. -
Google de novo. Adicione a URL do deploy às origens autorizadas. Até propagar, o botão registra The given origin is not allowed for the given client ID.
-
Antes de abrir ao público. Defina
SETUP_ENABLED: false, faça o upload decloud/config.js, Deploy, e confirme quePOST /functions/setupSchemaagora responde 119.
Quando não fazer desse jeito
Se você precisa ser o provedor de identidade de outras aplicações, ou seus clientes entram com o provedor SAML ou OpenID Connect da empresa deles, você precisa de um serviço de identidade, e produtos como Auth0, Keycloak ou Ory existem para isso. O authData do Parse é um repositório de credenciais por aplicação, não um broker de identidade.
Se “uma pessoa, uma conta” é uma exigência legal ou de cobrança, vincular a partir de uma sessão logada é necessário, mas não suficiente. Você também precisaria de um e-mail verificado antes de qualquer fusão, e de um caminho de suporte para a pessoa que tem duas contas e não lembra de nenhuma.
E note o que este post não executou: o roteiro manual do Google no navegador, que precisa de uma conta Google real. O docs/manual-google-check.md do repositório é o roteiro de dez minutos para isso.
Ele cobre um login novo, um login repetido, vincular uma senha a um usuário do Google, vincular o Google a um convidado, a colisão 208, cinco logins cronometrados, desvínculo, sair de todos os dispositivos e um client id errado. Quando tiver sido executado na demo, o arquivo de evidência dele substitui este parágrafo.
O repositório
github.com/templates-back4app/social-login-starter, MIT. public/ é o cliente inteiro, cloud/main.js o hook e as funções, scripts/ o setup, o servidor local e a medição de latência, test/ os 16 testes REST crus que rodam localmente contra o parse-server com PostgreSQL e, com uma variável de ambiente, contra o backend ao vivo.
docs/notes/build-diary.md registra o que foi observado durante a construção, incluindo os dez comportamentos vistos localmente que foram verificados de novo no Back4app, e docs/evidence/ guarda os CSVs por trás de cada número acima.