Skip to content

Security Architecture

Part of: MPAC SmartPOS Cloud Platform - Product RequirementsVersion: 2.0 Last Updated: 2026-01-28


Overview

This document defines the security architecture for the MPAC platform, covering authentication mechanisms, authorization models, data encryption, and PCI DSS compliance requirements. The architecture implements defense-in-depth principles with multiple layers of security controls including JWT-based authentication for users, OAuth2 for devices, HMAC-authenticated service communication, and comprehensive audit logging for all security-sensitive operations.

Table of Contents


Authentication Summary

Purpose: Secure identity verification for all actors in the system.

Authentication Methods

Identity TypeMethodToken TTLUse CaseToken Type
Human UserEmail/Password + MFA15 minutesPortal accessJWT (RS256)
DeviceOAuth2 private_key_jwt60-120 secondsPOS operationsJWT (ES256)
Payment TokenShort-lived bearer token5 minutesFrontend SDK payment confirmationOpaque token
S2S (HMAC)HMAC-SHA256 signatureN/A (request-scoped)Backend service communicationHMAC signature

Human User Authentication

Flow:

User Login
  └─ POST /auth/user/login
     Body: {email, password}
     ← Returns: {user_id, mfa_required: true, mfa_channel: "sms"}

  └─ POST /auth/user/mfa/send
     Body: {user_id}
     ← Sends MFA code via SMS/Email

  └─ POST /auth/user/mfa/verify
     Body: {user_id, code}
     ← Returns: {access_token, refresh_token, expires_in: 900}

JWT Format (Human):

json
{
  "iss": "svc-portal-users",
  "sub": "user_uuid",
  "role": "MERCHANT_ADMIN",
  "merchant_id": 1,
  "store_id": 2,
  "psp_id": 1,
  "impersonated_by": "system_op_uuid",
  "iat": 1706515200,
  "exp": 1706516100
}

Security Features:

  • bcrypt password hashing (cost factor 12)
  • Rate limiting: 5 failed attempts → 15-minute lockout
  • MFA mandatory for admin roles (SYSTEM_OP, PSP_ADMIN)
  • Session binding to IP address and User-Agent
  • Automatic logout after 15 minutes inactivity

Device Authentication

Flow:

Device Boot
  └─ Generate EC P-256 Keypair (Android Keystore)
     Private key: Hardware-backed, non-exportable

  └─ POST /auth/device/token
     Headers:
       Content-Type: application/x-www-form-urlencoded
     Body:
       grant_type=client_credentials
       client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
       client_assertion=<signed_jwt>

     client_assertion (JWT signed with device private key):
     {
       "iss": "device_uuid",
       "sub": "device_uuid",
       "aud": "https://api.mpac-cloud.com/auth/device/token",
       "exp": 1706515260,  // 60 seconds
       "iat": 1706515200,
       "jti": "unique_request_id"
     }

  ← Returns: {
      access_token: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
      token_type: "Bearer",
      expires_in: 90
    }

Security Features:

  • Hardware-backed keypair (Android Keystore)
  • Short token TTL (60-120 seconds)
  • Token replay prevention (jti claim tracking)
  • Device fingerprinting (serial number + Android ID)
  • Automatic token refresh by SDK (refresh at 75% TTL)

Service-to-Service Authentication (HMAC)

Flow:

python
# Client (svc-smarttab) creating HMAC signature
import hmac
import hashlib
import base64
import time

def create_hmac_auth(method, path, body, secret):
    timestamp = int(time.time())
    message = f"{method}:{path}:{timestamp}:{body}"
    signature = hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).digest()
    signature_b64 = base64.b64encode(signature).decode()

    return f"HMAC-SHA256 svc-smarttab:{timestamp}:{signature_b64}"

# Usage
auth_header = create_hmac_auth(
    "POST",
    "/v1/payment_intents",
    '{"amount":100000}',
    "shared_secret_abc123"
)
# Result: "HMAC-SHA256 svc-smarttab:1706515200:aGVsbG8gd29ybGQ="

Server Validation:

