Authentication Guide
Pikku provides flexible authentication mechanisms that work across HTTP and WebSocket connections. You can authenticate users through middleware (automatic) or function-based approaches (manual), both integrating with the UserSessionService
.
Authentication Approaches
1. Middleware-Based Authentication (Automatic)
Middleware authentication runs automatically before your functions execute, extracting and validating user sessions from requests.
API Key Authentication
import { authAPIKey, addMiddleware } from "@pikku/core"
export const apiKeyMiddleware = () => {
return authAPIKey<any, any>({
source: 'all', // Check both headers and query params
getSessionForAPIKey: async (services, apiKey) => {
return services.kysely
.selectFrom('user')
.select(['userId', 'apiKey'])
.where('apiKey', '=', apiKey)
.executeTakeFirstOrThrow(() => new UnauthorizedError('Invalid API key'))
}
})
}
// Apply middleware globally
addMiddleware([apiKeyMiddleware()])
Cookie Authentication with JWT
import { authCookie, addMiddleware } from "@pikku/core"
export const cookieMiddleware = () => {
return authCookie({
name: 'pikku:session',
jwt: true,
expiresIn: { value: 4, unit: 'week' },
options: { sameSite: 'lax', path: '/' },
})
}
addMiddleware([cookieMiddleware()])
Bearer Token Authentication
import { authBearer, addMiddleware } from "@pikku/core"
export const bearerMiddleware = () => {
return authBearer({
jwt: true, // Decode JWT tokens
getSession: async (services, token) => {
// Custom token validation
return await services.jwt.decode(token)
}
})
}
addMiddleware([bearerMiddleware()])
2. Function-Based Authentication (Manual)
Function-based authentication gives you full control over the authentication process within your functions.
Login Function
export const loginUser = pikkuSessionlessFunc<
Pick<DB.User, 'name'>,
UserSession
>(async (services, { name }) => {
let session: UserSession | undefined
try {
// Try to find existing user
session = await services.kysely
.selectFrom('user')
.select(['userId', 'apiKey'])
.where('name', '=', name.toLowerCase())
.executeTakeFirstOrThrow()
} catch {
// Create new user if not found
session = await services.kysely
.insertInto('user')
.values({ name: name.toLowerCase() })
.returning(['userId', 'apiKey'])
.executeTakeFirstOrThrow()
}
// Set session using UserSessionService
services.userSession?.set(session)
return session
})
Logout Function
export const logoutUser = pikkuSessionlessFunc<void, void>(async (
services,
_data,
_session
) => {
// Clear session using UserSessionService
services.userSession?.clear()
})
HTTP Authentication
Route Configuration
import { addHTTPRoute } from "@pikku/core"
// Public route (no authentication)
addHTTPRoute({
method: 'post',
route: '/login',
func: loginUser,
auth: false,
})
// Protected route (requires authentication)
addHTTPRoute({
method: 'post',
route: '/logout',
func: logoutUser,
auth: true, // Middleware authentication required
})
// Route with permissions
addHTTPRoute({
method: 'patch',
route: '/user/:userId',
func: updateUser,
auth: true,
permissions: {
isUserUpdatingSelf, // Custom permission check
},
})
Permission System
import { APIPermission } from "@pikku/core"
export const isUserUpdatingSelf: APIPermission<Pick<DB.User, 'userId'>> = async (
_services,
data,
session
) => {
return session?.userId === data.userId
}
export const isTodoCreator: APIPermission<Pick<DB.Todo, 'todoId'>> = async (
services,
{ todoId },
session
) => {
const { createdBy } = await services.kysely
.selectFrom('todo')
.select('createdBy')
.where('todoId', '=', todoId)
.executeTakeFirstOrThrow()
return session?.userId === createdBy
}
WebSocket Authentication
Channel Configuration
import { addChannel } from "@pikku/core"
addChannel({
name: 'events',
route: '/',
onConnect,
onDisconnect,
auth: true, // Global auth requirement for the channel
onMessage,
onMessageRoute: {
action: {
// Authentication function (overrides global auth)
auth: {
func: authenticate,
auth: false, // This specific route doesn't require pre-auth
},
// Route with permissions
subscribe: {
func: subscribe,
permissions: {}, // Custom permissions
},
unsubscribe,
emit: emitMessage,
},
},
})
WebSocket Authentication Functions
// Manual authentication within WebSocket
export const authenticate = pikkuChannelFunc<
{ token: string; userId: string },
{ authResult: boolean; action: 'auth' }
>(async ({ userSession }, data) => {
const authResult = data.token === 'valid'
if (authResult) {
// Set session for this WebSocket connection
await userSession?.set({ userId: data.userId })
}
return { authResult, action: 'auth' }
})
// Authenticated WebSocket function
export const emitMessage = pikkuChannelFunc<
{ name: string },
{ timestamp: string; from: string } | { message: string }
>(async ({ channel, eventHub }, data, session) => {
// Session is automatically available from middleware or previous auth
await eventHub?.publish(data.name, channel.channelId, {
timestamp: new Date().toISOString(),
from: session?.userId ?? 'anonymous',
})
})
UserSessionService Integration
The UserSessionService
is the core component that manages user sessions across both HTTP and WebSocket contexts.
Setting Sessions
// In middleware or functions
services.userSession?.set(session) // Set new session
services.userSession?.setInitial(session) // Set initial session (middleware)
services.userSession?.clear() // Clear session
Getting Sessions
// In functions - session is automatically passed as third parameter
export const myFunction = pikkuFunc<InputType, OutputType>(
async (services, data, session) => {
// session is automatically available
console.log('User ID:', session?.userId)
}
)
// Or access directly from service
const currentSession = await services.userSession?.get()
JWT Service Configuration
import { JoseJWTService } from '@pikku/jose'
const jwt = new JoseJWTService(
async () => [
{
id: 'my-key',
value: 'your-secret-key', // Use environment variable in production
},
],
logger
)
Client-Side Authentication
HTTP Client
import { PikkuFetch } from './pikku-fetch.gen.js'
const pikkuFetch = new PikkuFetch({
serverUrl: process.env.API_BASE_URL
})
// Set authentication
pikkuFetch.setAPIKey(userApiKey)
// Make authenticated requests
const todos = await pikkuFetch.get('/todos')
WebSocket Client
import { PikkuWebSocket } from './pikku-websocket.gen.js'
const websocket = new PikkuWebSocket<'events'>(serverUrl, apiKey)
websocket.ws.onopen = async () => {
// Authenticate if needed
websocket.send({
action: 'auth',
token: 'valid-token',
userId: 'user123'
})
}
Best Practices
- Use middleware for global authentication - Apply authentication middleware globally for consistent security
- Combine approaches - Use middleware for automatic auth and functions for login/logout operations
- Leverage permissions - Use the permission system for fine-grained access control
- Secure JWT secrets - Store JWT secrets in environment variables, not in code
- Handle WebSocket auth early - Authenticate WebSocket connections as soon as they connect
- Session management - Use
UserSessionService
consistently across HTTP and WebSocket contexts
This comprehensive authentication system provides the flexibility to handle various authentication scenarios while maintaining type safety and consistency across your application.