@elefantedev/auth-client
v0.2.0
Published
Server-side OIDC/BFF client for Elefante Auth
Readme
@elefantedev/auth-client
Cliente OIDC server-side para integrar aplicativos independentes ao Elefante
Auth. Ele implementa o padrão BFF: tokens ficam cifrados em cookie HttpOnly
host-only e nunca são entregues ao JavaScript do navegador.
O pacote é ESM, não possui dependências de runtime e funciona em Cloudflare
Workers ou Node.js 20+ com Web Crypto, Fetch, Request e Response.
Este pacote é distribuído sob licença proprietária. Consulte LICENSE.
Instalação
Instale a versão publicada no npm:
pnpm add @elefantedev/auth-clientTambém é possível instalar o tarball gerado localmente:
pnpm add ./elefantedev-auth-client-0.2.0.tgzNão importe este pacote em uma SPA. clientSecret, tokens e chaves de cookie
devem existir somente no Worker/BFF.
Configuração
import {
ElefanteAuthClient,
type FetcherLike,
} from "@elefantedev/auth-client";
interface Env {
AUTH_ISSUER: string;
AUTH_CLIENT_ID: string;
AUTH_CLIENT_SECRET: string;
AUTH_COOKIE_KEY: string;
APP_BASE_URL: string;
AUTH?: FetcherLike;
}
export function createAuth(env: Env) {
return new ElefanteAuthClient({
issuer: env.AUTH_ISSUER,
clientId: env.AUTH_CLIENT_ID,
clientSecret: env.AUTH_CLIENT_SECRET,
redirectUri: `${env.APP_BASE_URL}/auth/callback`,
postLogoutRedirectUri: `${env.APP_BASE_URL}/`,
cookieKeys: [
{ version: "v1", secret: env.AUTH_COOKIE_KEY },
],
sessionMaxAgeSeconds: 14 * 24 * 60 * 60,
...(env.AUTH ? { binding: env.AUTH } : {}),
});
}A chave de cookie deve ter no mínimo 32 bytes. Para rotacioná-la sem encerrar todas as sessões, coloque a chave nova primeiro e mantenha temporariamente as anteriores:
cookieKeys: [
{ version: "v2", secret: env.AUTH_COOKIE_KEY_V2 },
{ version: "v1", secret: env.AUTH_COOKIE_KEY_V1 },
]sessionMaxAgeSeconds controla a duração absoluta da sessão BFF. O default e
o limite máximo são 30 dias; sessões de navegador continuam sem Max-Age
quando o login usa remember: false.
Rotas mínimas do BFF
const auth = createAuth(env);
const url = new URL(request.url);
if (request.method === "GET" && url.pathname === "/login") {
return auth.beginLogin({ returnTo: "/conta" });
}
if (request.method === "GET" && url.pathname === "/auth/callback") {
const result = await auth.handleCallback(request);
const response = new Response(null, {
status: 303,
headers: {
Location: new URL(result.redirectTo, request.url).toString(),
},
});
for (const cookie of result.cookies) {
response.headers.append("Set-Cookie", cookie);
}
return response;
}
if (request.method === "GET" && url.pathname === "/conta") {
return auth.requireUser(
request,
({ user }) => Response.json({ user }),
() => Response.redirect(new URL("/login", request.url), 302),
);
}
if (request.method === "POST" && url.pathname === "/logout") {
return auth.logout(request, { global: true, redirectTo: "/" });
}handleCallback(), authenticate() e requireUser() nunca retornam access,
ID ou refresh tokens. O aplicativo continua responsável por validar
origem/CSRF nos endpoints de mutação, aplicar cabeçalhos de segurança e nunca
registrar resultados completos, cookies ou códigos.
Identidade externa e autorização local
O Elefante Auth comprova a identidade. Cada aplicativo decide localmente,
usando user.sub, se essa identidade pode entrar e quais permissões possui.
Não use e-mail como chave estável e não crie usuários, papéis ou permissões
automaticamente dentro do SDK.
No callback, valide o acesso local antes de colocar o cookie de sessão no
navegador. Se o usuário não puder acessar o app, reject() revoga os tokens
recém-emitidos sem expô-los:
const callback = await auth.handleCallback(c.req.raw);
const localUser = await findActiveUserBySubject(
c.env.DB,
callback.user.sub,
);
if (!localUser) {
const cookies = await callback.reject();
const response = c.json(
{ error: "Acesso não autorizado para este aplicativo." },
403,
);
for (const cookie of cookies) {
response.headers.append("Set-Cookie", cookie);
}
return response;
}
const response = c.redirect(callback.redirectTo, 303);
for (const cookie of callback.cookies) {
response.headers.append("Set-Cookie", cookie);
}
return response;Um middleware Hono pode aplicar a autorização local nas requisições seguintes:
const identity = await auth.authenticate(c.req.raw);
if (!identity) {
return c.json({ error: "Login necessário." }, 401);
}
const localUser = await findActiveUserBySubject(
c.env.DB,
identity.user.sub,
);
if (!localUser) {
return c.json(
{ error: "Acesso não autorizado para este aplicativo." },
403,
);
}
c.set("user", withPermissions(localUser));
await next();
for (const cookie of identity.cookies) {
c.header("Set-Cookie", cookie, { append: true });
}identity.cookies normalmente está vazio e recebe valores apenas quando a
biblioteca rotaciona tokens e precisa atualizar o cookie opaco.
Erros
AuthClientError.code usa a union exportada AuthClientErrorCode. Sessão
ausente, inválida ou revogada faz authenticate() retornar null;
indisponibilidade do IdP lança provider_unavailable com status 503.
Callbacks inválidos, respostas incorretas do provedor, falhas de revogação e
eventos inválidos permanecem erros tipados para o aplicativo decidir entre
400, 401, 502 e 503.
Eventos de conta
Webhooks do Elefante Auth chegam como JWS compacto. Verifique assinatura, issuer, audience, idade e tipo antes de processar:
const event = await auth.verifyEventJws(await request.text(), {
allowedTypes: [
"account.email_changed",
"account.deletion_requested",
"account.deletion_cancelled",
"account.deleted",
],
});O consumidor deve persistir event.jti e rejeitar replay antes de confirmar o
recebimento.
Testes de aplicativos consumidores
O subpath @elefantedev/auth-client/testing fornece um fake determinístico e
independente de Vitest, Jest ou outro test runner. Ele implementa
ElefanteAuthClientLike, registra chamadas e permite cenários autenticados,
anônimos e de erro sem iniciar um IdP:
import {
createTestAuthClient,
createTestUser,
} from "@elefantedev/auth-client/testing";
const auth = createTestAuthClient({
user: createTestUser({
sub: "user_123",
email: "[email protected]",
}),
callbackRedirectTo: "/conta",
});
const app = createApp(env, auth);
const response = await app.fetch(
new Request("https://app.elefante.dev/conta"),
);
expect(response.status).toBe(200);
expect(auth.calls.authenticate).toHaveLength(1);Use user: null para uma sessão anônima. Respostas específicas e falhas
tipadas podem ser configuradas por overrides; o fake continua registrando a
chamada antes de executar o override. Os resultados públicos nunca incluem
access, ID ou refresh tokens.
Desenvolvimento e pacote
Na raiz do repositório do Elefante Auth:
pnpm --filter @elefantedev/auth-client test
pnpm --filter @elefantedev/auth-client typecheck
pnpm --filter @elefantedev/auth-client build
pnpm auth-client:verify
pnpm auth-client:packtest:package valida o tarball exato como dependência ESM e TypeScript de um
projeto externo temporário. O diretório scripts/output é ignorado pelo Git.
