@pearl-framework/auth
v1.0.0
Published
Pearl.js auth — JWT, session, and API token authentication guards
Downloads
27
Maintainers
Readme
@pearl-framework/auth
JWT authentication, password hashing, and route protection for Pearl.js.
Installation
npm install @pearl-framework/auth @pearl-framework/core @pearl-framework/http jsonwebtoken bcryptjsSetup
Setting up auth takes four steps: implement AuthUser, implement UserProvider, create a guard, and protect your routes.
Step 1 — Implement AuthUser
Your user model needs to implement two methods:
import type { AuthUser } from '@pearl-framework/auth'
export class User implements AuthUser {
constructor(
public readonly id: number,
public readonly email: string,
public readonly password: string,
) {}
// Used as the JWT `sub` claim
getAuthIdentifier() { return this.id }
// The hashed password — used by UserProvider for credential checks
getAuthPassword() { return this.password }
}Step 2 — Implement UserProvider
The provider tells Pearl how to look up users:
import type { UserProvider } from '@pearl-framework/auth'
import { Hash } from '@pearl-framework/auth'
import { db } from '../providers/AppServiceProvider.js'
import { users } from '../schema/users.js'
import { eq } from 'drizzle-orm'
export class DrizzleUserProvider implements UserProvider<User> {
async findById(id: number | string): Promise<User | null> {
const [row] = await db.select().from(users).where(eq(users.id, Number(id)))
return row ? new User(row.id, row.email, row.password) : null
}
async findByCredentials(email: string, password: string): Promise<User | null> {
const [row] = await db.select().from(users).where(eq(users.email, email))
if (!row) return null
return await Hash.check(password, row.password)
? new User(row.id, row.email, row.password)
: null
}
}Step 3 — Create the guard
import { JwtGuard, AuthManager } from '@pearl-framework/auth'
const guard = new JwtGuard(new DrizzleUserProvider(), {
secret: process.env.JWT_SECRET!, // minimum 32 characters
expiresIn: '7d', // ms-compatible string or seconds as a number
})
const auth = new AuthManager(guard)Step 4 — Protect routes
import { Authenticate, OptionalAuth } from '@pearl-framework/auth'
// Required auth — returns 401 if the token is missing or invalid
router.get('/me', (ctx) => ctx.response.json(ctx.user()), [Authenticate(auth)])
// Optional auth — populates ctx.user() if a valid token is present, but does not reject
router.get('/feed', (ctx) => {
const user = ctx.user() // User | null
ctx.response.json(buildFeed(user))
}, [OptionalAuth(auth)])Auth Routes
// POST /auth/register
router.post('/auth/register', async (ctx) => {
const { name, email, password } = ctx.request.body
const [user] = await db.insert(users).values({
name,
email,
password: await Hash.make(password),
}).returning()
const token = await auth.attempt(email, password)
ctx.response.created({ user, token })
})
// POST /auth/login
router.post('/auth/login', async (ctx) => {
const { email, password } = ctx.request.body
const token = await auth.attempt(email, password)
if (!token) return ctx.response.unauthorized('Invalid credentials')
ctx.response.json({ token })
})Password Hashing
import { Hash } from '@pearl-framework/auth'
// Hash a plain-text password (bcrypt, cost factor 10)
const hash = await Hash.make('my-password')
// Verify a password against a stored hash
const valid = await Hash.check('my-password', hash) // → true
const wrong = await Hash.check('wrong', hash) // → falseAuthServiceProvider
If you use Pearl's service container, register auth through the provider:
import { AuthServiceProvider } from '@pearl-framework/auth'
export class AppAuthServiceProvider extends AuthServiceProvider {
protected config = {
guard: 'jwt',
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: '7d',
},
}
}
app.register(AppAuthServiceProvider)Security Notes
- Algorithm pinning —
jwt.verify()is always called with an explicitalgorithmsallowlist. This prevents algorithm confusion attacks where an attacker switches the token's algorithm to bypass verification. nonealgorithm blocked — passingalgorithm: 'none'throws at construction time with a clear error message.- Secrets — use a minimum of 32 random characters for
JWT_SECRET. Useopenssl rand -base64 32to generate one.
API Reference
JwtGuard
| Method | Description |
|---|---|
| attempt(identifier, password) | Verify credentials and return a signed JWT, or null |
| issueToken(user) | Issue a JWT for an already-authenticated user |
| user(token) | Verify a token and return the user, or null |
| check(token) | Alias for user() — returns true/false equivalent via non-null check |
| refresh(token) | Issue a new token for the owner of a valid existing token |
| decode(token) | Decode a JWT payload without verifying the signature |
Hash
| Method | Description |
|---|---|
| Hash.make(password) | Bcrypt-hash a plain-text password |
| Hash.check(password, hash) | Verify a plain-text password against a bcrypt hash |
