A collection of low-level and high-level, server-agnostic, Authentication and Authorization utilities.
A collection of low-level and high-level, server-agnostic, Authentication and Authorization utilities.
[!WARNING]
This package is in active development. It is not recommended for production use yet unless you are willing to help with testing and feedback.
Expect breaking changes, as I prioritize usability and correctness over stability at this stage.
requireSession, optionalSession, requireAuth, optionalAuthBuilt on top of minimal dependencies:
unjwt — Low-level JWT (JWS/JWE/JWK) via Web Cryptounsecure — Cryptographic utilities (HMAC, secure compare, etc.)Install the package:
npx nypm install unauth
Encrypted session cookies (JWE) with auto-refresh support.
import { defineSession, generateJWK, requireSession } from "unauth/h3v2";
const sessionKey = await generateJWK("A256GCM");
const useSession = defineSession<{ userId: string; role: string }>({
key: sessionKey,
maxAge: "7D",
hooks: {
async onRefresh({ session, refresh }) {
// Refresh with updated data from your database
const user = await db.users.findById(session.data.userId);
await refresh({ userId: user.id, role: user.role });
},
},
});
const app = new H3()
.post("/login", async (event) => {
const session = await useSession(event);
await session.update({ userId: "u1", role: "admin" });
return { ok: true };
})
.get(
"/me",
async (event) => {
const session = await useSession(event);
return { user: session.data };
},
{ middleware: [requireSession(useSession)] },
)
.post("/logout", async (event) => {
const session = await useSession(event);
await session.clear();
return { ok: true };
});
The onRefresh hook fires when the session crosses the refreshAfter threshold (default: 75% of maxAge). You control what happens:
await refresh() — Sliding window (re-issue with same data)await refresh({ role: "admin" }) — Update data during refreshawait clear() — Destroy the sessionAccess token (JWS, short-lived, client-readable) + refresh token (JWE, long-lived, encrypted) with coordinated lifecycle.
import { defineTokenPair, generateJWK, requireAuth } from "unauth/h3v2";
const atKeys = await generateJWK("ES256");
const rtKey = await generateJWK("A256GCM");
const useAuth = defineTokenPair<
{ sub: string; permissions: string[] },
{ sub: string; family: string }
>({
access: { key: atKeys, maxAge: "15m" },
refresh: { key: rtKey, maxAge: "30D" },
hooks: {
async onRefresh({ refresh, issue }) {
const user = await db.users.findById(refresh.data.sub);
if (!user || user.suspended) return; // don't issue — AT stays empty
await issue({
accessData: { sub: user.id, permissions: user.permissions },
// refreshData is optional — omit to rotate with current data
});
},
onAfterRefresh({ access, refresh, previousRefresh }) {
logger.info("token_refresh", {
sub: access.data.sub,
newAtId: access.id,
oldRtId: previousRefresh.id,
newRtId: refresh.id,
});
},
},
});
const app = new H3()
.post("/login", async (event) => {
const auth = await useAuth(event);
await auth.issue({
accessData: { sub: user.id, permissions: user.permissions },
refreshData: { sub: user.id, family: crypto.randomUUID() },
});
return { ok: true };
})
.get(
"/me",
async (event) => {
const { access } = await useAuth(event);
return { user: access.data };
},
{ middleware: [requireAuth(useAuth)] },
)
.post("/logout", async (event) => {
const auth = await useAuth(event);
await auth.revoke();
return { ok: true };
});
When the access token expires:
onRefresh fires with the valid refresh tokenissue({ accessData, refreshData? }) to re-issue the AT and rotate the RTrevoke() to clear both tokens (e.g., user banned, family revoked)onError without destroying tokensBoth access and refresh are unjwt session managers exposed directly — call .update() or .clear() on them for escape-hatch scenarios that bypass hooks.
Double-submit cookie pattern with HMAC-generated tokens.
import { defineCsrf } from "unauth/h3v2";
const csrf = defineCsrf({ secret: process.env.CSRF_SECRET! });
const app = new H3()
.get("/form", handler, { middleware: [csrf] })
.post("/form", handler, { middleware: [csrf] });
Separate middleware for sessions and token pairs:
import { requireSession, optionalSession } from "unauth/h3v2";
import { requireAuth, optionalAuth } from "unauth/h3v2";
// Session middleware
app.get("/me", handler, { middleware: [requireSession(useSession)] });
app.get("/feed", handler, { middleware: [optionalSession(useSession)] });
// Token pair middleware
app.get("/me", handler, { middleware: [requireAuth(useAuth)] });
app.get("/feed", handler, { middleware: [optionalAuth(useAuth)] });
// With authorization checks
app.delete("/admin/users/:id", handler, {
middleware: [
requireAuth(useAuth, {
onAuthenticated({ session }) {
if (!session.data.permissions.includes("admin:users:delete")) {
throw new HTTPError("Forbidden", { status: 403 });
}
},
}),
],
});
I started by building unjwt, as I needed a cryptographically secure way to transmit sensitive information between various programming languages and servers. Not long after I started requiring some standardization, in particular on how to prepare and expect authorization data to be shared between parties (client and servers), but as I was testing various libraries I’ve never been satisfied by their DX (although most of them were great for someone that already knows the topic).
So I started building unauth as a collection of low-level primitives that then can be wrapped in higher-level abstractions, via adapters, to provide a “batteries included” experience while retaining control and flexibility of using your preferred storage, database and web frameworks.
Published under the MIT license.
Made by community 💛
🤖 auto updated with automd
We use cookies
We use cookies to analyze traffic and improve your experience. You can accept or reject analytics cookies.