farrow-auth-session
v0.0.1
Published
Session-based authentication middleware for Farrow framework with flexible storage and parsing layers
Maintainers
Readme
farrow-auth-session
Session-based authentication middleware for Farrow HTTP framework with flexible storage and parsing layers.
Features
- 🔐 Flexible Authentication Architecture - Support any authentication method via SessionStore and SessionParser interfaces
- 🔒 Type Safety - Full TypeScript support with automatic type inference
- 🏗️ Modular Design - Complete decoupling of storage and parsing layers for easy extension
- 🎯 Context-Driven - Based on farrow-pipeline's Context system with request-level isolation
- ⚡ Automatic State Management - Tracks data modifications and intelligently determines when to save
- 🚀 Native Integration - Designed specifically for Farrow framework with seamless middleware integration
Installation
npm install farrow-auth-session
# or
yarn add farrow-auth-session
# or
pnpm add farrow-auth-sessionQuick Start
The following example demonstrates basic usage with the built-in Cookie Session components. You can also create custom authentication solutions by implementing the SessionStore and SessionParser interfaces.
Basic Cookie Session
import { Http } from 'farrow-http'
import { createSession, createSessionCtx, cookieSessionParser, cookieSessionStore } from 'farrow-auth-session'
// Define your user data type
type UserData = {
userId?: string
username?: string
role?: string
}
// Create auth context
const sessionUserDataCtx = createSessionCtx<UserData>({})
// Setup authentication middleware
const sessionMiddleware = createSession({
sessionUserDataCtx,
sessionParser: cookieSessionParser(),
sessionStore: cookieSessionStore<UserData>({
secret: process.env.SESSION_SECRET || 'your-secret-key-min-32-chars-long!!!'
}),
autoSave: true,
autoCreateOnMissing: true
})
// Create HTTP app
const app = Http()
// Method 1: Apply auth middleware globally
app.use(sessionMiddleware)
// Method 2: Apply to specific routes
const protectedRouter = Router()
protectedRouter.use(sessionMiddleware) // Only use auth in this router group
protectedRouter.get('/profile').use(() => {
const userData = sessionUserDataCtx.get()
return Response.json(userData)
})
protectedRouter.post('/update').use((request) => {
sessionUserDataCtx.set({ ...sessionUserDataCtx.get(), ...request.body })
return Response.json({ success: true })
})
app.route('/api/protected').use(protectedRouter) // Mount protected routes
// Use session in routes
app.post('/login').use(async (request) => {
// Your login logic here
const user = await validateUser(request.body)
// Set user data in session
sessionUserDataCtx.set({
userId: user.id,
username: user.name,
role: user.role
})
return Response.json({ success: true })
})
app.get('/profile').use(() => {
const userData = sessionUserDataCtx.get()
if (!userData?.userId) {
return Response.status(401).json({ error: 'Not authenticated' })
}
return Response.json(userData)
})
app.post('/logout').use(async () => {
await sessionUserDataCtx.destroy()
return Response.json({ success: true })
})
app.listen(3000)Cookie Components
CookieSessionParser - Cookie Session Parser
Responsible for parsing session ID from HTTP request cookies and setting/removing cookies in responses.
Key Features:
- Extract and decode session ID from request cookies
- Set encrypted session ID in response
- Support custom encoder/decoder
- Manage cookie lifecycle
CookieSessionStore - Cookie Session Storage
⚠️ Security Warning: CookieSessionStore stores session data directly in client-side cookies. Although it uses AES-256-CBC encryption, there are still security risks:
- Client can see the encrypted data
- Cookie size limitation (typically 4KB)
- Not suitable for storing sensitive information
Recommended Use Cases:
- Development and testing environments
- Storing non-sensitive user preferences
- Small applications or prototyping
For production use, consider implementing a custom SessionStore with server-side storage (Redis, database, etc.) for better security and scalability.
Key Features:
- AES-256-CBC encryption for session data
- Support rolling/renew expiration strategies
- Automatic session lifecycle management
- Data integrity verification
Configuration Options
Cookie Session Parser Options
cookieSessionParser({
sessionIdKey: 'sess:k', // Cookie key for session ID
cookieOptions: {
maxAge: 30 * 60 * 1000, // 30 minutes
httpOnly: true, // HTTP only cookie
sameSite: 'lax', // CSRF protection
secure: true, // HTTPS only (production)
domain: '.example.com', // Cookie domain
path: '/' // Cookie path
},
customCodec: { // Optional custom encoding
encode: (id) => customEncode(id),
decode: (encoded) => customDecode(encoded)
}
})Cookie Session Store Options
cookieSessionStore<UserData>({
secret: process.env.SESSION_SECRET, // Required: encryption secret key
sessionStoreKey: 'sess:data', // Cookie key for session data
rolling: true, // Reset expiry on every request
renew: false, // Renew only when near expiration
renewBefore: 10 * 60 * 1000, // Renew 10 minutes before expiry
cookieOptions: {
maxAge: 60 * 60 * 1000, // 1 hour
httpOnly: true,
sameSite: 'strict'
},
dataCreator: (request, userData) => {
// Initialize session data
return {
createdAt: Date.now(),
ip: request.headers['x-forwarded-for'],
...userData
}
}
})Session Expiration Strategies
Rolling Sessions
Resets expiration time on every request. Best for "keep alive" scenarios.
cookieSessionStore({
secret: process.env.SESSION_SECRET,
rolling: true,
cookieOptions: { maxAge: 30 * 60 * 1000 } // 30 minutes
})Renewing Sessions
Only updates expiration when close to expiry. Better performance.
cookieSessionStore({
secret: process.env.SESSION_SECRET,
renew: true,
renewBefore: 10 * 60 * 1000, // Renew 10 minutes before expiry
cookieOptions: { maxAge: 60 * 60 * 1000 } // 1 hour
})Fixed Sessions
Session expires at a fixed time regardless of activity.
cookieSessionStore({
secret: process.env.SESSION_SECRET,
rolling: false,
renew: false,
cookieOptions: { maxAge: 8 * 60 * 60 * 1000 } // 8 hours
})Route-Level Usage
Flexible Route Configuration
You can use authentication middleware in different route groups as needed:
import { Http, Router } from 'farrow-http'
const app = Http()
// Public routes (no auth required)
const publicRouter = Router()
publicRouter.get('/about').use(() => {
return Response.json({ message: 'About us' })
})
// Protected routes (auth required)
const protectedRouter = Router()
protectedRouter.use(sessionMiddleware) // Only use in this router group
protectedRouter.get('/<userId:string>').use((request) => {
const userData = sessionUserDataCtx.get()
if (!userData) {
return Response.status(401).json({ error: 'Login required' })
}
// Store route params in auth context
sessionUserDataCtx.set({ ...userData, currentUserId: request.params.userId })
return Response.json({
message: `User ${userData.username} is viewing ${request.params.userId}'s info`
})
})
protectedRouter.get('/dashboard').use(() => {
const userData = sessionUserDataCtx.get()
return Response.json({
dashboard: 'User dashboard data',
user: userData
})
})
// Admin routes (special permissions required)
const adminRouter = Router()
adminRouter.use(sessionMiddleware)
adminRouter.use((request, next) => {
const userData = sessionUserDataCtx.get()
if (!userData?.isAdmin) {
return Response.status(403).json({ error: 'Admin access required' })
}
return next(request)
})
adminRouter.get('/users').use(() => {
return Response.json({ users: getAllUsers() })
})
// Mount routes
app.route('/public').use(publicRouter)
app.route('/user').use(protectedRouter)
app.route('/admin').use(adminRouter)
app.listen(3000)Conditional Authentication
Decide whether to use authentication based on different conditions:
const apiRouter = Router()
// Optional auth: logged-in users get more permissions
apiRouter.use((request, next) => {
// Check for token or cookie
const hasAuth = request.headers.authorization || request.cookies?.['sess:k']
if (hasAuth) {
// Has auth info, apply auth middleware
return sessionMiddleware(request, next)
}
// No auth info, continue without auth
return next(request)
})
apiRouter.get('/posts').use(() => {
const userData = sessionUserDataCtx.get()
if (userData) {
// Logged-in user: return personalized content
return Response.json({
posts: getPersonalizedPosts(userData.userId),
recommended: true
})
} else {
// Guest user: return public content
return Response.json({
posts: getPublicPosts(),
recommended: false
})
}
})Advanced Usage
SessionUserDataCtx Core Methods
sessionUserDataCtx provides complete authentication data management functionality:
1. get() - Get current user data
app.get('/profile').use(() => {
const userData = sessionUserDataCtx.get()
if (!userData) {
return Response.status(401).json({ error: 'Not authenticated' })
}
return Response.json(userData)
})2. set(data) - Set user data
app.post('/login').use(async (request) => {
const user = await validateUser(request.body)
// Set user data (automatically marked as modified)
sessionUserDataCtx.set({
userId: user.id,
username: user.name,
role: user.role
})
return Response.json({ success: true })
})3. regenerate() - Regenerate session
Used for security-sensitive operations like privilege escalation or session refresh before important operations.
app.post('/admin/login').use(async () => {
// Regenerate session ID while preserving existing data
const success = await sessionUserDataCtx.regenerate()
if (success) {
// Update permissions
const current = sessionUserDataCtx.get()
sessionUserDataCtx.set({ ...current, isAdmin: true })
return Response.json({ message: 'Admin privileges activated' })
}
return Response.status(500).json({ error: 'Failed to regenerate session' })
})Return values:
true: Successfully regeneratedfalse: Operation failed (e.g., no data exists)undefined: Internal error
4. destroy() - Destroy session
Completely clears user authentication data and session.
app.post('/logout').use(async () => {
const result = await sessionUserDataCtx.destroy()
if (result) {
return Response.json({ message: 'Successfully logged out' })
}
return Response.status(500).json({ error: 'Logout failed' })
})Return values:
true: Successfully destroyedfalse: Operation failed (e.g., session doesn't exist)undefined: Internal error
5. saveToStore() - Manually save to storage
When autoSave: false, you need to manually call this method to save data.
const sessionMiddleware = createAuth({
sessionUserDataCtx,
authParser: cookieSessionParser(),
authStore: cookieSessionStore({ secret: 'secret-key' }),
autoSave: false // Disable auto-save
})
app.post('/save-progress').use(async () => {
sessionUserDataCtx.set({ ...userData, progress: 50 })
// Manual save
const saved = await sessionUserDataCtx.saveToStore()
if (saved) {
return Response.json({ message: 'Progress saved' })
}
return Response.status(500).json({ error: 'Save failed' })
})Return values:
true: Successfully savedfalse: Save failedundefined: Internal error
6. isModified - Check if data was modified
Read-only property to check if data was modified in the current request.
app.use((request, next) => {
const response = next()
// Log session modification status
if (sessionUserDataCtx.isModified) {
console.log(`Session modified for ${request.pathname}`)
}
return response
})Custom Adapter Development
Core Concepts
farrow-auth-session achieves decoupling through two interfaces:
- SessionStore: Data storage (Redis, database, cookies, etc.)
- SessionParser: Credential parsing (extract from request, set in response)
They communicate via sessionMetaDataCtx to pass session metadata (sessionId, expiration time).
Implementing SessionStore
import { SessionStore, sessionMetaDataCtx } from 'farrow-auth-session'
class RedisStore<T> implements SessionStore<T, string> {
async get(sessionId: string) {
const data = await redis.get(sessionId)
if (!data) return null // Not exists
// Set metadata for Parser
sessionMetaDataCtx.set({
sessionId,
expiresTime: data.expires
})
return data.value
}
async set(userData: T) {
const meta = sessionMetaDataCtx.get()
if (!meta) return false
await redis.set(meta.sessionId, userData)
return true
}
async create(userData?: T) {
const sessionId = generateId()
const expiresTime = Date.now() + 3600000
await redis.set(sessionId, userData || {})
sessionMetaDataCtx.set({ sessionId, expiresTime })
return userData || {} as T
}
async destroy() {
const meta = sessionMetaDataCtx.get()
if (!meta) return false
await redis.del(meta.sessionId)
return true
}
// Optional: Update expiry only
async touch() {
const meta = sessionMetaDataCtx.get()
if (!meta) return false
await redis.expire(meta.sessionId, 3600)
return true
}
}Implementing SessionParser
import { SessionParser, sessionMetaDataCtx, Response } from 'farrow-auth-session'
class HeaderParser implements SessionParser<string> {
async get(request) {
// Extract from request header
return request.headers?.['x-session-id'] || null
}
async set() {
const meta = sessionMetaDataCtx.get()
if (!meta) return Response
// Set response header
return Response.header('X-Session-Id', meta.sessionId)
}
async remove() {
return Response.header('X-Session-Id', '')
}
}Return Value Convention
- Success: Return data or
true - Failure: Return
nullorfalse - Error: Return
undefined
Real-world Example
See fa-session-redis (experimental but tested)
API Reference
createSession(config)
Creates authentication middleware.
config.sessionUserDataCtx- Context for user data storageconfig.sessionParser- Parser for credentials (cookies, headers, etc.)config.sessionStore- Storage backend for session dataconfig.autoSave- Automatically save modified sessionsconfig.autoCreateOnMissing- Automatically create new sessions when missing
createSessionCtx(defaultData)
Creates a typed authentication context.
cookieSessionParser(options?)
Creates a cookie-based session ID parser.
cookieSessionStore(options?)
Creates an encrypted cookie-based session store.
SessionStore<UserData, Credit>
Interface for custom storage implementations.
SessionParser
Interface for custom credential parsers.
Utilities
import { oneMinute, oneHour, oneDay, oneWeek } from 'farrow-auth-session'
// Time constants in seconds
const sessionDuration = 2 * oneHour * 1000 // 2 hours in millisecondsTypeScript Support
The library provides full TypeScript support with type inference:
import { InferUserData, InferCredit } from 'farrow-auth-session'
// Infer types from config
type MyUserData = InferUserData<typeof authConfig>
type MyCredit = InferCredit<typeof authConfig>License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
