@deeblr/auth-jwt
v0.4.0
Published
JWT access tokens, refresh-token rotation with reuse detection, and Personal Access Tokens for Deeblr Auth. Zero runtime dependencies (Node's built-in crypto only).
Readme
@deeblr/auth-jwt
Real RFC 7519 JWT access tokens, refresh-token rotation with reuse
detection, and Personal Access Tokens for Deeblr Auth. Zero runtime
dependencies — everything is built on Node's crypto module, not
jsonwebtoken.
Install
npm install @deeblr/auth-jwtMost people won't install this directly — configure apiTokens (and
optionally sessions: { strategy: "jwt" }) on @deeblr/auth's
DeeblrAuth. Install it directly if you're building on
@deeblr/auth-core/@deeblr/auth-session yourself.
JWT (JwtSigner)
import { JwtSigner } from "@deeblr/auth-jwt";
const signer = new JwtSigner({
algorithm: "HS256", // or "RS256" with { privateKey, publicKey }
secret: process.env.JWT_SECRET!,
issuer: "my-app",
audience: "my-app-api",
});
const token = signer.sign({
subject: user.id,
expiresInSeconds: 900,
claims: { role: "admin", organization: "org_1" }, // custom claims
});
const claims = signer.verify(token); // throws TOKEN_INVALID / TOKEN_EXPIRED
const inspected = signer.decode(token); // NO signature check — inspection only, never for auth decisionsRS256 is for deployments where the service verifying tokens must not be
able to mint them — distribute publicKey freely, keep privateKey only
where tokens are issued.
Refresh tokens (RefreshTokenService)
Opaque, hashed-at-rest, one-time-use tokens with reuse detection:
import { RefreshTokenService, InMemoryTokenStore } from "@deeblr/auth-jwt";
const service = new RefreshTokenService({ store: new InMemoryTokenStore(), clock, hooks, ttlSeconds: 60 * 60 * 24 * 30 });
const { token } = await service.issue(user.id);
const { token: newToken } = await service.rotate(token); // old token is now dead
await service.revoke(newToken); // kill one token
await service.revokeFamily(familyId); // kill every token descended from one login ("log out everywhere")Rotation IS one-time use — every rotate() call revokes the presented
token before issuing its replacement. If an already-rotated (dead) token is
ever presented again, that's treated as a compromise signal and the
entire family is revoked, forcing re-authentication — the same strategy
Auth0 and most modern OAuth implementations use.
InMemoryTokenStore is real and functional (fine for dev/single-instance),
not a placeholder. For multi-instance production, supply your own
TokenStore (4 methods) backed by a database or Redis.
Personal Access Tokens / API keys / service tokens
import { PersonalAccessTokenService } from "@deeblr/auth-jwt";
const pats = new PersonalAccessTokenService({ store, clock, hooks });
const { token } = await pats.create({ userId, name: "CI token", scopes: ["repo:read"], expiresInSeconds: 60 * 60 * 24 * 90 });
const record = await pats.verify(token, "repo:read"); // throws PERMISSION_DENIED if missing the scope
await pats.revoke(record.id);TokenManager — the combined auth.tokens.* API
import { TokenManager } from "@deeblr/auth-jwt";
const tokens = new TokenManager({
jwt: { secret: process.env.JWT_SECRET! },
clock,
hooks,
accessTokenTtlSeconds: 900, // default
refreshTokenTtlSeconds: 2592000, // default (30 days)
});
const pair = await tokens.create({ userId: user.id, claims: { role: "admin" } });
tokens.verify(pair.accessToken);
await tokens.refresh(pair.refreshToken);
await tokens.revoke(pair.refreshToken);
await tokens.createPersonalAccessToken({ userId: user.id, scopes: ["read"] });session: { strategy: "jwt" } — stateless sessions
JwtSessionStrategy implements @deeblr/auth-session's SessionStrategy
using signed JWTs — no storage at all. Stated honestly:
touch()(sliding expiration) always returns a new session id (a freshly-signed token) — a JWT can't be edited in place.list()/destroyAllForUser()throwAuthConfigError— nothing is persisted to enumerate. Need "list my sessions" or "log out everywhere"? Use thememory/databasestrategy, orRefreshTokenService(which IS persisted for exactly this reason).destroy()is a documented no-op unless you supply arevocationCache(anyCacheAdapter), in which case it adds the token'sjtito a denylist for its remaining lifetime.
sessions: {
strategy: "jwt",
jwt: { secret: process.env.SESSION_JWT_SECRET! },
revocationCache: myRedisCacheAdapter, // optional
}Hooks and events
auth:beforeTokenIssue, auth:afterTokenIssue, plus token.created,
token.revoked (includes reason: "reuse_detected" when applicable),
token.rotated — declaration-merged onto the shared AuthHookEventMap.
License
MIT
