Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
735 lines
20 KiB
Markdown
735 lines
20 KiB
Markdown
# SaaS Security & Compliance
|
||
|
||
**Meta-Description:** SaaS Security Best Practices. SOC 2, GDPR Compliance, Data Encryption und Security Architecture für B2B SaaS.
|
||
|
||
**Keywords:** SaaS Security, SOC 2, GDPR, Data Encryption, Authentication, Compliance, Security Architecture
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
**Security** ist für B2B SaaS nicht optional – es ist Verkaufsargument. Von **SOC 2** bis **GDPR** erwarten Enterprise-Kunden nachweisbare Sicherheit. Dieser Guide zeigt Security Best Practices und Compliance-Anforderungen.
|
||
|
||
---
|
||
|
||
## Security Architecture Overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ SAAS SECURITY ARCHITECTURE │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Defense in Depth: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Layer 1: Network Security │ │
|
||
│ │ ├── WAF (Web Application Firewall) │ │
|
||
│ │ ├── DDoS Protection │ │
|
||
│ │ ├── Rate Limiting │ │
|
||
│ │ └── IP Whitelisting (optional) │ │
|
||
│ ├─────────────────────────────────────────────────────│ │
|
||
│ │ Layer 2: Application Security │ │
|
||
│ │ ├── Authentication (MFA, SSO) │ │
|
||
│ │ ├── Authorization (RBAC, ABAC) │ │
|
||
│ │ ├── Input Validation │ │
|
||
│ │ └── CSRF/XSS Protection │ │
|
||
│ ├─────────────────────────────────────────────────────│ │
|
||
│ │ Layer 3: Data Security │ │
|
||
│ │ ├── Encryption at Rest (AES-256) │ │
|
||
│ │ ├── Encryption in Transit (TLS 1.3) │ │
|
||
│ │ ├── Key Management (KMS) │ │
|
||
│ │ └── Data Masking/Tokenization │ │
|
||
│ ├─────────────────────────────────────────────────────│ │
|
||
│ │ Layer 4: Infrastructure Security │ │
|
||
│ │ ├── Private Networks (VPC) │ │
|
||
│ │ ├── Security Groups │ │
|
||
│ │ ├── Secrets Management │ │
|
||
│ │ └── Container Security │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Compliance Frameworks: │
|
||
│ ├── SOC 2 Type II │
|
||
│ ├── GDPR │
|
||
│ ├── HIPAA (Healthcare) │
|
||
│ ├── PCI DSS (Payments) │
|
||
│ └── ISO 27001 │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Authentication & Authorization
|
||
|
||
```typescript
|
||
// lib/auth/config.ts
|
||
import { NextAuthConfig } from 'next-auth';
|
||
import { PrismaAdapter } from '@auth/prisma-adapter';
|
||
import { db } from '@/lib/db';
|
||
import Google from 'next-auth/providers/google';
|
||
import Credentials from 'next-auth/providers/credentials';
|
||
import { compare } from 'bcryptjs';
|
||
|
||
export const authConfig: NextAuthConfig = {
|
||
adapter: PrismaAdapter(db),
|
||
providers: [
|
||
Google({
|
||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
|
||
}),
|
||
Credentials({
|
||
credentials: {
|
||
email: { label: 'Email', type: 'email' },
|
||
password: { label: 'Password', type: 'password' }
|
||
},
|
||
async authorize(credentials) {
|
||
if (!credentials?.email || !credentials?.password) {
|
||
return null;
|
||
}
|
||
|
||
const user = await db.user.findUnique({
|
||
where: { email: credentials.email as string },
|
||
include: { tenant: true }
|
||
});
|
||
|
||
if (!user || !user.passwordHash) {
|
||
return null;
|
||
}
|
||
|
||
const isValid = await compare(
|
||
credentials.password as string,
|
||
user.passwordHash
|
||
);
|
||
|
||
if (!isValid) {
|
||
// Log failed attempt
|
||
await logSecurityEvent('failed_login', {
|
||
email: credentials.email,
|
||
ip: 'unknown' // Get from request
|
||
});
|
||
return null;
|
||
}
|
||
|
||
// Check if MFA is required
|
||
if (user.mfaEnabled) {
|
||
return { ...user, requiresMfa: true };
|
||
}
|
||
|
||
return user;
|
||
}
|
||
})
|
||
],
|
||
callbacks: {
|
||
async jwt({ token, user }) {
|
||
if (user) {
|
||
token.tenantId = user.tenantId;
|
||
token.role = user.role;
|
||
token.permissions = await getUserPermissions(user.id);
|
||
}
|
||
return token;
|
||
},
|
||
async session({ session, token }) {
|
||
session.user.tenantId = token.tenantId;
|
||
session.user.role = token.role;
|
||
session.user.permissions = token.permissions;
|
||
return session;
|
||
}
|
||
},
|
||
pages: {
|
||
signIn: '/auth/login',
|
||
error: '/auth/error'
|
||
},
|
||
session: {
|
||
strategy: 'jwt',
|
||
maxAge: 24 * 60 * 60 // 24 hours
|
||
},
|
||
events: {
|
||
async signIn({ user, isNewUser }) {
|
||
await logSecurityEvent('login', {
|
||
userId: user.id,
|
||
isNewUser
|
||
});
|
||
},
|
||
async signOut({ token }) {
|
||
await logSecurityEvent('logout', {
|
||
userId: token.sub
|
||
});
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
```typescript
|
||
// lib/auth/mfa.ts
|
||
import { authenticator } from 'otplib';
|
||
import { db } from '@/lib/db';
|
||
import { encrypt, decrypt } from '@/lib/crypto';
|
||
|
||
export async function setupMFA(userId: string): Promise<{
|
||
secret: string;
|
||
qrCodeUrl: string;
|
||
}> {
|
||
const secret = authenticator.generateSecret();
|
||
|
||
// Encrypt secret before storing
|
||
const encryptedSecret = await encrypt(secret);
|
||
|
||
await db.user.update({
|
||
where: { id: userId },
|
||
data: {
|
||
mfaSecret: encryptedSecret,
|
||
mfaEnabled: false // Enable after verification
|
||
}
|
||
});
|
||
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId },
|
||
select: { email: true }
|
||
});
|
||
|
||
const otpAuthUrl = authenticator.keyuri(
|
||
user!.email,
|
||
'YourApp',
|
||
secret
|
||
);
|
||
|
||
return {
|
||
secret,
|
||
qrCodeUrl: otpAuthUrl
|
||
};
|
||
}
|
||
|
||
export async function verifyMFAToken(
|
||
userId: string,
|
||
token: string
|
||
): Promise<boolean> {
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId },
|
||
select: { mfaSecret: true }
|
||
});
|
||
|
||
if (!user?.mfaSecret) {
|
||
return false;
|
||
}
|
||
|
||
const secret = await decrypt(user.mfaSecret);
|
||
const isValid = authenticator.verify({ token, secret });
|
||
|
||
if (isValid) {
|
||
// Enable MFA if this is setup verification
|
||
await db.user.update({
|
||
where: { id: userId },
|
||
data: { mfaEnabled: true }
|
||
});
|
||
}
|
||
|
||
return isValid;
|
||
}
|
||
|
||
// Generate backup codes
|
||
export async function generateBackupCodes(userId: string): Promise<string[]> {
|
||
const codes: string[] = [];
|
||
|
||
for (let i = 0; i < 10; i++) {
|
||
codes.push(generateSecureCode(8));
|
||
}
|
||
|
||
// Hash and store codes
|
||
const hashedCodes = await Promise.all(
|
||
codes.map(code => hashCode(code))
|
||
);
|
||
|
||
await db.mfaBackupCode.createMany({
|
||
data: hashedCodes.map(hash => ({
|
||
userId,
|
||
codeHash: hash,
|
||
used: false
|
||
}))
|
||
});
|
||
|
||
return codes; // Return plain codes to show user once
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Role-Based Access Control
|
||
|
||
```typescript
|
||
// lib/auth/rbac.ts
|
||
export const permissions = {
|
||
// Project permissions
|
||
'project:read': 'View projects',
|
||
'project:create': 'Create projects',
|
||
'project:update': 'Update projects',
|
||
'project:delete': 'Delete projects',
|
||
|
||
// User management
|
||
'user:read': 'View users',
|
||
'user:invite': 'Invite users',
|
||
'user:update': 'Update users',
|
||
'user:delete': 'Remove users',
|
||
|
||
// Billing
|
||
'billing:read': 'View billing',
|
||
'billing:manage': 'Manage billing',
|
||
|
||
// Admin
|
||
'admin:settings': 'Manage settings',
|
||
'admin:audit': 'View audit logs'
|
||
} as const;
|
||
|
||
export type Permission = keyof typeof permissions;
|
||
|
||
export const roles = {
|
||
owner: {
|
||
name: 'Owner',
|
||
permissions: Object.keys(permissions) as Permission[]
|
||
},
|
||
admin: {
|
||
name: 'Admin',
|
||
permissions: [
|
||
'project:read', 'project:create', 'project:update', 'project:delete',
|
||
'user:read', 'user:invite', 'user:update',
|
||
'billing:read', 'admin:settings'
|
||
] as Permission[]
|
||
},
|
||
member: {
|
||
name: 'Member',
|
||
permissions: [
|
||
'project:read', 'project:create', 'project:update',
|
||
'user:read'
|
||
] as Permission[]
|
||
},
|
||
viewer: {
|
||
name: 'Viewer',
|
||
permissions: ['project:read', 'user:read'] as Permission[]
|
||
}
|
||
} as const;
|
||
|
||
export type Role = keyof typeof roles;
|
||
|
||
// Permission check middleware
|
||
export function requirePermission(permission: Permission) {
|
||
return async (req: Request) => {
|
||
const session = await getServerSession();
|
||
|
||
if (!session?.user) {
|
||
throw new Error('Unauthorized');
|
||
}
|
||
|
||
const userPermissions = session.user.permissions || [];
|
||
|
||
if (!userPermissions.includes(permission)) {
|
||
await logSecurityEvent('permission_denied', {
|
||
userId: session.user.id,
|
||
permission,
|
||
resource: req.url
|
||
});
|
||
throw new Error('Forbidden');
|
||
}
|
||
};
|
||
}
|
||
|
||
// React hook for permission checks
|
||
export function usePermission(permission: Permission): boolean {
|
||
const { data: session } = useSession();
|
||
return session?.user?.permissions?.includes(permission) ?? false;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Data Encryption
|
||
|
||
```typescript
|
||
// lib/crypto/encryption.ts
|
||
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
|
||
import { promisify } from 'util';
|
||
|
||
const scryptAsync = promisify(scrypt);
|
||
|
||
const ALGORITHM = 'aes-256-gcm';
|
||
const KEY_LENGTH = 32;
|
||
const IV_LENGTH = 16;
|
||
const AUTH_TAG_LENGTH = 16;
|
||
|
||
// Derive key from master key
|
||
async function deriveKey(masterKey: string, salt: Buffer): Promise<Buffer> {
|
||
return scryptAsync(masterKey, salt, KEY_LENGTH) as Promise<Buffer>;
|
||
}
|
||
|
||
export async function encrypt(plaintext: string): Promise<string> {
|
||
const masterKey = process.env.ENCRYPTION_KEY!;
|
||
const salt = randomBytes(16);
|
||
const key = await deriveKey(masterKey, salt);
|
||
const iv = randomBytes(IV_LENGTH);
|
||
|
||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||
const encrypted = Buffer.concat([
|
||
cipher.update(plaintext, 'utf8'),
|
||
cipher.final()
|
||
]);
|
||
const authTag = cipher.getAuthTag();
|
||
|
||
// Combine: salt + iv + authTag + encrypted
|
||
const combined = Buffer.concat([salt, iv, authTag, encrypted]);
|
||
return combined.toString('base64');
|
||
}
|
||
|
||
export async function decrypt(ciphertext: string): Promise<string> {
|
||
const masterKey = process.env.ENCRYPTION_KEY!;
|
||
const combined = Buffer.from(ciphertext, 'base64');
|
||
|
||
// Extract components
|
||
const salt = combined.subarray(0, 16);
|
||
const iv = combined.subarray(16, 32);
|
||
const authTag = combined.subarray(32, 48);
|
||
const encrypted = combined.subarray(48);
|
||
|
||
const key = await deriveKey(masterKey, salt);
|
||
|
||
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||
decipher.setAuthTag(authTag);
|
||
|
||
const decrypted = Buffer.concat([
|
||
decipher.update(encrypted),
|
||
decipher.final()
|
||
]);
|
||
|
||
return decrypted.toString('utf8');
|
||
}
|
||
|
||
// Field-level encryption for sensitive data
|
||
export class EncryptedField {
|
||
static async encrypt(value: string): Promise<string> {
|
||
return encrypt(value);
|
||
}
|
||
|
||
static async decrypt(value: string): Promise<string> {
|
||
return decrypt(value);
|
||
}
|
||
}
|
||
|
||
// Example usage in Prisma
|
||
// middleware for automatic encryption
|
||
export function encryptionMiddleware() {
|
||
return async (params: any, next: any) => {
|
||
// Define fields that need encryption
|
||
const encryptedFields: Record<string, string[]> = {
|
||
User: ['ssn', 'taxId'],
|
||
PaymentMethod: ['cardNumber', 'cvv'],
|
||
Integration: ['accessToken', 'refreshToken']
|
||
};
|
||
|
||
const fields = encryptedFields[params.model];
|
||
if (!fields) return next(params);
|
||
|
||
// Encrypt on create/update
|
||
if (['create', 'update', 'upsert'].includes(params.action)) {
|
||
for (const field of fields) {
|
||
if (params.args.data?.[field]) {
|
||
params.args.data[field] = await encrypt(params.args.data[field]);
|
||
}
|
||
}
|
||
}
|
||
|
||
const result = await next(params);
|
||
|
||
// Decrypt on read
|
||
if (['findUnique', 'findFirst', 'findMany'].includes(params.action) && result) {
|
||
const items = Array.isArray(result) ? result : [result];
|
||
for (const item of items) {
|
||
for (const field of fields) {
|
||
if (item[field]) {
|
||
item[field] = await decrypt(item[field]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Audit Logging
|
||
|
||
```typescript
|
||
// lib/security/audit.ts
|
||
import { db } from '@/lib/db';
|
||
import { headers } from 'next/headers';
|
||
|
||
interface AuditEvent {
|
||
action: string;
|
||
userId?: string;
|
||
tenantId?: string;
|
||
resourceType?: string;
|
||
resourceId?: string;
|
||
details?: Record<string, unknown>;
|
||
ip?: string;
|
||
userAgent?: string;
|
||
}
|
||
|
||
export async function logSecurityEvent(
|
||
action: string,
|
||
details: Record<string, unknown> = {}
|
||
): Promise<void> {
|
||
const headersList = await headers();
|
||
|
||
const event: AuditEvent = {
|
||
action,
|
||
details,
|
||
ip: headersList.get('x-forwarded-for') || headersList.get('x-real-ip') || 'unknown',
|
||
userAgent: headersList.get('user-agent') || 'unknown'
|
||
};
|
||
|
||
await db.auditLog.create({
|
||
data: {
|
||
...event,
|
||
details: event.details,
|
||
createdAt: new Date()
|
||
}
|
||
});
|
||
|
||
// For critical events, also alert
|
||
if (isCriticalEvent(action)) {
|
||
await sendSecurityAlert(event);
|
||
}
|
||
}
|
||
|
||
function isCriticalEvent(action: string): boolean {
|
||
const criticalActions = [
|
||
'failed_login_attempt',
|
||
'permission_denied',
|
||
'data_export',
|
||
'user_deleted',
|
||
'api_key_created',
|
||
'mfa_disabled',
|
||
'suspicious_activity'
|
||
];
|
||
|
||
return criticalActions.includes(action);
|
||
}
|
||
|
||
// Audit log query for compliance
|
||
export async function getAuditLogs(
|
||
tenantId: string,
|
||
options: {
|
||
startDate?: Date;
|
||
endDate?: Date;
|
||
action?: string;
|
||
userId?: string;
|
||
limit?: number;
|
||
offset?: number;
|
||
}
|
||
) {
|
||
return db.auditLog.findMany({
|
||
where: {
|
||
tenantId,
|
||
...(options.action && { action: options.action }),
|
||
...(options.userId && { userId: options.userId }),
|
||
createdAt: {
|
||
...(options.startDate && { gte: options.startDate }),
|
||
...(options.endDate && { lte: options.endDate })
|
||
}
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
take: options.limit || 100,
|
||
skip: options.offset || 0
|
||
});
|
||
}
|
||
|
||
// Data retention (GDPR compliance)
|
||
export async function purgeOldAuditLogs(retentionDays: number = 365): Promise<number> {
|
||
const cutoffDate = new Date();
|
||
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
||
|
||
const result = await db.auditLog.deleteMany({
|
||
where: {
|
||
createdAt: { lt: cutoffDate }
|
||
}
|
||
});
|
||
|
||
return result.count;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## GDPR Compliance
|
||
|
||
```typescript
|
||
// lib/gdpr/data-subject.ts
|
||
import { db } from '@/lib/db';
|
||
import { encrypt } from '@/lib/crypto';
|
||
|
||
// Data export (Right to Portability)
|
||
export async function exportUserData(userId: string): Promise<object> {
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId },
|
||
include: {
|
||
profile: true,
|
||
projects: true,
|
||
activities: true,
|
||
preferences: true
|
||
}
|
||
});
|
||
|
||
if (!user) {
|
||
throw new Error('User not found');
|
||
}
|
||
|
||
// Remove internal fields
|
||
const exportData = {
|
||
personalInfo: {
|
||
email: user.email,
|
||
name: user.name,
|
||
createdAt: user.createdAt
|
||
},
|
||
profile: user.profile,
|
||
projects: user.projects.map(p => ({
|
||
name: p.name,
|
||
createdAt: p.createdAt
|
||
})),
|
||
preferences: user.preferences,
|
||
activityLog: user.activities.slice(0, 1000) // Limit
|
||
};
|
||
|
||
// Log the export
|
||
await logSecurityEvent('data_export', {
|
||
userId,
|
||
dataTypes: Object.keys(exportData)
|
||
});
|
||
|
||
return exportData;
|
||
}
|
||
|
||
// Data deletion (Right to Erasure)
|
||
export async function deleteUserData(
|
||
userId: string,
|
||
options: { hardDelete?: boolean } = {}
|
||
): Promise<void> {
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId }
|
||
});
|
||
|
||
if (!user) {
|
||
throw new Error('User not found');
|
||
}
|
||
|
||
if (options.hardDelete) {
|
||
// Permanent deletion
|
||
await db.$transaction([
|
||
db.activity.deleteMany({ where: { userId } }),
|
||
db.project.deleteMany({ where: { ownerId: userId } }),
|
||
db.profile.delete({ where: { userId } }),
|
||
db.user.delete({ where: { id: userId } })
|
||
]);
|
||
} else {
|
||
// Soft delete with anonymization
|
||
await db.$transaction([
|
||
db.user.update({
|
||
where: { id: userId },
|
||
data: {
|
||
email: `deleted_${userId}@anonymized.local`,
|
||
name: 'Deleted User',
|
||
deletedAt: new Date(),
|
||
passwordHash: null,
|
||
mfaSecret: null
|
||
}
|
||
}),
|
||
db.profile.update({
|
||
where: { userId },
|
||
data: {
|
||
bio: null,
|
||
avatar: null,
|
||
phone: null
|
||
}
|
||
})
|
||
]);
|
||
}
|
||
|
||
await logSecurityEvent('user_deleted', {
|
||
userId,
|
||
hardDelete: options.hardDelete
|
||
});
|
||
}
|
||
|
||
// Consent management
|
||
export async function updateConsent(
|
||
userId: string,
|
||
consents: {
|
||
marketing?: boolean;
|
||
analytics?: boolean;
|
||
thirdParty?: boolean;
|
||
}
|
||
): Promise<void> {
|
||
await db.userConsent.upsert({
|
||
where: { userId },
|
||
create: {
|
||
userId,
|
||
...consents,
|
||
updatedAt: new Date()
|
||
},
|
||
update: {
|
||
...consents,
|
||
updatedAt: new Date()
|
||
}
|
||
});
|
||
|
||
await logSecurityEvent('consent_updated', {
|
||
userId,
|
||
consents
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Security Checklist
|
||
|
||
| Category | Requirement | Priority |
|
||
|----------|-------------|----------|
|
||
| **Auth** | MFA for admins | Critical |
|
||
| **Auth** | SSO/SAML support | Important |
|
||
| **Data** | Encryption at rest | Critical |
|
||
| **Data** | Encryption in transit | Critical |
|
||
| **Access** | RBAC implementation | Critical |
|
||
| **Audit** | Comprehensive logging | Critical |
|
||
| **GDPR** | Data export capability | Required |
|
||
| **GDPR** | Data deletion process | Required |
|
||
| **Network** | WAF configured | Important |
|
||
| **Secrets** | Secure key management | Critical |
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
SaaS Security erfordert:
|
||
|
||
1. **Defense in Depth**: Mehrere Sicherheitsebenen
|
||
2. **Compliance**: SOC 2, GDPR von Anfang an
|
||
3. **Encryption**: Daten at rest und in transit
|
||
4. **Audit Trails**: Vollständige Nachverfolgbarkeit
|
||
|
||
Security ist kein Feature, sondern Grundvoraussetzung.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Security architecture layers diagram, defense in depth visualization"
|
||
2. "SOC 2 compliance dashboard, audit controls checklist"
|
||
3. "Encryption key management flow, secure vault concept"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [SOC 2 Compliance Guide](https://www.aicpa.org/resources/soc-2)
|
||
- [GDPR Official Text](https://gdpr.eu/)
|
||
- [OWASP Security Guidelines](https://owasp.org/www-project-web-security-testing-guide/)
|
||
- [AWS Security Best Practices](https://aws.amazon.com/security/security-resources/)
|