Security Model
APSO implements security at multiple layers, from authentication to data isolation.
Security Layers
Authentication
JWT Token Flow
Token Structure
{
"header": {
"alg": "RS256",
"typ": "JWT"
},
"payload": {
"sub": "user-uuid",
"email": "user@example.com",
"organizationId": "org-uuid",
"roles": ["admin"],
"iat": 1699000000,
"exp": 1699086400
}
}Token Validation
// JWT verification middleware
@Injectable()
export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get('JWT_SECRET'),
});
// Attach user context
request.user = {
id: payload.sub,
email: payload.email,
organizationId: payload.organizationId,
roles: payload.roles || [],
};
return true;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
}Multi-Tenancy
Organization Isolation
All data is scoped by organization:
// Every query includes organization filter
async findAll(user: User): Promise<Project[]> {
return this.repository.find({
where: {
organizationId: user.organizationId, // Always scoped
},
});
}Scoping Levels
| Level | Description | Use Case |
|---|---|---|
organization | Visible to all org members | Projects, settings |
user | Visible only to creator | Drafts, preferences |
global | Visible across all orgs | Shared resources |
public | No authentication required | Public content |
Data Scoping Configuration
{
"entities": [
{
"name": "Project",
"scoping": "organization"
},
{
"name": "UserSettings",
"scoping": "user"
}
]
}Authorization
Role-Based Access Control
// Define roles
enum Role {
ADMIN = 'admin',
MEMBER = 'member',
VIEWER = 'viewer',
}
// Role guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.roles?.includes(role));
}
}
// Usage
@Roles(Role.ADMIN)
@Delete(':id')
remove(@Param('id') id: string) {
return this.service.remove(id);
}Resource-Level Permissions
// Check ownership before modification
async update(id: string, dto: UpdateProjectDto, user: User) {
const project = await this.repository.findOne({
where: { id, organizationId: user.organizationId },
});
if (!project) {
throw new NotFoundException();
}
// Additional permission check
if (project.ownerId !== user.id && !user.roles.includes('admin')) {
throw new ForbiddenException('Not allowed to modify this resource');
}
return this.repository.save({ ...project, ...dto });
}Input Validation
Request Validation
All inputs are validated using class-validator:
export class CreateProjectDto {
@IsString()
@IsNotEmpty()
@MaxLength(200)
@Matches(/^[a-zA-Z0-9\s\-_]+$/, {
message: 'Name contains invalid characters',
})
name: string;
@IsOptional()
@IsString()
@MaxLength(5000)
description?: string;
@IsOptional()
@IsUUID()
parentId?: string;
}SQL Injection Prevention
TypeORM parameterized queries prevent SQL injection:
// Safe - parameterized
const results = await this.repository
.createQueryBuilder('project')
.where('project.name LIKE :search', { search: `%${query}%` })
.getMany();
// Never do this - vulnerable
// .where(`name LIKE '%${query}%'`)XSS Protection
Output encoding for all responses:
// Sanitize HTML content
import { sanitize } from 'class-sanitizer';
export class CreateCommentDto {
@Transform(({ value }) => sanitize(value))
@IsString()
content: string;
}Security Headers
// Helmet middleware for security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
}));Rate Limiting
// Throttle decorator
@Throttle({ default: { limit: 100, ttl: 60000 } })
@Controller('projects')
export class ProjectsController {}
// Per-route limits
@Throttle({ default: { limit: 5, ttl: 60000 } })
@Post('upload')
uploadFile() {}Audit Logging
// Log security-relevant events
@Injectable()
export class AuditLogger {
log(event: {
action: string;
userId: string;
resourceType: string;
resourceId: string;
metadata?: Record<string, any>;
}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
...event,
}));
}
}
// Usage
this.auditLogger.log({
action: 'DELETE',
userId: user.id,
resourceType: 'Project',
resourceId: id,
});Security Checklist
- JWT tokens with short expiration
- HTTPS enforced in production
- Input validation on all endpoints
- SQL injection prevention via ORM
- XSS protection via output encoding
- CORS configuration
- Rate limiting
- Security headers (HSTS, CSP)
- Multi-tenant data isolation
- Audit logging
Related
Last updated on