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 Type | Method | Token TTL | Use Case | Token Type |
|---|---|---|---|---|
| Human User | Email/Password + MFA | 15 minutes | Portal access | JWT (RS256) |
| Device | OAuth2 private_key_jwt | 60-120 seconds | POS operations | JWT (ES256) |
| Payment Token | Short-lived bearer token | 5 minutes | Frontend SDK payment confirmation | Opaque token |
| S2S (HMAC) | HMAC-SHA256 signature | N/A (request-scoped) | Backend service communication | HMAC 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):
{
"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:
# 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:
# 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_idSecurity 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 impersonatePermission Enforcement
Middleware Pattern:
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:
@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:
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 replicasApplication-Level Encryption:
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; includeSubDomainsCertificate 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:
# 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 expiryKey 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:
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 processorNetwork 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:
# 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
Related Domains
- Identity & Access Management (IAM) - Detailed authentication flows
- Payment Gateway - Payment security requirements
- Device Management - Device provisioning security
Related Technical Sections
- Communication Patterns - Protocol-level security
- Database Architecture - Data encryption at rest
- Performance & Scalability - Rate limiting and DDoS protection
Related Testing Sections
- Security Testing - Penetration testing procedures
- Compliance Testing - PCI DSS audit checklist
Navigation
Previous: Database ArchitectureNext: Performance & ScalabilityUp: Technical Architecture Index