Skip to main content

Sequence Diagram: License Validation Flow

Purpose: Complete license validation workflow from CODITECT client startup through license token generation and local verification.

Actors:

  • CODITECT Client (local application)
  • License API (Django on GKE)
  • Identity Platform (Google OAuth2)
  • PostgreSQL (Cloud SQL)
  • Cloud KMS (license signing)

Flow: Check-on-start pattern with offline-capable signed tokens


Mermaid Sequence Diagram


Step-by-Step Breakdown

1. Authentication (Steps 1-2)

Client initiates authentication:

# Client-side: Authentication
import requests

def authenticate(email: str, password: str) -> str:
"""
Authenticate with Identity Platform.

Returns JWT access token (1 hour expiry).
"""
response = requests.post(
'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword',
params={'key': API_KEY},
json={
'email': email,
'password': password,
'returnSecureToken': True
}
)

if response.status_code != 200:
raise AuthenticationError('Invalid credentials')

data = response.json()
return data['idToken'] # JWT token

Generate Hardware ID:

# Client-side: Hardware fingerprinting
import hashlib
import uuid
import platform
import subprocess

def generate_hardware_id() -> str:
"""
Generate stable hardware ID for device identification.

Components:
- CPU model (stable across reboots)
- Disk serial number (stable)
- MAC address (stable)

Returns SHA256 hash for privacy.
"""
# Get CPU model
cpu = platform.processor()

# Get disk serial (macOS example)
disk_serial = subprocess.check_output(
['diskutil', 'info', '/'],
text=True
)
# Parse disk serial from output

# Get MAC address
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
for elements in range(0,8*6,8)][::-1])

# Combine and hash
hardware_string = f"{cpu}|{disk_serial}|{mac}"
hardware_id = hashlib.sha256(hardware_string.encode()).hexdigest()

return hardware_id

2. License Validation Request (Step 3)

Client sends validation request:

# Client-side: License validation
def validate_license(
jwt_token: str,
license_key: str,
hardware_id: str,
version: str
) -> dict:
"""
Validate license with cloud API.

Returns signed license token if valid.
"""
response = requests.post(
'https://api.coditect.ai/api/v1/license/validate',
headers={
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
},
json={
'license_key': license_key,
'hardware_id': hardware_id,
'version': version
}
)

if response.status_code != 200:
raise LicenseValidationError(response.json()['error'])

return response.json()

3. API Processing (Steps 4-9)

Server-side: License validation endpoint:

# Server-side: Django REST Framework endpoint
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import serializers
from django.utils import timezone
from datetime import timedelta
from apps.licenses.models import License, DeviceActivation, Tier
from apps.tenants.context import get_current_tenant

class LicenseValidationSerializer(serializers.Serializer):
"""Serializer for license validation request."""
license_key = serializers.CharField(max_length=255)
hardware_id = serializers.CharField(max_length=255)
version = serializers.CharField(max_length=50)

class LicenseValidationResponseSerializer(serializers.Serializer):
"""Serializer for license validation response."""
license_token = serializers.DictField()
valid_until = serializers.DateTimeField()

class LicenseViewSet(viewsets.GenericViewSet):
"""License validation and management endpoints."""
permission_classes = [IsAuthenticated]

@action(detail=False, methods=['post'])
def validate(self, request):
"""
Validate license and return signed token.

Validation steps:
1. JWT validation (via IsAuthenticated permission)
2. License lookup in database (tenant-scoped)
3. Active/expiry checks
4. Version compatibility check
5. Device limit check (if applicable)
6. Sign license token with Cloud KMS
"""
# Step 1: JWT already validated by IsAuthenticated permission
user = request.user
tenant = get_current_tenant()

# Validate request data
serializer = LicenseValidationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

license_key = serializer.validated_data['license_key']
hardware_id = serializer.validated_data['hardware_id']
version = serializer.validated_data['version']

# Step 2: Look up license (automatic tenant filtering via TenantModel)
try:
license_obj = License.objects.get(license_key=license_key)
except License.DoesNotExist:
return Response(
{'error': 'License not found'},
status=status.HTTP_404_NOT_FOUND
)

# Step 3: Validation checks
if not license_obj.is_active:
return Response(
{'error': 'License inactive'},
status=status.HTTP_403_FORBIDDEN
)

if license_obj.is_expired:
return Response(
{'error': 'License expired'},
status=status.HTTP_403_FORBIDDEN
)

# Verify tenant matches user (redundant with RLS, but explicit check)
if license_obj.tenant_id != tenant.id:
return Response(
{'error': 'License not owned by user'},
status=status.HTTP_403_FORBIDDEN
)

# Step 4: Version compatibility
if not license_obj.is_version_allowed(version):
return Response(
{
'error': f'Version {version} not allowed for {license_obj.tier} tier'
},
status=status.HTTP_403_FORBIDDEN
)

# Step 5: Device limit check (Team/Enterprise only)
if license_obj.tier in [Tier.TEAM, Tier.ENTERPRISE]:
activation_count = DeviceActivation.objects.filter(
license=license_obj,
is_active=True
).count()

device_limit = license_obj.get_device_limit()

if activation_count >= device_limit:
# Check if this hardware_id is already activated
existing = DeviceActivation.objects.filter(
license=license_obj,
hardware_id=hardware_id,
is_active=True
).first()