python
# Server (mpac-pgw) validating HMAC signature
def verify_hmac_auth(request, secret):
    auth_header = request.headers["Authorization"]
    # Parse: "HMAC-SHA256 client_id:timestamp:signature"
    parts = auth_header.replace("HMAC-SHA256 ", "").split(":")
    client_id, timestamp, signature_b64 = parts

    # Check timestamp (reject if > 5 minutes old)
    if abs(int(time.time()) - int(timestamp)) > 300:
        raise Unauthorized("Request expired")

    # Reconstruct message
    message = f"{request.method}:{request.path}:{timestamp}:{request.body}"

    # Calculate expected signature
    expected_sig = hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).digest()
    expected_b64 = base64.b64encode(expected_sig).decode()

    # Constant-time comparison
    if not hmac.compare_digest(signature_b64, expected_b64):
        raise Unauthorized("Invalid signature")

    return client_id

Security Features:

  • Request integrity protection
  • Timestamp validation (reject old requests)
  • Replay attack prevention
  • Constant-time signature comparison
  • Secret rotation every 180 days

Authorization Model

Purpose: Role-based access control with hierarchical permissions.

Role Hierarchy

SYSTEM_OP (Level 5)
  └─ Full system access
  └─ Can impersonate: PSP_ADMIN, MERCHANT_ADMIN, STORE_MANAGER, STAFF

PSP_ADMIN (Level 4)
  └─ Manage all merchants under PSP
  └─ Can impersonate: MERCHANT_ADMIN, STORE_MANAGER, STAFF

MERCHANT_ADMIN (Level 3)
  └─ Manage stores under merchant
  └─ Can impersonate: STORE_MANAGER, STAFF

STORE_MANAGER (Level 2)
  └─ Manage devices and staff for store
  └─ Cannot impersonate

STAFF (Level 1)
  └─ Limited read access
  └─ Cannot impersonate

Permission Enforcement

Middleware Pattern:

python
from fastapi import Depends, HTTPException, status
from typing import Annotated

class Caller:
    user_id: str
    role: UserRole
    merchant_id: int | None
    store_id: int | None
    psp_id: int | None
    impersonated_by: str | None

async def get_caller(
    authorization: str = Header(...),
    jwt_validator: JWTValidator = Depends()
) -> Caller:
    """Extract and validate caller identity from JWT."""
    token = authorization.replace("Bearer ", "")
    claims = await jwt_validator.validate(token)

    return Caller(
        user_id=claims["sub"],
        role=UserRole(claims["role"]),
        merchant_id=claims.get("merchant_id"),
        store_id=claims.get("store_id"),
        psp_id=claims.get("psp_id"),
        impersonated_by=claims.get("impersonated_by")
    )

CallerDep = Annotated[Caller, Depends(get_caller)]

Endpoint Authorization:

python
@app.get("/merchants/{merchant_id}")
async def get_merchant(
    merchant_id: int,
    caller: CallerDep
):
    # System operators can access any merchant
    if caller.role == UserRole.SYSTEM_OP:
        pass  # Allow

    # PSP admins can only access merchants in their PSP
    elif caller.role == UserRole.PSP_ADMIN:
        merchant = await get_merchant_by_id(merchant_id)
        if merchant.psp_id != caller.psp_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Access denied to merchant outside your PSP"
            )

    # Merchant admins can only access their own merchant
    elif caller.role == UserRole.MERCHANT_ADMIN:
        if merchant_id != caller.merchant_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Access denied to other merchants"
            )

    # Store managers and staff cannot access merchant details
    else:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Insufficient permissions"
        )

    return await get_merchant_by_id(merchant_id)

Impersonation Flow

Purpose: Allow admins to troubleshoot user issues by impersonating lower roles.

Flow:

Admin Login (SYSTEM_OP)
  └─ Regular JWT issued: {sub: admin_uuid, role: SYSTEM_OP}

  └─ POST /auth/admin/impersonate
     Body: {target_user_id: "merchant_admin_uuid"}

     # Validate permissions
     IF admin.role.level <= target.role.level:
       REJECT "Cannot impersonate equal or higher role"

     # Issue impersonation JWT
     ← Returns: {
         access_token: JWT {
           sub: "merchant_admin_uuid",
           role: "MERCHANT_ADMIN",
           merchant_id: 1,
           impersonated_by: "admin_uuid"
         }
       }

  # All actions now logged with impersonation context
  # UI displays: "Impersonating: merchant_admin@example.com"

