@atlex/auth
v0.2.1
Published
JWT and session authentication, guards, and password hashing for Atlex
Maintainers
Readme
@atlex/auth
Complete authentication system with JWT, sessions, guards, authorization, password hashing, and email verification for Express + Node.js applications.
Installation
npm install @atlex/authQuick start
Create an authenticatable user model:
import { Authenticatable } from '@atlex/auth'
export interface User extends Authenticatable {
id: string
email: string
password: string
emailVerifiedAt: Date | null
}Set up authentication in your service provider:
import { AuthServiceProvider } from '@atlex/auth'
import { Application } from '@atlex/core'
const app = new Application()
app.register(AuthServiceProvider, {
guards: {
web: {
driver: 'session',
provider: 'users',
},
api: {
driver: 'token',
provider: 'users',
},
},
providers: {
users: async (app) => new DatabaseUserProvider(User),
},
hashing: {
driver: 'bcrypt',
},
})Protect routes and authenticate users:
import { AuthMiddleware, StartSession } from '@atlex/auth'
// Use middleware
router.use(StartSession)
router.post('/login', AuthMiddleware, async (req, res) => {
const { email, password } = req.body
if (await auth().attempt(email, password)) {
const user = auth().user()
res.json({ authenticated: true, user })
} else {
res.status(401).json({ message: 'Invalid credentials' })
}
})Features
Multi-Guard Authentication
- SessionGuard: Cookie-based session authentication
- TokenGuard: JWT/API token authentication
- Guard-aware
auth()facade with per-request caching - Stateless and stateful guard contracts
Password Hashing
- Bcrypt: Industry-standard with adaptive work factors
- Argon2: OWASP-recommended, memory-hard algorithm
- Scrypt: Key derivation with configurable parameters
- Hash verification with automatic algorithm detection
JWT & Tokens
- Token pair support (access + refresh tokens)
- Configurable expiration windows
- Token blacklisting with multiple store backends
- Refresh token repository for persistence
- Secure token parsing and validation
Session Management
- Multiple session store backends
- FileStore: File-based session persistence
- DatabaseStore: SQL database session storage
- RedisStore: High-performance Redis backend
- CookieStore: Client-side encrypted sessions
- NullStore: In-memory (development/testing)
- Configurable session lifetime and garbage collection
Authorization
- Gate: Allow/deny rules for any resource or action
- Policies: Class-based authorization logic
- Ability checks with user context
- BeforeCallback hooks for early authorization
- AfterCallback hooks for conditional enforcement
Password Reset
- PasswordBroker: Secure reset token generation
- PasswordBrokerManager: Multi-broker support
- Configurable token expiration
- Email integration ready
- Verification before reset completion
Email Verification
- EmailVerifier: Track and enforce email verification
- MustVerifyEmail contract for user models
- Middleware enforcement with redirect handling
- Signed verification links
- Resend verification logic
Middleware
- StartSession: Initialize session per request
- AuthMiddleware: Guard-based authentication check
- AuthorizeMiddleware: Gate/policy enforcement
- EnsureEmailIsVerified: Verification gate
- ThrottleLogins: Brute-force protection with configurable lockout
Events
- Attempting: Before credentials checked
- Authenticated: User successfully authenticated
- Login: User logged in (session/token issued)
- Failed: Authentication failed
- Logout: User logged out
- Lockout: Account locked after failed attempts
- PasswordReset: Password reset complete
- PasswordResetLinkSent: Reset email dispatched
- Verified: Email verified
- Registered: New user registered
- CurrentDeviceLogout: Logout from current device
- OtherDeviceLogout: Logout from other devices
Core APIs
AuthManager
Central facade for authentication operations:
import { auth } from '@atlex/core'
// Get the active guard (from config)
const guard = auth().guard()
// Get a specific named guard
const webGuard = auth().guard('web')
const apiGuard = auth().guard('api')
// Attempt login with credentials
const success = await auth().attempt(email, password)
// Issue login (emit events, create session/token)
await auth().login(user)
// Get current authenticated user
const user = auth().user()
// Check if authenticated
const isAuth = auth().check()
// Check if guest (not authenticated)
const isGuest = auth().guest()
// Logout current user
await auth().logout()
// Validate password against user's hash
const valid = await auth().validate(user, plainPassword)
// Access password broker for reset flows
const broker = auth().broker('passwords')Google Social Login
Applications can verify a Google ID token with GoogleProvider, then decide how that verified
profile maps to their own user table:
import { app } from '@atlex/core'
import { AuthManager, GoogleProvider } from '@atlex/auth'
import { config } from '@atlex/config'
const provider = new GoogleProvider({
clientId: config('auth.google.clientId'),
})
const auth = app.make<AuthManager>('auth')
const user = await auth.socialLogin(provider, idToken, async (socialUser) => {
return await User.firstOrCreate(
{ email: socialUser.email },
{
name: socialUser.name,
google_id: socialUser.googleId,
email_verified_at: socialUser.emailVerified ? new Date() : null,
},
)
})@atlex/auth verifies provider tokens and returns normalized social profile data. Creating,
linking, and persisting application users remains owned by the consuming app.
Sign in with Apple
Applications can verify an Apple identity token with AppleProvider. Use the Apple services
ID or bundle ID as clientId:
import { app } from '@atlex/core'
import { AppleProvider, AuthManager } from '@atlex/auth'
import { config } from '@atlex/config'
const provider = new AppleProvider({
clientId: config('auth.apple.clientId'),
})
const auth = app.make<AuthManager>('auth')
const user = await auth.socialLogin(provider, identityToken, async (socialUser) => {
return await User.firstOrCreate(
{ apple_id: socialUser.appleId },
{
email: socialUser.email,
name: socialUser.name ?? 'Apple User',
apple_id: socialUser.appleId,
email_verified_at: socialUser.emailVerified ? new Date() : null,
},
)
})Apple only sends the user's name on the first sign-in. Store it immediately by verifying the identity token with the first-sign-in profile data before resolving or persisting the user:
const socialUser = await provider.verify(identityToken, {
name: { firstName, lastName },
})
const user = await User.firstOrCreate(
{ apple_id: socialUser.appleId },
{
email: socialUser.email,
name: socialUser.name ?? 'Apple User',
apple_id: socialUser.appleId,
email_verified_at: socialUser.emailVerified ? new Date() : null,
},
)Guards
SessionGuard
Cookie-based session authentication:
const guard = auth().guard('web')
// Authenticate user and create session
await guard.authenticate(user)
// Get current user
const user = guard.user()
// Check authentication
const authenticated = guard.check()
// Check guest
const guest = guard.guest()
// Logout (destroy session)
await guard.logout()TokenGuard
JWT/API token authentication:
const guard = auth().guard('api')
// Issue token pair (access + refresh)
const tokens = await guard.authenticate(user)
// { accessToken: '...', refreshToken: '...' }
// Get current user from token
const user = guard.user()
// Check if token is valid
const valid = guard.check()
// Logout (blacklist token)
await guard.logout()Password Hashing
import { HashManager, BcryptHasher, Argon2Hasher } from '@atlex/auth'
const hash = new HashManager({
driver: 'bcrypt',
bcrypt: { rounds: 12 },
argon2: { memory: 65536 },
})
// Hash a password
const digest = await hash.hash('plain-password')
// Verify password against hash
const valid = await hash.verify('plain-password', digest)
// Check if hash needs rehashing (algorithm update, etc.)
const needsRehash = hash.needsRehash(digest)JWT Provider
import { JwtProvider, RefreshTokenRepository } from '@atlex/auth'
const jwt = new JwtProvider(config, refreshRepo)
// Create JWT token with custom claims
const token = jwt.encode({ sub: user.id, email: user.email })
// Parse and verify token
const claims = jwt.decode(token)
// Attempt login and get token pair
const result = await jwt.attempt(email, password)
if (result.succeeded) {
console.log(result.accessToken, result.refreshToken)
}
// Refresh access token
const newAccessToken = await jwt.refresh(refreshToken)
// Revoke token via blacklist
await jwtBlacklist.blacklist(token)Authorization
Gate
import { Gate } from '@atlex/auth'
const gate = new Gate()
// Define ability
gate.define('edit-post', (user: User, post: Post) => {
return user.id === post.authorId
})
// Check authorization
if (gate.allows('edit-post', post)) {
// Allow edit
}
// Or use can/cannot on request context
const allowed = req.auth.can('edit-post', post)
const denied = req.auth.cannot('edit-post', post)
// Use before callback for admin override
gate.before((user: User, ability: string) => {
if (user.isAdmin) return true
})Policy
import { Policy } from '@atlex/auth'
class PostPolicy extends Policy {
view(user: User, post: Post) {
return !post.isDraft || user.id === post.authorId
}
edit(user: User, post: Post) {
return user.id === post.authorId
}
delete(user: User, post: Post) {
return user.id === post.authorId || user.isAdmin
}
}
// Register policy
gate.policy(Post, PostPolicy)
// Check ability via policy
if (req.auth.can('edit', post)) {
// Allow
}Password Reset
import { PasswordBroker } from '@atlex/auth'
const broker = auth().broker('passwords')
// Send reset link to user
const sent = await broker.sendResetLink(email)
// Reset password with token
const status = await broker.reset(email, token, newPassword)
// Check if token exists and hasn't expired
const exists = await broker.tokenExists(email, token)Session Management
import { Session, SessionManager } from '@atlex/auth'
const sessionManager = new SessionManager(config, store)
// Create new session
const session = sessionManager.create()
// Store data in session
session.put('user_id', user.id)
session.put('cart', { items: [...] })
// Retrieve data
const userId = session.get('user_id')
const all = session.all()
// Remove data
session.forget('cart')
session.flush()
// Regenerate ID (security)
await session.regenerate()Email Verification
import { EmailVerifier } from '@atlex/auth'
const verifier = new EmailVerifier(config)
// Generate signed verification link
const link = verifier.verificationUrl(user)
// Mark user email as verified
await verifier.verify(user)
// Check if user has verified email
const isVerified = user.hasVerifiedEmail?.()Configuration
Create config/auth.ts:
import type { AuthConfig } from '@atlex/auth'
import { DatabaseUserProvider } from '@atlex/auth'
export default {
defaults: {
guard: 'web',
provider: 'users',
},
guards: {
web: {
driver: 'session',
provider: 'users',
},
api: {
driver: 'token',
provider: 'users',
},
},
providers: {
users: async (app) => new DatabaseUserProvider(User),
},
jwt: {
secret: process.env.JWT_SECRET!,
algo: 'HS256',
accessTokenExpires: '15m',
refreshTokenExpires: '7d',
issuer: 'atlex-app',
},
session: {
driver: 'cookie',
lifetime: 120, // minutes
cookieName: 'ATLEXSESSID',
},
hashing: {
driver: 'bcrypt',
bcrypt: {
rounds: 12,
},
argon2: {
memory: 65536,
time: 4,
parallelism: 1,
},
scrypt: {
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 64 * 1024 * 1024,
},
},
passwords: {
table: 'password_resets',
expires: 3600, // seconds
},
throttle: {
maxAttempts: 5,
decay: 60, // minutes
},
} as AuthConfigExamples
Complete Login Flow
import { Router, Request } from 'express'
import { auth, StartSession, AuthMiddleware } from '@atlex/auth'
const router = Router()
router.use(StartSession)
// Login page
router.get('/login', (req, res) => {
res.render('login')
})
// Login handler
router.post('/login', AuthMiddleware, ThrottleLogins, async (req, res) => {
const { email, password } = req.body
const dispatcher = req.app.make('event.dispatcher')
try {
dispatcher.dispatch(new Attempting(email))
const authenticated = await auth().attempt(email, password)
if (!authenticated) {
dispatcher.dispatch(new Failed(email))
return res.status(401).json({ message: 'Invalid credentials' })
}
const user = auth().user()
dispatcher.dispatch(new Authenticated(user))
dispatcher.dispatch(new Login(user))
res.json({ authenticated: true, user })
} catch (error) {
if (error instanceof LockoutError) {
return res.status(429).json({ message: 'Too many attempts' })
}
throw error
}
})
// Logout handler
router.post('/logout', async (req, res) => {
const user = auth().user()
await auth().logout()
req.app.make('event.dispatcher').dispatch(new Logout(user))
res.json({ message: 'Logged out successfully' })
})
export default routerAPI Token Authentication
import { TokenGuard } from '@atlex/auth'
router.post('/api/login', async (req, res) => {
const { email, password } = req.body
const guard = auth().guard('api') as TokenGuard
const result = await guard.attempt(email, password)
if (!result.succeeded) {
return res.status(401).json({ message: 'Invalid credentials' })
}
res.json({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
expiresIn: 900, // 15 minutes
})
})
// Refresh token
router.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.body
try {
const newToken = await auth().guard('api').refresh(refreshToken)
res.json({ accessToken: newToken })
} catch (error) {
res.status(401).json({ message: 'Token expired' })
}
})Protected Routes with Authorization
import { AuthorizeMiddleware } from '@atlex/auth'
// Require authentication
router.get(
'/dashboard',
(req, res, next) => {
if (!auth().check()) {
return res.redirect('/login')
}
next()
},
(req, res) => {
const user = auth().user()
res.render('dashboard', { user })
},
)
// Require specific ability
router.patch('/posts/:id', AuthorizeMiddleware('edit-post'), async (req, res) => {
const post = await Post.find(req.params.id)
if (!req.auth.can('edit-post', post)) {
return res.status(403).json({ message: 'Unauthorized' })
}
// Update post...
res.json(post)
})
// Policy-based authorization
router.delete('/posts/:id', async (req, res) => {
const post = await Post.find(req.params.id)
const policy = gate.policy(Post)
if (!policy.delete(auth().user(), post)) {
return res.status(403).json({ message: 'Forbidden' })
}
await post.delete()
res.json({ message: 'Deleted' })
})Password Reset Flow
router.post('/forgot-password', async (req, res) => {
const { email } = req.body
const broker = auth().broker('passwords')
const sent = await broker.sendResetLink(email)
res.json({
message: 'Password reset link sent to email',
sent,
})
})
router.post('/reset-password', async (req, res) => {
const { email, token, password, password_confirmation } = req.body
if (password !== password_confirmation) {
return res.status(422).json({ message: 'Passwords do not match' })
}
const broker = auth().broker('passwords')
const status = await broker.reset(email, token, password)
if (status === PasswordResetStatus.TOKEN_INVALID) {
return res.status(422).json({ message: 'Invalid reset token' })
}
res.json({ message: 'Password reset successfully' })
})Email Verification
router.get('/email/verify/:id/:hash', async (req, res) => {
const user = await User.find(req.params.id)
if (!user || !verifier.verify(user, req.params.hash)) {
return res.status(422).json({ message: 'Invalid verification link' })
}
await user.markEmailAsVerified()
res.json({ message: 'Email verified successfully' })
})
// Middleware to require verified email
router.post('/sensitive-action', EnsureEmailIsVerified, async (req, res) => {
// Only verified users can reach here
const user = auth().user()
// ...
})API Overview
Authentication
AuthManager.guard(name?)- Resolve a guardAuthManager.attempt(email, password)- Attempt loginAuthManager.login(user)- Log user inAuthManager.user()- Get current userAuthManager.check()- Check if authenticatedAuthManager.guest()- Check if guestAuthManager.logout()- Log user outAuthManager.validate(user, password)- Verify passwordAuthManager.broker(name)- Get password broker
Guards
SessionGuard.authenticate(user)- Create sessionSessionGuard.user()- Get user from sessionSessionGuard.check()- Check session validitySessionGuard.logout()- Destroy sessionTokenGuard.attempt(email, password)- Login and issue tokensTokenGuard.refresh(token)- Refresh access tokenTokenGuard.user()- Get user from tokenTokenGuard.logout()- Blacklist token
Password Hashing
HashManager.hash(password)- Hash passwordHashManager.verify(password, hash)- Verify passwordHashManager.needsRehash(hash)- Check if rehash neededBcryptHasher,Argon2Hasher,ScryptHasher- Algorithm implementations
JWT
JwtProvider.encode(claims)- Create JWTJwtProvider.decode(token)- Verify and decodeJwtProvider.attempt(email, password)- Login and get tokensJwtBlacklist.blacklist(token)- Revoke tokenRefreshTokenRepository.store(token, user, expiry)- Store refresh token
Authorization
Gate.define(ability, callback)- Define abilityGate.policy(model, Policy)- Register policyGate.allows(ability, ...args)- Check permissionGate.denies(ability, ...args)- Check denialGateForUser.can(ability, ...args)- Check as userPolicy- Base class for policy definitions
Session
Session.put(key, value)- Store valueSession.get(key)- Retrieve valueSession.all()- Get all dataSession.forget(key)- Remove valueSession.flush()- Clear sessionSession.regenerate()- Generate new ID
Password Reset
PasswordBroker.sendResetLink(email)- Send reset linkPasswordBroker.reset(email, token, password)- Reset passwordPasswordBroker.tokenExists(email, token)- Check token
Events
All events are dispatched via EventDispatcher:
import { Attempting, Authenticated, Login, Failed, Logout } from '@atlex/auth'
dispatcher.on(Authenticated, (event) => {
console.log('User authenticated:', event.user)
})
dispatcher.on(Login, (event) => {
console.log('User logged in:', event.user)
})
dispatcher.on(Failed, (event) => {
console.log('Authentication failed:', event.email)
})Documentation
For more information, visit atlex.dev/guide/auth
License
MIT
Part of Atlex — A modern framework for Node.js.
