Skip to main content
Suas páginas guardam uma sessão de contato uk_ct_…. O seu backend precisa saber quem está chamando, e perguntar para a gente a cada requisição colocaria a nossa latência e a nossa disponibilidade na frente da sua API. Então a sessão de contato compra um JWT de vida curta, assinado por ambiente, que o seu backend verifica offline contra um conjunto de chaves públicas.
Duas credenciais, dois trabalhos. A sessão opaca é de vida longa e revogável. O JWT é de vida curta, e é exatamente por isso que dá para verificá-lo offline — e revogar um é um segundo documento em cache, não uma chamada, então não custa nada por requisição no seu backend. Veja Revogação.

Emitindo

Este é o fluxo de refresh, e não há uma segunda credencial para gerenciar: a sessão opaca é o refresh token. Chame de novo sempre que o último JWT estiver perto de expirar, enquanto a sessão viver.

Quando a sessão acaba

A sessão opaca vive 30 dias, contados do momento em que foi emitida. Não há renovação deslizante: a data não se move porque alguém continuou usando o produto, e não existe rota para estendê-la. Quando ela vence, a resposta é emitir uma nova. O SDK não descobre isso na próxima chamada — ele avisa no momento em que acontece, mesmo numa aba que só está aberta:
O subscribe também publica o signedOut, e é o que uma UI deve escutar — ela só precisa desenhar quem está ali agora. O onSessionExpired é para o código que precisa fazer algo a respeito, e para esse código as duas formas de uma sessão acabar são instruções opostas: reautenticar alguém que saiu é um laço, e reautenticar alguém cujo prazo venceu é a correção inteira. Por isso ele nunca dispara num signOut().

Renovar antes do fim, não depois

Um host federado deve responder ao onSessionExpiring, que é o mesmo evento cinco minutos antes — com a sessão ainda de pé:
Os cinco minutos são o ponto inteiro. O track() enfileira de forma durável e descarrega a cada cinco segundos, carimbando cada lote com a sessão que existir naquele instante — então uma renovação feita depois do vencimento manda tudo o que caiu no intervalo como anônimo: /v1/events rebaixa em vez de recusar, então nada falha e nada avisa. O intervalo dura o tempo que o seu código levar para perceber, e uma aba em segundo plano o alonga. Repetir a afirmação é legítimo: o hash é um HMAC sobre o external id e não carrega relógio nenhum, então, enquanto o seu app ainda responder por essa pessoa, ele diz exatamente o que dizia no carregamento. Um host que já a desconectou responde com signOut(). É anunciado uma vez por sessão — uma aba restaurada com quatro minutos de vida ouve na hora, em vez de nunca — e nunca para uma sessão que já venceu, que é o momento do onSessionExpired e faria você bootar duas vezes pela mesma sessão. Passe { marginMs } se a sua chamada de identidade precisar de mais espaço.
Cada emissão registra o contato no medidor de contatos ativos do mês, de forma idempotente. É esse o número que a sua fatura lê — veja o medidor.

O que o token carrega