Audit Log:

sql
INSERT INTO impersonation_logs (
  admin_user_id,
  target_user_id,
  started_at,
  ip_address,
  user_agent
) VALUES (
  'admin_uuid',
  'merchant_admin_uuid',
  NOW(),
  '203.0.113.45',
  'Mozilla/5.0...'
);

Security Features:

  • Downward impersonation only (higher roles → lower roles)
  • Full audit trail with timestamps and IP
  • UI indicator showing impersonation status
  • Automatic session timeout (same as regular JWT)
  • Cannot impersonate other impersonators

Data Encryption

Purpose: Protect sensitive data at rest and in transit.

At Rest Encryption

Database Encryption:

AWS RDS PostgreSQL
  └─ AES-256 encryption enabled
  └─ Encrypted storage volumes
  └─ Encrypted automated backups
  └─ Encrypted read replicas

Application-Level Encryption:

python
from cryptography.fernet import Fernet

class FieldEncryption:
    """Encrypt sensitive fields before storing in database."""

    def __init__(self, kms_client):
        # Fetch data encryption key (DEK) from KMS
        self.dek = kms_client.decrypt(encrypted_dek)
        self.cipher = Fernet(self.dek)

    def encrypt(self, plaintext: str) -> str:
        """Encrypt plaintext value."""
        return self.cipher.encrypt(plaintext.encode()).decode()

    def decrypt(self, ciphertext: str) -> str:
        """Decrypt ciphertext value."""
        return self.cipher.decrypt(ciphertext.encode()).decode()

# Usage in models
class Device(Base):
    __tablename__ = "devices"

    device_uuid = Column(UUID, primary_key=True)
    public_key_pem = Column(Text)  # Not encrypted (public data)
    api_credentials_encrypted = Column(Text)  # Encrypted with DEK

    def set_api_credentials(self, credentials: dict):
        encryptor = FieldEncryption(kms_client)
        self.api_credentials_encrypted = encryptor.encrypt(
            json.dumps(credentials)
        )

Encrypted Fields:

  • JWT private keys (svc-portal)
  • Payment provider API keys (mpac-pgw)
  • HMAC shared secrets (all services)
  • Customer PII (optional, based on requirements)

S3 Encryption:

S3 Buckets (receipts, reports, logs)
  └─ Server-side encryption (SSE-S3)
  └─ Bucket encryption enabled by default
  └─ TLS required for uploads (deny HTTP)

In Transit Encryption

TLS Configuration:

All Services
  └─ TLS 1.2+ (TLS 1.3 preferred)
  └─ Certificate: AWS Certificate Manager
  └─ Cipher suites: ECDHE-RSA-AES128-GCM-SHA256 or stronger
  └─ HSTS enabled: max-age=31536000; includeSubDomains

Certificate Management:

  • Automatic renewal via AWS ACM
  • Wildcard cert: *.mpac-cloud.com
  • Separate cert for devices: device-api.mpac-cloud.com
  • Certificate pinning in device SDK (optional)

Key Management

JWT Signing Keys:

python
# Key rotation schedule
JWT_KEY_ROTATION_DAYS = 90

async def rotate_jwt_keys():
    """Generate new RSA keypair for JWT signing."""
    # Generate new keypair
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048
    )
    public_key = private_key.public_key()

    # Store new key with future activation date
    await db.execute(
        """
        INSERT INTO jwt_keys (key_id, private_key_pem, public_key_pem, active_from)
        VALUES (:key_id, :private_pem, :public_pem, NOW() + INTERVAL '7 days')
        """,
        {
            "key_id": f"kid_{uuid4()}",
            "private_pem": serialize_private_key(private_key),
            "public_pem": serialize_public_key(public_key),
        }
    )

    # Old keys remain valid for 7 days (grace period)
    # After 7 days, only new key is used for signing
    # Old keys still validate existing tokens until expiry

Key Rotation Schedule:

  • JWT signing keys: Every 90 days
  • Provider API keys: Per provider policy (typically 180 days)
  • HMAC secrets: Every 180 days or on compromise
  • Database encryption keys: Annually

