Skip to content

Security.md

Codewriter90x edited this page Jan 24, 2026 · 1 revision

Security

This document covers authentication, authorization, secrets management, and security best practices.

Authentication

JWT Token-Based Authentication

OpenCashFlow uses JSON Web Tokens (JWT) for stateless authentication.

Token Structure

{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "user-id-guid",
    "email": "user@example.com",
    "tenant": "company-id-guid",
    "role": "Administrator",
    "permissions": ["CUST_PAYM_VIEW", "CUST_PAYM_NEW"],
    "iat": 1700000000,
    "exp": 1700000900,
    "iss": "https://api.opencashflow.local",
    "aud": "https://app.opencashflow.local"
  },
  "signature": "..."
}

Token Lifetimes

Token Type Duration Purpose
Access Token 15 minutes API authentication
Refresh Token 7 days Obtain new access tokens
Remember Me 30 days Extended sessions

Token Storage

Tokens are stored in HTTP-only cookies to prevent XSS attacks:

options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;

Authentication Flow

1. User submits credentials
         │
         ▼
2. API validates credentials against database
         │
         ▼
3. Generate JWT with user claims
         │
         ▼
4. Set HTTP-only cookie with token
         │
         ▼
5. Subsequent requests include cookie automatically
         │
         ▼
6. API validates token signature and expiry
         │
         ▼
7. Request proceeds if valid

Password Security

Hashing

Passwords are hashed using PBKDF2 with a unique salt per user:

public class PasswordHasher
{
    public (string hash, string salt) HashPassword(string password)
    {
        byte[] salt = RandomNumberGenerator.GetBytes(32);
        byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
            password,
            salt,
            iterations: 100000,
            hashAlgorithm: HashAlgorithmName.SHA256,
            outputLength: 32
        );

        return (Convert.ToBase64String(hash), Convert.ToBase64String(salt));
    }
}

Password Requirements

  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one digit
  • At least one special character

Multi-Factor Authentication (MFA)

MFA support is built into the user model:

public class AspNetUser
{
    public bool TwoFactorEnabled { get; set; }
    // TOTP secret stored encrypted
}

Authorization

Role-Based Access Control (RBAC)

Built-in Roles

Role Description Typical Permissions
Administrator Full company access All permissions
Employee Standard user View and create payments
GIManagers System administrators Cross-tenant access (hidden)

Role Assignment

Users are assigned roles via the AspNetUserRoles join table:

public class AspNetUserRole
{
    public Guid UserID { get; set; }
    public Guid RoleID { get; set; }
}

Permission System

Permissions follow a hierarchical naming convention:

{SECTION}_{SUBSECTION}_{ACTION}

Permission Categories

public static class Permissions
{
    // Company Management
    public const string COMP = "COMP";
    public const string COMP_VIEW = "COMP_VIEW";
    public const string COMP_EDIT = "COMP_EDIT";

    // Payments
    public const string CUST_PAYM = "CUST_PAYM";
    public const string CUST_PAYM_VIEW = "CUST_PAYM_VIEW";
    public const string CUST_PAYM_NEW = "CUST_PAYM_NEW";
    public const string CUST_PAYM_EDIT = "CUST_PAYM_EDIT";
    public const string CUST_PAYM_DELE = "CUST_PAYM_DELE";

    // Employees
    public const string EMPL = "EMPL";
    public const string EMPL_VIEW = "EMPL_VIEW";
    public const string EMPL_NEW = "EMPL_NEW";
    public const string EMPL_EDIT = "EMPL_EDIT";
    public const string EMPL_DELE = "EMPL_DELE";

    // Billing
    public const string BILL = "BILL";
    public const string BILL_VIEW = "BILL_VIEW";
    public const string BILL_EDIT = "BILL_EDIT";
}

Permission Evaluation

Permissions are evaluated in this order:

  1. Check if user has explicit denied permission → Deny
  2. Check if user has explicit assigned permission → Allow
  3. Check if user's role has the permission → Allow/Deny
  4. Default → Deny
public bool HasPermission(AspNetUser user, string permission)
{
    // Check explicit denials first
    if (user.DeniedPermissions?.Contains(permission) == true)
        return false;

    // Check explicit assignments
    if (user.AssignedPermissions?.Contains(permission) == true)
        return true;

    // Check role permissions
    return user.Roles.Any(r => r.Permissions.Contains(permission));
}

API Authorization

Attribute-Based Authorization

[ApiController]
[Authorize]
public class PaymentController : ControllerBase
{
    [HttpGet]
    [RequirePermission("CUST_PAYM_VIEW")]
    public async Task<IActionResult> GetAll() { }

    [HttpPost]
    [RequirePermission("CUST_PAYM_NEW")]
    public async Task<IActionResult> Create() { }

    [HttpDelete("{id}")]
    [RequirePermission("CUST_PAYM_DELE")]
    public async Task<IActionResult> Delete(Guid id) { }
}

Subscription Authorization

The SubscriptionAuthorizationMiddleware ensures users have active subscriptions:

public class SubscriptionAuthorizationMiddleware
{
    public async Task InvokeAsync(HttpContext context)
    {
        var tenantId = context.User.GetTenantId();
        var subscription = await _subscriptionService.GetActiveAsync(tenantId);

        if (subscription == null || subscription.Status != "active")
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsJsonAsync(new
            {
                success = false,
                message = "Active subscription required"
            });
            return;
        }

        await _next(context);
    }
}

Multi-Tenancy Security

Data Isolation

Every database query filters by TenantID to prevent cross-tenant data access:

public async Task<IEnumerable<Payment>> GetPaymentsAsync(Guid tenantId)
{
    return await _context.Payment_DS
        .Where(p => p.TenantID == tenantId)  // Tenant filter
        .Where(p => !p.IsDeleted)
        .ToListAsync();
}

Tenant Validation

Before any operation, validate the user belongs to the tenant:

private Guid GetCurrentTenantId()
{
    var claim = User.FindFirst("tenant");
    if (claim == null)
        throw new UnauthorizedException("No tenant claim found");

    return Guid.Parse(claim.Value);
}

Secrets Management

What NOT to Commit

These files must never be committed to version control:

File/Pattern Contains
.env Environment variables
appsettings.*.json (local) Local configuration
*.pfx, *.pem, *.key Certificates and keys
credentials.json API credentials
secrets.json User secrets

.gitignore Configuration

# Environment files
.env
.env.local
.env.*.local

# User secrets
secrets.json

# Certificates
*.pfx
*.pem
*.key
*.crt

# Local settings (except Development for reference)
appsettings.Production.json
appsettings.Staging.json

# IDE settings with potential secrets
.idea/
.vs/

Secret Rotation

Rotate these secrets before production deployment:

  1. JWT Secret Key - Generate new 64+ character key
  2. Database Password - Use strong, unique password
  3. Stripe API Keys - Switch from test to live keys
  4. SMTP Password - Use application-specific password

Environment Variables

Store secrets in environment variables, not configuration files:

# Production secrets (set in deployment environment)
export JWTSETTINGS__SECRETKEY="your-production-secret-key"
export DEFAULT_CONN_STRING="Host=prod-db;Password=prod-password"
export STRIPE__SECRETKEY="sk_live_..."

API Security

CORS Configuration

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins(allowedOrigins)
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

Rate Limiting

Implement rate limiting to prevent abuse:

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.Window = TimeSpan.FromMinutes(1);
        opt.PermitLimit = 100;
        opt.QueueLimit = 0;
    });
});

Input Validation

Always validate input before processing:

public class Payment_Create_DTO
{
    [Required]
    [Range(0.01, double.MaxValue)]
    public decimal Amount { get; set; }

    [Required]
    [StringLength(10)]
    [RegularExpression("^(Income|Outcome)$")]
    public string EntryType { get; set; }

    [StringLength(500)]
    public string? Description { get; set; }
}

SQL Injection Prevention

Always use parameterized queries (EF Core handles this automatically):

// Safe - parameterized
var payments = await _context.Payment_DS
    .Where(p => p.TenantID == tenantId)
    .ToListAsync();

// NEVER do this - vulnerable to SQL injection
var query = $"SELECT * FROM Payments WHERE TenantID = '{tenantId}'";

XSS Prevention

  1. Razor automatically encodes output:

    @Model.UserInput  <!-- Automatically HTML encoded -->
  2. For raw HTML (rare), explicitly mark:

    @Html.Raw(Model.SafeHtml)  <!-- Only for trusted content -->
  3. Content Security Policy header:

    app.Use(async (context, next) =>
    {
        context.Response.Headers.Add("Content-Security-Policy",
            "default-src 'self'; script-src 'self'");
        await next();
    });

HTTPS Enforcement

Development

if (builder.Environment.IsDevelopment())
{
    options.RequireHttpsMetadata = false; // Allow HTTP in development
}

Production

// Force HTTPS in production
app.UseHttpsRedirection();
app.UseHsts();

// JWT validation requires HTTPS
options.RequireHttpsMetadata = true;

Security Headers

app.Use(async (context, next) =>
{
    // Prevent clickjacking
    context.Response.Headers.Add("X-Frame-Options", "DENY");

    // Prevent MIME type sniffing
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

    // Enable XSS filter
    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");

    // Referrer policy
    context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");

    await next();
});

Audit Logging

All data changes are tracked:

public class Admin_AuditLog
{
    public Guid AuditLogID { get; set; }
    public string Action { get; set; }        // Create, Update, Delete
    public string EntityName { get; set; }    // Payment, Company, etc.
    public Guid EntityID { get; set; }
    public string? OldValues { get; set; }    // JSON of previous state
    public string? NewValues { get; set; }    // JSON of new state
    public Guid UserID { get; set; }
    public Guid TenantID { get; set; }
    public DateTime Timestamp { get; set; }
    public string? IPAddress { get; set; }
}

Security Checklist

Before Deployment

  • Rotate all default secrets
  • Enable HTTPS everywhere
  • Configure proper CORS origins
  • Set up rate limiting
  • Enable security headers
  • Review and minimize exposed endpoints
  • Ensure proper error handling (no stack traces in production)
  • Configure Sentry for error monitoring
  • Run OWASP ZAP security scan
  • Review all user input validation

Ongoing

  • Monitor authentication failures
  • Review audit logs regularly
  • Keep dependencies updated
  • Rotate secrets periodically
  • Review access permissions
  • Test backup restoration

Security Scanning

GitHub Actions includes OWASP ZAP scanning:

# .github/workflows/zap-baseline.yml
- name: OWASP ZAP Baseline Scan
  uses: zaproxy/action-baseline@v0.7.0
  with:
    target: 'https://staging.opencashflow.com'

Run manually for full scan:

docker run -t owasp/zap2docker-stable zap-full-scan.py \
  -t https://staging.opencashflow.com

Reporting Security Issues

If you discover a security vulnerability:

  1. Do NOT create a public GitHub issue
  2. Email security concerns to the maintainers directly
  3. Include steps to reproduce
  4. Allow time for a fix before public disclosure

See SECURITY.md in the repository root for full details.

OpenCashFlow

Preview Status

  • Developer Preview
  • Not production-ready
  • First-run setup included

Clone this wiki locally