bcryptjs password hashing + jose JWT sessions: working versions (tested 2026-10-15)
Hash passwords with bcryptjs and issue and verify signed JWT session tokens with jose. Framework-agnostic auth core.
passed3 passed · 0 failed2026-10-15 13:05 UTC- Status
- passed
- Last passed
- 2026-10-15 13:05 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38738874208
The code
Exactly the files that passed. Your agent can pull them with get_recipe("bcryptjs-jose-sessions") (install), or see them on GitHub.
{
"name": "bcryptjs-jose-sessions",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"bcryptjs": "3.0.3",
"jose": "6.2.12"
},
"devDependencies": {
"tsx": "4.23.15",
"@types/node": "24.19.2"
}
}
import bcrypt from "bcryptjs";
import { SignJWT, jwtVerify } from "jose";
export const hashPassword = (pw: string) => bcrypt.hash(pw, 10);
export const checkPassword = (pw: string, hash: string) => bcrypt.compare(pw, hash);
export async function createSession(userId: string, secret: Uint8Array, ttl = "2h") {
return new SignJWT({ role: "user" })
.setProtectedHeader({ alg: "HS256" })
.setSubject(userId)
.setIssuedAt()
.setIssuer("my-app")
.setExpirationTime(ttl)
.sign(secret);
}
export async function readSession(token: string, secret: Uint8Array) {
const { payload } = await jwtVerify(token, secret, { issuer: "my-app", algorithms: ["HS256"] });
return { userId: payload.sub!, role: payload.role as string };
}
import { test } from "node:test";
import assert from "node:assert/strict";
import { hashPassword, checkPassword, createSession, readSession } from "../src/auth.ts";
const secret = new TextEncoder().encode("a-32-byte-long-test-secret-value!");
test("hash and verify a password", async () => {
const h = await hashPassword("hunter2");
assert.notEqual(h, "hunter2");
assert.equal(await checkPassword("hunter2", h), true);
assert.equal(await checkPassword("wrong", h), false);
});
test("session round trip", async () => {
const t = await createSession("u1", secret);
assert.deepEqual(await readSession(t, secret), { userId: "u1", role: "user" });
});
test("rejects tampered and expired tokens", async () => {
const t = await createSession("u1", secret);
await assert.rejects(readSession(t, new TextEncoder().encode("another-32-byte-long-secret-val!!")));
const old = await createSession("u1", secret, "-1s");
await assert.rejects(readSession(old, secret));
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
NextAuth.js v4 credentials provider + JWT/session callbacks (mocked)
NextAuth v4 authOptions for Next.js with a Credentials provider, bcrypt-checked users and role claims flowing through jwt and session callbacks, unit-tested without a server.
Verify JWTs against a JWKS with jose (RS256, key rotation) in Vitest
Verify third-party RS256 access tokens (Auth0, Cognito, Clerk style) with jose's JWKS key sets, checking issuer, audience, expiry and key rotation, tested offline with a local JWKS.