
Company News
AWS Security Hub Adds Socket for Supply Chain Security
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.
@nostr-wot/auth
Advanced tools
Nostr authentication for HTTP servers — challenge / signed-event verify (NIP-98) / JWT. Framework-agnostic Web-standard handlers + Next.js App Router shim + client helper.
Drop-in Nostr authentication for HTTP servers. Stateless HMAC challenge → NIP-98 (kind 27235) signed-event verify → JWT. Mount four route handlers, get login.
| Entry | What's in it |
|---|---|
@nostr-wot/auth | createAuthService, createHandlers (Web standard), low-level challenge/verify/JWT primitives |
@nostr-wot/auth/next | createNextHandlers — Next.js App Router shim |
@nostr-wot/auth/client | loginWithSigner, fetchMe, logout — drives the full flow on the client |
You don't want email + password on a Nostr app. NIP-98 lets clients prove ownership of a pubkey by signing a server-issued challenge. This package implements the server side: issue, verify, JWT. Stateless by default — no DB, no Redis, no per-instance memory.
npm i @nostr-wot/auth nostr-tools
// lib/auth.ts
import { createAuthService } from "@nostr-wot/auth";
import { createNextHandlers } from "@nostr-wot/auth/next";
export const auth = createAuthService({
secret: process.env.NOSTR_AUTH_SECRET!, // 32+ random bytes; HMAC + JWT
challengeTtlSec: 300, // 5 min
jwtTtlSec: 60 * 60 * 24 * 7, // 1 week
jwtIssuer: "https://myapp.com",
expectedVerifyUrl: "https://myapp.com/api/auth/verify",
// Optional: hook into a successful verify to add custom claims or
// create/lookup a user row in your DB.
async onVerify({ pubkey }) {
const user = await db.user.upsert(pubkey);
return { uid: user.id, role: user.role };
},
});
export const handlers = createNextHandlers(auth, {
cookie: true, // also set/clear the JWT as an HttpOnly cookie
cookieName: "myapp_auth", // default "nw_auth"
cookieAttrs: { sameSite: "Lax", secure: true },
});
// app/api/auth/challenge/route.ts
import { handlers } from "@/lib/auth";
export const POST = handlers.challenge;
// app/api/auth/verify/route.ts
import { handlers } from "@/lib/auth";
export const POST = handlers.verify;
// app/api/auth/me/route.ts
import { handlers } from "@/lib/auth";
export const GET = handlers.me;
// app/api/auth/logout/route.ts
import { handlers } from "@/lib/auth";
export const POST = handlers.logout;
That's it. Four files. <5min of work.
import { loginWithSigner, fetchMe, logout } from "@nostr-wot/auth/client";
import { Nip07Signer } from "@nostr-wot/signers";
const signer = new Nip07Signer();
const { jwt, pubkey } = await loginWithSigner({
baseUrl: "/api/auth",
signer,
});
// Cookie is now set; subsequent fetches with `credentials: 'include'` work.
const me = await fetchMe({ baseUrl: "/api/auth" });
// → { pubkey, uid, role, iat, exp, ... }
await logout({ baseUrl: "/api/auth" });
secret. Returns <base64url(payload)>.<base64url(hmac)>. No state.["challenge", "<the-string>"], ["u", "<verify-url>"], ["method", "POST"]. Server verifies the signature with nostr-tools, recomputes the HMAC, checks the timestamp against the TTL, validates u/method if configured, then issues a JWT.jose, claims include pubkey (subject) + anything onVerify returns. Optionally set as an HttpOnly cookie./me — reads the JWT off Authorization: Bearer … or the cookie, returns the payload./logout — clears the cookie. Pure header op.The kind-27235 standard is NIP-98 (HTTP Auth). This package extends it with a challenge tag for replay-resistance.
secret, all existing JWTs and pending challenges become invalid. Plan for it.created_at on the signed event is checked within ±60s by default — tunable via skewSec. Tighten in lockstep with your server's clock guarantees.expectedVerifyUrl. Strongly recommended in production. Without it, a client could reuse a verify event signed for a different origin (e.g. an attacker's site).HttpOnly, SameSite=Lax, Path=/. Secure toggles on automatically when SameSite=None. For cross-subdomain flows, set cookieAttrs.domain.created_at. For strict no-replay guarantees (e.g. seeding the JWT from a queue worker), wire a per-challenge consume-once store via a wrapper around verifyChallenge.onVerifycreateAuthService({
secret: process.env.NOSTR_AUTH_SECRET!,
async onVerify({ pubkey, event }) {
const user = await db.users.findUnique({ where: { pubkey } });
if (!user || user.banned) throw new Error("forbidden");
return {
uid: user.id,
role: user.role,
tier: user.tier,
};
},
});
The returned object is merged into the JWT payload. Throwing aborts the login (returns 401 with reason: "hook_rejected").
For RSC or server actions:
// app/dashboard/page.tsx (server component)
import { handlers } from "@/lib/auth";
import { headers } from "next/headers";
export default async function Dashboard() {
const headerList = await headers();
const req = new Request("http://x", { headers: headerList });
const payload = await handlers.readJwt(req);
if (!payload) redirect("/login");
return <h1>Welcome {payload.pubkey}</h1>;
}
For middleware:
// middleware.ts
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export async function middleware(req: Request) {
const cookie = req.headers.get("cookie")?.match(/nw_auth=([^;]+)/)?.[1];
const payload = cookie ? await auth.verifyJwt(decodeURIComponent(cookie)) : null;
if (!payload) return NextResponse.redirect("/login");
}
createHandlers returns Web-standard (req: Request) => Promise<Response> functions. Mount in:
app.post("/auth/challenge", (c) => handlers.challenge(c.req.raw))if (url.pathname === "/auth/challenge") return handlers.challenge(req)Deno.serve(handlers.challenge)For when you need to plug into a custom flow:
import {
issueChallenge,
verifyChallenge,
verifyAuthEvent,
signAuthJwt,
verifyAuthJwt,
} from "@nostr-wot/auth";
const { challenge, expiresAt } = await issueChallenge(secret, 300);
const result = await verifyChallenge(challenge, secret, 300);
// → { ok, reason?, issuedAt? }
const verifyResult = await verifyAuthEvent(signedEvent, {
secret,
challengeTtlSec: 300,
expectedUrl: "https://myapp.com/api/auth/verify",
expectedMethod: "POST",
});
const jwt = await signAuthJwt({ pubkey, role: "admin" }, { secret });
const payload = await verifyAuthJwt(jwt, { secret });
MIT
FAQs
Nostr authentication for HTTP servers — challenge / signed-event verify (NIP-98) / JWT. Framework-agnostic Web-standard handlers + Next.js App Router shim + client helper.
We found that @nostr-wot/auth demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.

Research
/Security News
Popular npm packages keyv and cacheable compromised.

Security News
A misconfiguration gave three Anthropic models internet access, and one, believing it was in a simulation, shipped a credential-stealing package to PyPI.