string
O id do ambiente. Fixe ele. Um token do ambiente de teste não pode satisfazer uma verificação de produção.É o environment.id de GET /v1/me, a chamada que a sua chave secreta já deveria fazer no start-up — guarde o valor como USERKIT_ENVIRONMENT_ID. A listagem de ambientes não serve aqui: ela é do painel, e recusa uma chave de API. O painel mostra o mesmo id em Configurações → Chaves de API, ao lado da chave publicável.
uuid
O id do contato.
uuid
O id da sessão — o que você passa para DELETE /v1/contact/sessions/{id}.
string
O seu próprio id para essa pessoa, presente quando o contato tem um. No modo federado é nele que o seu backend se apoia.
string
boolean
Se a identidade por trás da sessão foi comprovada. Barre nisso antes de qualquer coisa por onde os dados de outra pessoa pudessem vazar — um token com verified: false é uma assinatura perfeitamente válida sobre uma afirmação não comprovada.
uuid
O time dentro do qual esta chamada está agindo — o que o cliente nomeou em X-Customer-Id na hora de emitir, ou o vínculo mais antigo quando ele não nomeou nenhum. Ausente para um contato que não pertence a customer nenhum, que é todo contato B2C e todo contato no primeiro dia.Ausente quer dizer “sem time”, nunca “sem necessidade de checar”. Um id de customer que o contato não tem é recusado com 404 na emissão, em vez de produzir um token sem a claim que o seu backend usa como porta.
string
A key do papel que essa pessoa tem nesse time. owner e member estão em todo ambiente; ao lado deles ficam os que a sua organização definiu, então isto é uma string aberta e não uma união de dois valores.
string[]
O que esse papel concede, e é nisto que o seu backend deve barrar — não no nome do papel. Dois vocabulários na mesma lista: as que começam com $ são nossas e são as únicas que o UserKit avalia ($team.manage, $billing.manage, $keys.manage, $webhooks.manage); todo o resto é seu, guardado e devolvido aqui e nunca lido por nós.É esta a claim que faz grades:write ser uma decisão offline: um papel novo criado no painel chega ao seu gate sem deploy nenhum do seu lado. Veja Times de customer.
boolean
Se quem está segurando esta sessão é alguém do seu suporte em vez da pessoa que ela nomeia. O contato é real e a identidade foi comprovada — é isso que “ver o que o cliente vê” quer dizer —, então sirva a requisição; mas um ato destrutivo feito por aqui está sendo feito pelo suporte, e só o seu produto sabe quais dos seus atos são esses.Sempre presente, inclusive como false, ao contrário das claims de time. A assimetria é de propósito: uma claim ausente teria que ser lida como “não é impersonação”, que é uma afirmação feita a partir de uma ausência — e é a direção tranquilizadora, a que faz alguém parar de checar. false é uma afirmação sobre a qual dá para agir; nada não é.
integer
Segundos Unix. A validade é de 5 minutos.

O time viaja no mint, não na sessão

As claims de time seguem o que esta emissão pediu, e é por isso que trocar de time é emitir de novo. @userkit/js faz isso sozinho: setActiveCustomer descarta o JWT em cache junto com a escolha, então a próxima getToken() mint a um token que nomeia o time novo.
Se você emite na mão, invalide o seu cache ao trocar de time. Um token de cinco minutos que continua nomeando o time anterior é um 403 sem causa visível para quem navega entre times pelo cliente — e some num F5, que é o pior formato que um bug pode ter.
No servidor não existe time ativo: quem emite é getToken({ customerId }) do @userkit/nextjs, e o id é o que a sua rota já leu, normalmente da URL. Sem ele a resposta é o vínculo mais antigo — um time de verdade, e raramente o que a página está mostrando.

Verificando

Busque o conjunto de chaves uma vez, guarde em cache, e verifique localmente.
Endereçado pela chave publicável — o único identificador que o seu backend já tem na config. Público e sem autenticação por definição: são chaves públicas. Também não há portão de Origin, porque quem chama é um servidor, e servidores não mandam Origin.
Verifique a assinatura. Não leia o payload sem ela — um JWT é base64, não é criptografia, e qualquer coisa pode ser digitada dentro de um.

Guarde o conjunto de chaves em cache

A resposta carrega:
Isso é deliberado. Um verificador precisa continuar verificando durante os nossos cinco minutos ruins, então um CDN ou um cache em processo deve seguir respondendo enquanto uma nova busca é tentada. A maioria dos clientes de JWKS respeita isso sozinha; os acima respeitam. As chaves são criadas na primeira leitura, então o documento nunca fica vazio para um ambiente configurado. Quando a emissão de JWT está indisponível no servidor, a resposta é um conjunto vazio em vez de um erro — o formato continua válido, e é o POST /v1/contact/token que devolve o 501 jwt_unavailable. O que as duas diretivas de staleness compram, e o resto do que acontece com o seu produto num minuto ruim nosso, está em Disponibilidade e degradação.

Revogação

