Security Project: Building a Secure API from Scratch
In this tutorial, you will learn about Security Project: Building a Secure API from Scratch. We cover key concepts, practical examples, and best practices to help you master this topic.
This project brings together everything you have learned about backend security. You will build a secure API for a healthcare application (HIPAA-relevant) with authentication, authorization, encryption, input validation, security logging, Rate Limiting, and secure deployment.
flowchart TB
subgraph Security Layers
TLS[TLS 1.3 + HSTS]
WAF[Rate Limiting + WAF Rules]
Auth[Authentication: JWT + MFA]
AuthZ[Authorization: RBAC + ABAC]
Input[Input Validation: Zod]
Encryption[Encryption: AES-256-GCM]
Audit[Audit Logging]
CSP[CSP + Secure Headers]
end
Request[Client Request] --> TLS
TLS --> WAF
WAF --> Auth
Auth --> AuthZ
AuthZ --> Input
Input --> Encryption
Encryption --> Audit
Audit --> Response[Secure Response + CSP Headers]
Project Requirements
Build the following components and integrate them into a secure API:
1. Secure Authentication Module
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class SecureAuth {
constructor() {
this.SALT_ROUNDS = 12;
this.ACCESS_TOKEN_EXPIRY = '15m';
this.REFRESH_TOKEN_EXPIRY = '7d';
}
async hashPassword(password) {
return bcrypt.hash(password, this.SALT_ROUNDS);
}
async verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
generateAccessToken(user) {
return jwt.sign(
{
sub: user.id,
role: user.role,
permissions: user.permissions,
type: 'access'
},
process.env.JWT_PRIVATE_KEY,
{
algorithm: 'RS256',
expiresIn: this.ACCESS_TOKEN_EXPIRY,
issuer: 'https://healthapi.example.com',
jwtid: crypto.randomUUID()
}
);
}
generateRefreshToken(user) {
return jwt.sign(
{
sub: user.id,
type: 'refresh',
tokenVersion: user.tokenVersion
},
process.env.JWT_PRIVATE_KEY,
{
algorithm: 'RS256',
expiresIn: this.REFRESH_TOKEN_EXPIRY,
issuer: 'https://healthapi.example.com',
jwtid: crypto.randomUUID()
}
);
}
async authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
algorithms: ['RS256'],
issuer: 'https://healthapi.example.com'
});
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
}
Expected output:
Login returns access token (15min) and refresh token (7d). All authenticated endpoints require RS256-signed JWT.
2. Encryption Module for PHI
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
class PHIEncryption {
constructor() {
this.key = Buffer.from(process.env.PHI_ENCRYPTION_KEY, 'hex');
}
encrypt(plaintext) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, this.key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return JSON.stringify({ iv: iv.toString('hex'), data: encrypted, tag: authTag });
}
decrypt(ciphertext) {
const { iv, data, tag } = JSON.parse(ciphertext);
const decipher = crypto.createDecipheriv(ALGORITHM, this.key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(tag, 'hex'));
let decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
async encryptPatientRecord(record) {
const encrypted = { ...record };
if (record.ssn) encrypted.ssn = this.encrypt(record.ssn);
if (record.diagnosis) encrypted.diagnosis = this.encrypt(record.diagnosis);
if (record.insuranceId) encrypted.insuranceId = this.encrypt(record.insuranceId);
return encrypted;
}
}
Expected output:
Patient PHI fields (SSN, diagnosis, insurance) are encrypted with AES-256-GCM. Decryption only possible with correct key.
3. Comprehensive Security Middleware
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const securityMiddleware = [
// Security headers
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"],
scriptSrc: ["'none'"],
styleSrc: ["'none'"],
imgSrc: ["'none'"],
connectSrc: ["'self'"],
fontSrc: ["'none'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"]
}
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}),
// Rate limiting
rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: true,
legacyHeaders: false
}),
// Input validation middleware
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
// Specific validation schemas
const patientValidation = [
body('email').isEmail().normalizeEmail(),
body('name').trim().isLength({ min: 1, max: 100 }).escape(),
body('ssn').matches(/^\d{3}-\d{2}-\d{4}$/),
body('dateOfBirth').isISO8601(),
body('phone').isMobilePhone('any')
];
Expected output:
All responses include security headers. Rate limited to 60 req/min. Input validated per endpoint schema.
Acceptance Criteria
- Authentication: JWT with RS256, 15-min access tokens, 7-day refresh tokens with rotation.
- Authorization: RBAC with roles (doctor, nurse, admin, patient). ABAC for patient data access (only assigned doctor).
- Encryption: AES-256-GCM for PHI fields (SSN, diagnosis, insurance).
- Input validation: Zod schemas on all endpoints.
- Rate limiting: 60 req/min general, 5 req/15min for login.
- Security headers: CSP, HSTS, XFO, X-Content-Type-Options.
- Audit logging: All PHI access logged with user, timestamp, and action.
- Container security: Distroless image, non-root user, read-only FS.
- Zero Trust: mTLS for internal service communication.
Testing
const supertest = require('supertest');
describe('Secure API Security Tests', () => {
let app;
let patientToken;
let doctorToken;
beforeAll(async () => {
app = await buildApp();
patientToken = await getTokenFor('patient');
doctorToken = await getTokenFor('doctor');
});
test('should reject requests without authentication', async () => {
await supertest(app)
.get('/api/patients/123')
.expect(401);
});
test('should reject expired tokens', async () => {
const expiredToken = generateExpiredToken();
await supertest(app)
.get('/api/patients/123')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
test('should enforce RBAC - patient cannot access admin', async () => {
await supertest(app)
.get('/api/admin/users')
.set('Authorization', `Bearer ${patientToken}`)
.expect(403);
});
test('should enforce ABAC - doctor can only access assigned patients', async () => {
await supertest(app)
.get('/api/patients/unassigned-456')
.set('Authorization', `Bearer ${doctorToken}`)
.expect(403);
});
test('should encrypt PHI in database', async () => {
const patient = await createPatient();
const raw = await db.query('SELECT ssn FROM patients WHERE id = ?', [patient.id]);
expect(raw[0].ssn).not.toContain(patient.ssn);
});
test('should have security headers', async () => {
const res = await supertest(app).get('/api/health');
expect(res.headers['strict-transport-security']).toBeDefined();
expect(res.headers['x-content-type-options']).toBe('nosniff');
expect(res.headers['x-frame-options']).toBe('DENY');
});
test('should rate limit login endpoint', async () => {
for (let i = 0; i < 6; i++) {
await supertest(app)
.post('/api/auth/login')
.send({ email: 'test@test.com', password: 'wrong' })
.expect(i < 5 ? 401 : 429);
}
});
test('should audit PHI access', async () => {
await supertest(app)
.get('/api/patients/123')
.set('Authorization', `Bearer ${doctorToken}`);
const auditLogs = await db.query(
'SELECT * FROM audit_log WHERE action = ? AND resource = ?',
['PATIENT_READ', '/api/patients/123']
);
expect(auditLogs.length).toBe(1);
expect(auditLogs[0].user_id).toBeDefined();
});
});
Expected output:
All 8 security tests pass: auth, token expiry, RBAC, ABAC, encryption, headers, rate limiting, audit logging.
Common Mistakes
- Skipping input validation on internal endpoints — internal services should validate data from other services.
- Encrypting data but not handling key rotation — implement envelope encryption with KMS.
- Implementing authorization but not testing every role-endpoint combination.
- Logging security events but never monitoring them — set up alerts on audit logs.
- Deploying without a security review — conduct at least a checklist-based review.
Submission Checklist
- JWT authentication with RS256
- RBAC + ABAC authorization
- AES-256-GCM encryption for PHI
- Input validation on all endpoints
- Rate limiting (global + login)
- Security headers (CSP, HSTS, etc.)
- Audit logging for sensitive operations
- Container hardening (distroless, non-root)
- Security test suite (8+ tests)
- Security review completed
What's Next
Congratulations on completing the backend security module. Continue to Backend Logging Patterns to learn about logging, Observability, and monitoring.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro