Skip to Content
GuidesServer GuidesAuthentication

Authentication

Secure your Gluonic sync endpoints with authentication.

Overview

The auth function runs before every sync request:

  • GET /sync/v1/bootstrap
  • GET /sync/v1/delta
  • GET /sync/v1/ws (WebSocket)
  • POST /sync/v1/tx

Responsibility: Verify user identity before allowing access to sync operations.

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

Last updated on