Um JWT é verificado offline, então nada consegue chamá-lo de volta. Deletar uma sessão interrompe o próximo refresh, não o token que já está numa página — o que, sozinho, faz de cinco minutos o tempo máximo que uma sessão revogada continua funcionando. Cinco minutos é o piso, não a resposta. A resposta é um segundo documento, usado exatamente como o conjunto de chaves: buscado de tempos em tempos, guardado em cache, conferido localmente.
Compare revoked com o sid do token. Encerrar uma sessão chega ali na mesma instrução que a deleta, então toda porta — sair, revogar um dispositivo, “sair de todos os outros”, o suporte revogando um dispositivo — publica sem que nada precise lembrar de fazer isso.
integer
O limite que você ganha. Uma sessão revogada para de verificar dentro desses segundos, em vez de dentro da validade inteira do token.
integer
Até onde o documento alcança para trás. Uma cópia mais velha que isso não diz nada sobre os tokens vivos agora — descarte em vez de confiar.
A lista só carrega revogações da última janela, porque uma mais antiga não nomeia token algum que ainda pudesse ser apresentado. É isso que a mantém um documento curto, de consultar de tempos em tempos, e não uma lista de revogação que cresce para sempre.
Quando não conseguir buscá-la, continue verificando. Uma lista que recusa todas as requisições sempre que fica inalcançável é uma falha pior do que a que ela evita. A degradação correta é voltar ao limite que sempre existiu — os poucos minutos do próprio token — e nunca para “ninguém está logado”.

Com @userkit/nextjs

Assinatura e revogação, ambas offline, numa chamada só. Nada aqui fala com a gente no caminho da requisição.
As claims chegam nomeadas: claims.contactId, claims.verified, claims.impersonated e as do time — claims.customerId, claims.customerRole e claims.customerPermissions, que é sempre um array (vazio para quem não tem time), então um gate é um includes e nada antes dele:
claims.revocationCheck diz em qual dos três casos você está — fresh, stale (a última cópia, ainda dentro da janela dela) ou unavailable (aceito só pela assinatura e pela expiração). Passe requireRevocationCheck: true para recusar no unavailable nas poucas ações em que essa é a troca certa; deixe desligado no resto.

Fazendo na mão

As duas únicas regras: guarde em cache (isso é uma consulta periódica, não uma chamada por requisição) e trate uma busca que falhou como “não consigo revogar mais rápido do que o token expira”, nunca como uma recusa.

Sessões que o contato enxerga

O token de sessão de contato vive no seu domínio sem httpOnly. Isso faz de “ver meus dispositivos e encerrar um” parte da defesa desse token, não um extra.
Revogar é deletar, e a mesma instrução publica o id na lista de revogação — então o alcance chega ao JWT já emitido, não só ao próximo refresh. “Sair de todos os outros” mantém a sessão que pediu. O id de sessão de outro contato e um que nunca existiu respondem o mesmo 404. Só sessões. Encerrar um dispositivo não cancela um magic link ou um código por e-mail que estejam em aberto — isso é uma afirmação sobre a caixa de entrada, não sobre um notebook, e quem está no meio de um login em outra aba não deveria perdê-lo. Esses morrem no próprio relógio: uma hora para o link, dez minutos para o código.

Rotacionando a chave de assinatura

Exige api_keys:write — a mesma trava do segredo de identidade, porque essa chave emite os tokens em que o seu backend confia. A chave antiga para de assinar na hora e continua verificando por 24 horas: a sobreposição que permite aos caches de JWKS se atualizarem e aos tokens em voo expirarem. Seu backend não precisa de deploy — ele rebusca o conjunto de chaves e encontra os dois kid. Numa suspeita de comprometimento a conta corre ao contrário: todo token assinado com a chave antiga morre em até 5 minutos da rotação. É esse o limite a citar.

O medidor de contatos ativos

Exige customers:read. Um contato conta como ativo num mês quando um JWT é emitido para ele — então uma sessão aberta em janeiro e renovada em fevereiro está ativa nos dois. Só o ambiente live, porque só contatos de produção contam. O mês é UTC, decidido uma vez no schema: a exibição pode traduzir, uma fatura não pode ser ambígua. Isso lê a mesma consulta que a fatura lê.