@userkit/nextjs
v0.7.0
Published
UserKit for the Next.js App Router: route handlers that keep the session in an httpOnly cookie, server-side session reads, and a route guard.
Downloads
1,422
Maintainers
Readme
@userkit/nextjs
UserKit para o App Router do Next.js.
npm install @userkit/nextjsVersão
0.2.3, acompanhando o@userkit/jse o@userkit/react. Os três pacotes são versionados juntos.
O que este pacote muda
O @userkit/react sozinho fala com a API direto do navegador e guarda a sessão
no localStorage. É o arranjo para o qual a superfície de plano de clientes foi
desenhada, e ele não precisa de servidor nenhum.
Só que um app Next tem servidor. Então aqui a sessão vira um cookie
httpOnly: script na página não consegue lê-la, a chave publicável não entra no
bundle, e — o que o arranjo do navegador não consegue dar — a página sabe quem é
a pessoa antes de renderizar.
É o mesmo arranjo que o painel do próprio UserKit usa.
Fora isso, é o mesmo pacote: todo componente e todo hook do @userkit/react
sai daqui também, pelo mesmo nome, apontado para os handlers em vez da API.
Instale e importe de um lugar só. A lista de reexports é explícita e não um
export *, porque cada nome nela precisa de uma rota encaminhada do outro lado —
o useFeatureWaitlist é hoje o único que não está aqui, e é por isso.
Três coisas para montar
// app/api/userkit/[...userkit]/route.ts
import { createUserKitHandlers } from "@userkit/nextjs/handlers";
export const { GET, POST, PUT, DELETE } = createUserKitHandlers();// app/layout.tsx
import { UserKitProvider } from "@userkit/nextjs";
export default function Layout({ children }) {
return <UserKitProvider>{children}</UserKitProvider>;
}// proxy.ts (middleware.ts no Next 15)
import { createUserKitGuard } from "@userkit/nextjs/proxy";
export const proxy = createUserKitGuard({ signInPath: "/sign-in" });
export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"] };E uma variável, que fica no servidor:
USERKIT_PUBLISHABLE_KEY=uk_pk_live_…Repare que ela não é NEXT_PUBLIC_. A chave é publicável — no arranjo
direto ela mora no HTML — mas aqui ela não tem motivo para entrar no bundle, e
uma variável que não pode vazar é melhor que uma que só por acaso não vaza.
No servidor
// app/dashboard/page.tsx
import { getSession } from "@userkit/nextjs/server";
export default async function Dashboard() {
const session = await getSession();
return <h1>Olá, {session?.contact.name}</h1>;
}getSession() é memoizado por requisição com o cache do React: chamar num
layout e mais três vezes em componentes custa uma ida só. Entre requisições ele
não é cacheado — uma sessão revogada precisa morrer na hora, e isso vale mais
que o hop.
getToken() devolve o JWT curto para mandar a outro serviço — sua própria
API, um worker, qualquer coisa que valide contra o JWKS em vez de nos chamar.
Dentro deste app você não precisa dele: o getSession() já sabe.
null quer dizer deslogado, e só isso
Vale para as três leituras — getSession(), getContact() e getToken().
Quando a API não responde — rede morta, 500, 503 — elas lançam um
UserKitError em vez de devolver null: são dois fatos diferentes, e
juntá-los renderiza uma página deslogada para quem está logado. No getToken()
o estrago aparece um hop adiante e mais quieto: um token nulo vira uma
requisição sem Authorization, e o serviço lá na frente responde 401 —
reportando problema de autenticação para o que foi uma oscilação entre este app
e nós.
O requireSession() distingue os dois casos pelo status do erro (401 é "não
tem ninguém"; qualquer outro é "não deu para saber"). Se numa tela específica
uma leitura degradada for melhor que um erro, capture — mas essa decisão é sua,
e é por isso que ela não vem tomada.
Do outro lado: validando o JWT
// app/api/coisa/route.ts, ou qualquer serviço que não seja o app Next
import { verifyContactToken } from "@userkit/nextjs/verify";
export async function GET(request: Request) {
const token = request.headers.get("authorization")?.replace(/^Bearer /, "");
const claims = token ? await verifyContactToken(token) : null;
if (!claims) return new Response("unauthorized", { status: 401 });
return Response.json({ contact: claims.contactId });
}Assinatura e revogação, as duas offline. O conjunto de chaves e a lista de sessões revogadas são buscados de tempos em tempos e guardados em cache no processo — nenhuma chamada nossa no caminho da requisição, que é a propriedade que o arranjo todo existe para comprar.
A lista de revogação é o que faz "revogar" significar agora em vez de dentro
de cinco minutos: um JWT não pode ser chamado de volta, então uma sessão
revogada continuaria valendo até o exp dela. claims.revocationCheck diz qual
dos três casos você pegou — fresh, stale (a última cópia, ainda dentro da
janela dela) ou unavailable.
unavailable aceita o token, de propósito: se não conseguimos responder, a
degradação certa é voltar ao limite que sempre existiu — os poucos minutos do
próprio token — e nunca para "ninguém está logado". Passe
requireRevocationCheck: true nas poucas ações em que essa troca é outra.
Este módulo não importa nada de next: o serviço que confere um token muitas
vezes não é o app que o emitiu, e essa metade roda em qualquer runtime com
WebCrypto.
Sobre a guarda de rotas
Ela confere só que o cookie existe. Se aquele token ainda vale, e o que ele
pode fazer, é decidido onde os dados estão — no getSession() da página e na
API a cada chamada. Manter a checagem barata assim significa nenhuma ida de rede
na frente de cada navegação, e uma sessão revogada ainda morre no instante em que
a página tenta carregar qualquer coisa.
Leia isso como camadas, não como atalho: a guarda é o que impede alguém de chegar; nunca é o que impede alguém de ler.
O mesmo vale, uma camada abaixo, para o que os handlers repassam: caminho,
método, cabeçalhos e parâmetros de query são declarados um a um na allowlist,
nunca relaiados em bloco. A query é a parte que o navegador escreve à vontade, e
é por isso que ela é nominal — ?unread=true no feed de notificações chega
porque a rota diz que ele chega, e qualquer outro parâmetro é descartado antes
de a chamada sair daqui.
O config é você quem exporta, e o nome precisa ser exatamente esse. Com
qualquer outro nome ele é ignorado em silêncio, o matcher junto, e a guarda passa
a rodar em cada asset — um visitante deslogado recebe o CSS redirecionado para a
tela de login.
O JWT não volta para o navegador
getToken() no cliente responde erro em modo proxy, de propósito. O arranjo
inteiro existe para nenhuma credencial chegar em script; devolver uma ali
desfaria isso. O JWT é emitido no seu servidor, onde o cookie já está.
Se você precisa dele no navegador, o arranjo direto (@userkit/react puro) é o
que atende — e continua sendo uma escolha legítima.