if not existing:
return Response(
{
'error': f'Device limit ({device_limit}) exceeded',
'active_devices': activation_count,
'limit': device_limit
},
status=status.HTTP_403_FORBIDDEN
)

# Step 6: Build license payload
valid_until = timezone.now() + timedelta(hours=24)

license_payload = {
'license_key': license_obj.license_key,
'tier': license_obj.tier,
'expires_at': license_obj.expires_at.isoformat(),
'max_seats': license_obj.max_seats,
'allowed_versions': license_obj.get_allowed_versions(),
'hardware_id': hardware_id,
'issued_at': timezone.now().isoformat(),
'valid_until': valid_until.isoformat()
}

# Step 7: Sign with Cloud KMS
from apps.licenses.services import sign_license_token
signature = sign_license_token(license_payload)

license_token = {
**license_payload,
'signature': signature
}

# Serialize response
response_serializer = LicenseValidationResponseSerializer({
'license_token': license_token,
'valid_until': valid_until
})

return Response(response_serializer.data, status=status.HTTP_200_OK)

Cloud KMS signing:

# apps/licenses/services.py - Cloud KMS integration
from google.cloud import kms
import hashlib
import base64
import json

def sign_license_token(payload: dict) -> str:
"""
Sign license token with Cloud KMS RSA-4096 key.

Private key never leaves Cloud HSM.
"""
# Serialize payload (canonical JSON for consistent hashing)
payload_json = json.dumps(payload, sort_keys=True)

# Hash payload (SHA256)
digest = hashlib.sha256(payload_json.encode()).digest()

# Sign with Cloud KMS
client = kms.KeyManagementServiceClient()

key_name = (
'projects/coditect-cloud-infra/locations/global/'
'keyRings/license-signing/cryptoKeys/license-key/'
'cryptoKeyVersions/1'
)

digest_dict = {'sha256': digest}

response = client.asymmetric_sign(
request={
'name': key_name,
'digest': digest_dict
}
)

# Base64 encode signature
signature = base64.b64encode(response.signature).decode()

return signature

4. Client Verification (Steps 10-13)

Client-side: Signature verification:

# Client-side: Verify license signature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import base64
import json

def verify_license_signature(license_token: dict) -> bool:
"""
Verify license token signature using KMS public key.

Can be done offline - no network required.
"""
# Extract signature
signature = base64.b64decode(license_token['signature'])

# Reconstruct payload (without signature)
payload = {k: v for k, v in license_token.items() if k != 'signature'}
payload_json = json.dumps(payload, sort_keys=True)

# Load KMS public key (cached locally)
public_key_pem = load_kms_public_key() # From local cache

public_key = serialization.load_pem_public_key(
public_key_pem.encode(),
backend=default_backend()
)

# Verify signature
try:
public_key.verify(
signature,
payload_json.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception:
return False

Cache license locally:

# Client-side: Cache license
import os
import json

def cache_license_locally(license_token: dict):
"""
Cache validated license token for offline use.

Location: ~/.coditect/license.json
"""
license_cache_path = os.path.expanduser('~/.coditect/license.json')

os.makedirs(os.path.dirname(license_cache_path), exist_ok=True)

with open(license_cache_path, 'w') as f:
json.dump(license_token, f, indent=2)

print(f"License cached at {license_cache_path}")

Error Scenarios

Invalid Credentials

License Expired

Device Limit Exceeded

Invalid Signature


Performance Characteristics

Latency Breakdown:

StepOperationLatencyNotes
1Authentication (Identity Platform)~200msFirst time only, cached
2Hardware ID generation~50msLocal CPU/disk/MAC read
3API request (network)~100msDepends on location
4JWT validation~10msLocal Redis cache lookup
5Database query~5msIndexed lookup
6Validation checks~2msIn-memory
7Device limit query~5msConditional
8Cloud KMS signing~50msHSM operation
9Build token~1msJSON serialization
10Signature verification~10msLocal RSA verification
11Cache write~5msLocal file write

Total: ~438ms (first time), ~438ms (cached auth)

Offline Mode: Once license cached and signature verified, CODITECT can run offline until valid_until expires (24 hours default).


Security Considerations

Threat Model:

  1. Man-in-the-Middle (MITM):

    • Mitigation: TLS 1.3 for all API calls
    • Certificate pinning in client
  2. License Tampering:

    • Mitigation: Cloud KMS RSA-4096 signature
    • Client verifies signature before accepting
    • Tampering detected immediately
  3. Token Theft:

    • Mitigation: JWT short-lived (1 hour)
    • Refresh tokens rotated
    • Hardware ID binding prevents use on other devices
  4. Replay Attacks:

    • Mitigation: issued_at timestamp in license token
    • valid_until expiry enforced
    • Nonce could be added for extra protection
  5. Credential Stuffing:

    • Mitigation: Rate limiting on authentication endpoint
    • Account lockout after 5 failed attempts
    • CAPTCHA on repeated failures

  • ADR-001: License Key Generation
  • ADR-003: Seat Management
  • ADR-007: Hardware Fingerprinting
  • ADR-016: Version-Based Licensing
  • ADR-020: Security Hardening

Last Updated: 2025-11-30 Diagram Type: Sequence (Mermaid) Scope: Core licensing flow