diff --git a/server/auth.test.ts b/server/auth.test.ts new file mode 100644 index 0000000..2eab451 --- /dev/null +++ b/server/auth.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, setSystemTime } from 'bun:test' +import { hashPassword, hashToken, signAccessToken, verifyAccessToken, verifyPassword } from './auth' + +afterEach(() => { + setSystemTime() +}) + +describe('auth utilities', () => { + it('hashes and verifies a password', async () => { + const hash = await hashPassword('secret123') + expect(await verifyPassword('secret123', hash)).toBe(true) + expect(await verifyPassword('wrong', hash)).toBe(false) + }) + + it('signs and verifies an access token', async () => { + const token = await signAccessToken('user1', 'admin', 'mysecret') + const payload = await verifyAccessToken(token, 'mysecret') + expect(payload).toEqual({ userId: 'user1', role: 'admin' }) + }) + + it('returns null for an expired access token', async () => { + setSystemTime(new Date('2026-01-01T00:00:00.000Z')) + const token = await signAccessToken('user1', 'user', 'mysecret') + + setSystemTime(new Date('2026-01-01T00:16:00.000Z')) + const payload = await verifyAccessToken(token, 'mysecret') + expect(payload).toBeNull() + }) + + it('returns null for an access token with wrong secret', async () => { + const token = await signAccessToken('user1', 'user', 'mysecret') + const payload = await verifyAccessToken(token, 'wrongsecret') + expect(payload).toBeNull() + }) + + it('hashToken is deterministic', () => { + expect(hashToken('abc')).toBe(hashToken('abc')) + expect(hashToken('abc')).not.toBe(hashToken('xyz')) + }) +}) diff --git a/server/auth.ts b/server/auth.ts new file mode 100644 index 0000000..261db24 --- /dev/null +++ b/server/auth.ts @@ -0,0 +1,44 @@ +import { sign, verify } from 'hono/jwt' + +export interface AccessTokenPayload { + userId: string + role: string +} + +export async function hashPassword(password: string): Promise { + return Bun.password.hash(password) +} + +export async function verifyPassword(password: string, hash: string): Promise { + return Bun.password.verify(password, hash) +} + +export async function signAccessToken( + userId: string, + role: string, + secret: string, +): Promise { + return sign( + { userId, role, exp: Math.floor(Date.now() / 1000) + 15 * 60 }, + secret, + 'HS256', + ) +} + +export async function verifyAccessToken( + token: string, + secret: string, +): Promise { + try { + const payload = await verify(token, secret, 'HS256') as { userId: string; role: string } + return { userId: payload.userId, role: payload.role } + } catch { + return null + } +} + +export function hashToken(token: string): string { + const hasher = new Bun.CryptoHasher('sha256') + hasher.update(token) + return hasher.digest('hex') +}