Skip to content

AisonSu/farrow-auth-session

Repository files navigation

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-session

Quick 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 regenerated
  • false: 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 destroyed
  • false: 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 saved
  • false: Save failed
  • undefined: 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 null or false
  • 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 storage
  • config.sessionParser - Parser for credentials (cookies, headers, etc.)
  • config.sessionStore - Storage backend for session data
  • config.autoSave - Automatically save modified sessions
  • config.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 milliseconds

TypeScript 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.

About

Session-based authentication middleware for Farrow HTTP framework with flexible storage and parsing layers.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors