Authentication
Secure your Gluonic sync endpoints with authentication.
Overview
The auth function runs before every sync request:
GET /sync/v1/bootstrapGET /sync/v1/deltaGET /sync/v1/ws(WebSocket)POST /sync/v1/tx
Responsibility: Verify user identity before allowing access to sync operations.
JWT Authentication (Recommended)
Use the built-in JWTAuth helper:
import { SyncServer } from '@gluonic/server'
import { JWTAuth } from '@gluonic/auth-jwt'
const server = SyncServer({
database,
auth: JWTAuth({
secret: process.env.JWT_SECRET,
expiresIn: '7d', // Optional: token expiration
getUserId: (decoded) => decoded.userId // Optional: extract user ID
})
})API Reference: JWTAuth - Complete authentication options
What JWTAuth does:
- Extracts token from
Authorization: Bearer {token}header - For WebSocket: Also checks
?token={token}query parameter - Verifies JWT signature
- Attaches decoded user to
req.user - Handles errors with clear messages
Creating Tokens
// Sign in endpoint
app.post('/auth/signin', async (req, reply) => {
const { email, password } = req.body
// Verify credentials
const user = await verifyCredentials(email, password)
if (!user) {
return reply.status(401).send({ error: 'Invalid credentials' })
}
// Create JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '7d' }
)
reply.send({ token, user })
})Session-Based Authentication
Using Database Sessions
export async function requireAuth(req: FastifyRequest) {
const sessionToken = req.headers.authorization?.replace('Bearer ', '')
if (!sessionToken) {
throw new Error('No session token')
}
// Lookup session in database
const session = await prisma.session.findUnique({
where: { token: sessionToken },
include: { user: true }
})
if (!session) {
throw new Error('Invalid session')
}
if (session.expiresAt < new Date()) {
throw new Error('Session expired')
}
// Attach user
;(req as any).user = session.user
}Using Redis Sessions
import { Redis } from 'ioredis'
const redis = new Redis(process.env.REDIS_URL)
export async function requireAuth(req: FastifyRequest) {
const sessionToken = req.headers.authorization?.replace('Bearer ', '')
if (!sessionToken) {
throw new Error('No session token')
}
// Lookup in Redis
const sessionData = await redis.get(`session:${sessionToken}`)
if (!sessionData) {
throw new Error('Invalid or expired session')
}
const session = JSON.parse(sessionData)
// Attach user
;(req as any).user = session.user
}API Key Authentication
For server-to-server or testing:
export async function requireAuth(req: FastifyRequest) {
const apiKey = req.headers['x-api-key'] as string
if (!apiKey) {
throw new Error('No API key provided')
}
// Lookup API key
const key = await prisma.apiKey.findUnique({
where: { key: apiKey },
include: { user: true }
})
if (!key || !key.active) {
throw new Error('Invalid API key')
}
// Attach user
;(req as any).user = key.user
}OAuth Integration
For third-party OAuth providers:
export async function requireAuth(req: FastifyRequest) {
const accessToken = req.headers.authorization?.replace('Bearer ', '')
if (!accessToken) {
throw new Error('No access token')
}
// Verify with OAuth provider (e.g., Google)
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` }
})
if (!response.ok) {
throw new Error('Invalid OAuth token')
}
const oauthUser = await response.json()
// Find or create user
let user = await prisma.user.findUnique({
where: { email: oauthUser.email }
})
if (!user) {
user = await prisma.user.create({
data: {
email: oauthUser.email,
name: oauthUser.name
}
})
}
;(req as any).user = user
}WebSocket Authentication
WebSocket connections need special handling:
export async function requireAuth(req: FastifyRequest) {
// HTTP requests: Bearer token in header
let token = req.headers.authorization?.replace('Bearer ', '')
// WebSocket: Token in query param (can't set headers on WebSocket)
if (!token && (req as any).query?.token) {
token = (req as any).query.token as string
}
if (!token) {
throw new Error('No authorization token')
}
// Verify token (same logic for both HTTP and WS)
const decoded = jwt.verify(token, JWT_SECRET)
;(req as any).user = decoded
}Client sends:
// WebSocket connection
const ws = new WebSocket('wss://api.example.com/sync/v1/ws?token=YOUR_JWT_TOKEN')Extracting Organization ID
After authentication, extract the org ID:
await registerSyncRoutes(app, {
auth: requireAuth,
// Extract org ID from authenticated user
adapter: PrismaAdapter({
prisma,
autoDiscover: {
enabled: true,
ownershipFields: ['userId'] // This field stores the org ID
}
})
})Note: The adapter uses the authenticated user’s ID as the orgId for filtering data.
Multi-Tenant Setup
For multi-tenant apps with separate organizations:
export async function requireAuth(req: FastifyRequest) {
const token = req.headers.authorization?.replace('Bearer ', '')
const decoded = jwt.verify(token, JWT_SECRET) as {
userId: string
organizationId: string // User belongs to an org
}
// Attach BOTH user and org
;(req as any).user = {
id: decoded.userId,
organizationId: decoded.organizationId
}
}
// Then use organizationId for filtering
await registerSyncRoutes(app, {
adapter: PrismaAdapter({
prisma,
autoDiscover: {
ownershipFields: ['organizationId'] // Filter by org, not user
}
})
})Rate Limiting
Add rate limiting to prevent abuse:
import rateLimit from '@fastify/rate-limit'
// Global rate limit
app.register(rateLimit, {
max: 100,
timeWindow: '1 minute'
})
// Or per-endpoint
await registerSyncRoutes(app, {
auth: async (req) => {
await requireAuth(req)
// Check rate limit after auth
const userId = (req as any).user.id
await checkRateLimit(userId, 'sync')
}
})Error Handling
export async function requireAuth(req: FastifyRequest) {
try {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
throw new Error('MISSING_TOKEN')
}
const decoded = jwt.verify(token, JWT_SECRET)
;(req as any).user = decoded
} catch (error: any) {
// Log authentication failures
console.error('Auth failed:', {
error: error.message,
path: req.url,
ip: req.ip
})
// Throw with clear error message
if (error.message === 'MISSING_TOKEN') {
throw new Error('No authorization token provided')
}
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired')
}
throw new Error('Invalid authorization token')
}
}Testing
Unit Tests
import { requireAuth } from './auth'
describe('requireAuth', () => {
it('should attach user to request', async () => {
const req = {
headers: {
authorization: 'Bearer valid-jwt-token'
}
} as any
await requireAuth(req)
expect(req.user).toBeDefined()
expect(req.user.id).toBe('user-123')
})
it('should throw on missing token', async () => {
const req = { headers: {} } as any
await expect(requireAuth(req)).rejects.toThrow('No authorization token')
})
})Integration Tests
import { createTestServer } from './test-utils'
describe('Sync endpoints', () => {
it('should require auth', async () => {
const app = await createTestServer()
// Without auth
const res1 = await app.inject({
method: 'GET',
url: '/sync/v1/bootstrap'
})
expect(res1.statusCode).toBe(401)
// With auth
const res2 = await app.inject({
method: 'GET',
url: '/sync/v1/bootstrap',
headers: {
authorization: `Bearer ${validToken}`
}
})
expect(res2.statusCode).toBe(200)
})
})Best Practices
DO âś…
- Use HTTPS in production (never HTTP)
- Rotate JWT secrets regularly
- Set reasonable token expiration (7-30 days)
- Log authentication failures
- Rate limit auth endpoints
- Validate token structure before verifying
DON’T ❌
- Don’t expose JWT secret in client code
- Don’t accept tokens without expiration
- Don’t skip auth on any endpoint
- Don’t log sensitive tokens
- Don’t use weak secrets in production
Next Steps
- Authorization - Permission models
- Deployment - Production security
- API Reference - Built-in auth helpers (coming soon)
Last updated on