PCI DSS Compliance

Purpose: Ensure secure handling of payment card data per PCI DSS requirements.

Payment Data Handling

Never Stored (PCI DSS Requirement 3.2):

  • Full card number (PAN)
  • CVV/CVC security code
  • PIN or PIN block
  • Magnetic stripe data

Stored (Encrypted, PCI DSS Requirement 3.4):

  • Card last 4 digits (truncated PAN)
  • Approval code
  • ARN (Acquirer Reference Number)
  • Expiration date (masked: MM/YY)

Storage Example:

python
class CardSlip(Base):
    """Credit card transaction record (PCI DSS compliant)."""
    __tablename__ = "card_slips"

    id = Column(UUID, primary_key=True)
    # NEVER store full PAN
    card_last_4 = Column(String(4))  # OK: Truncated PAN
    card_brand = Column(String(20))   # OK: Visa, Mastercard, etc.

    # Encrypted sensitive fields
    approval_code_encrypted = Column(Text)
    arn_encrypted = Column(Text)

    # Transaction metadata
    amount = Column(Numeric(15, 2))
    currency = Column(String(3))
    transaction_date = Column(DateTime)

Tokenization

External Processing:

SmartPOS Device (Credit Card App)
  └─ Capture card data via secure card reader
  └─ Send to SP-NET processor (PCI Level 1 certified)
     ├─ SP-NET tokenizes card data
     ├─ Returns payment token
     └─ Returns approval code

  └─ MPAC receives:
     {
       "payment_token": "tok_abc123",  # Token (not PAN)
       "approval_code": "123456",
       "card_last_4": "1234",
       "card_brand": "Visa"
     }

Benefits:

  • MPAC never handles raw card data
  • Reduced PCI scope (SAQ A-EP)
  • External processor handles PCI compliance
  • MPAC stores only tokens and metadata

Network Segmentation

Architecture:

Internet
  └─ WAF (AWS WAF)
     └─ ALB (Application Load Balancer)
        ├─ Public VPC (10.0.0.0/16)
        │  ├─ Portal Web UI
        │  └─ Public API Gateway

        └─ Private VPC (10.1.0.0/16)
           ├─ svc-portal (auth, merchants)
           ├─ svc-smarttab (orders, bills)
           └─ PGW VPC (10.2.0.0/16) - ISOLATED
              └─ mpac-pgw (payment processing)
                 ├─ No direct internet access
                 ├─ NAT Gateway for outbound only
                 └─ Private Link to SP-NET processor

Network Security:

  • PGW isolated in separate VPC
  • Security groups: Deny all by default
  • Only allow traffic from svc-smarttab
  • No SSH access (Session Manager only)
  • VPC Flow Logs enabled

Audit Logging

Security Events Logged:

python
# All security-sensitive operations logged
LOGGED_EVENTS = [
    "user.login",
    "user.login_failed",
    "user.mfa_sent",
    "user.mfa_verified",
    "user.logout",
    "admin.impersonate",
    "device.provision",
    "device.activate",
    "device.suspend",
    "payment.created",
    "payment.completed",
    "payment.refunded",
]

# Log format
{
  "event": "user.login",
  "timestamp": "2026-01-28T11:05:00Z",
  "user_id": "user_uuid",
  "ip_address": "203.0.113.45",
  "user_agent": "Mozilla/5.0...",
  "success": true,
  "metadata": {"mfa_method": "sms"}
}

Log Retention:

  • Security logs: 12 months (online)
  • Audit logs: 7 years (cold storage, S3 Glacier)
  • Tamper-proof: Write-only S3 bucket with object lock

Compliance Measures:

  • Regular security audits (quarterly)
  • Penetration testing (annual)
  • Vulnerability scanning (continuous with AWS Inspector)
  • Access control reviews (monthly)
  • Incident response plan (tested quarterly)

Cross-References

  • Security Testing - Penetration testing procedures
  • Compliance Testing - PCI DSS audit checklist

Previous: Database ArchitectureNext: Performance & ScalabilityUp: Technical Architecture Index

MPAC — MP-Solution Advanced Cloud Service