← Back to LOONIX

Security Architecture

Encryption, OAuth, and XSS Prevention in Zero-Infrastructure Applications

The Challenge: Building secure applications without traditional backend infrastructure requires rethinking security architecture. When you move to a serverless model, you can't rely on server-side validation, session management, or traditional authentication flows.

The Solution: A comprehensive security architecture that leverages modern browser APIs, OAuth 2.0, and defense-in-depth principles. This article covers the complete security stack I've implemented, from encryption to XSS prevention.

Security Architecture Overview

┌─────────────────────────────────────────────────────────────┐ │ SECURITY LAYERS │ ├─────────────────────────────────────────────────────────────┤ │ LAYER 1: CLIENT-SIDE ENCRYPTION │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ AES-256 GCM │ │ RSA-4096 │ │ SHA-256 │ │ │ │ Data at Rest │ │ Key Exchange │ │ Hashing │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ LAYER 2: AUTHENTICATION & AUTHORIZATION │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ OAuth 2.0 │ │ PKCE Flow │ │ Token │ │ │ │ GitHub │ │ Security │ │ Management │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ LAYER 3: INPUT VALIDATION & SANITIZATION │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ CSP Headers │ │ DOMPurify │ │ Type │ │ │ │ XSS Defense │ │ HTML │ │ Validation │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ LAYER 4: NETWORK SECURITY │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ HTTPS Only │ │ HSTS │ │ CORS │ │ │ │ TLS 1.3 │ │ Pinning │ │ Policies │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘

Layer 1: Encryption

Data at Rest: AES-256-GCM

All sensitive data is encrypted before storage using AES-256-GCM (Galois/Counter Mode). This provides both confidentiality and integrity through authenticated encryption.

// Encryption Utility
const CryptoUtils = {
    // Generate a random encryption key
    async generateKey() {
        return await crypto.subtle.generateKey(
            {
                name: 'AES-GCM',
                length: 256
            },
            true,
            ['encrypt', 'decrypt']
        );
    },

    // Encrypt data
    async encrypt(data, key) {
        const encoder = new TextEncoder();
        const iv = crypto.getRandomValues(new Uint8Array(12));

        const encrypted = await crypto.subtle.encrypt(
            {
                name: 'AES-GCM',
                iv: iv
            },
            key,
            encoder.encode(data)
        );

        return {
            ciphertext: this.arrayBufferToBase64(encrypted),
            iv: this.arrayBufferToBase64(iv)
        };
    },

    // Decrypt data
    async decrypt(encryptedData, key) {
        const ciphertext = this.base64ToArrayBuffer(encryptedData.ciphertext);
        const iv = this.base64ToArrayBuffer(encryptedData.iv);

        const decrypted = await crypto.subtle.decrypt(
            {
                name: 'AES-GCM',
                iv: iv
            },
            key,
            ciphertext
        );

        const decoder = new TextDecoder();
        return decoder.decode(decrypted);
    },

    arrayBufferToBase64(buffer) {
        return btoa(String.fromCharCode(...new Uint8Array(buffer)));
    },

    base64ToArrayBuffer(base64) {
        const binary = atob(base64);
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes.buffer;
    }
};
        

Key Exchange: RSA-OAEP

For secure key exchange, I use RSA-OAEP with 4096-bit keys. This allows secure communication between parties without pre-shared secrets.

// RSA Key Generation
const RSAUtils = {
    async generateKeyPair() {
        return await crypto.subtle.generateKey(
            {
                name: 'RSA-OAEP',
                modulusLength: 4096,
                publicExponent: new Uint8Array([1, 0, 1]),
                hash: 'SHA-256'
            },
            true,
            ['encrypt', 'decrypt']
        );
    },

    async exportPublicKey(key) {
        const exported = await crypto.subtle.exportKey('spki', key);
        return this.arrayBufferToBase64(exported);
    },

    async importPublicKey(pem) {
        const binary = this.base64ToArrayBuffer(pem);
        return await crypto.subtle.importKey(
            'spki',
            binary,
            { name: 'RSA-OAEP', hash: 'SHA-256' },
            true,
            ['encrypt']
        );
    }
};
        

Layer 2: OAuth 2.0 Authentication

PKCE Flow for Security

The OAuth 2.0 PKCE (Proof Key for Code Exchange) flow prevents authorization code interception attacks. This is critical for public clients (like single-page apps).

// OAuth 2.0 with PKCE
const OAuth = {
    config: {
        clientId: 'YOUR_CLIENT_ID',
        redirectUri: window.location.origin + '/auth/callback',
        scope: 'repo user',
        authEndpoint: 'https://github.com/login/oauth/authorize',
        tokenEndpoint: 'https://github.com/login/oauth/access_token'
    },

    // Generate code verifier for PKCE
    async generateCodeVerifier() {
        const array = new Uint8Array(32);
        crypto.getRandomValues(array);
        return this.base64UrlEncode(array);
    },

    // Generate code challenge from verifier
    async generateCodeChallenge(verifier) {
        const encoder = new TextEncoder();
        const data = encoder.encode(verifier);
        const digest = await crypto.subtle.digest('SHA-256', data);
        return this.base64UrlEncode(new Uint8Array(digest));
    },

    // Initiate OAuth flow
    async login() {
        const verifier = await this.generateCodeVerifier();
        const challenge = await this.generateCodeChallenge(verifier);

        // Store verifier for callback
        sessionStorage.setItem('pkce_verifier', verifier);

        const params = new URLSearchParams({
            client_id: this.config.clientId,
            redirect_uri: this.config.redirectUri,
            scope: this.config.scope,
            response_type: 'code',
            code_challenge: challenge,
            code_challenge_method: 'S256'
        });

        window.location.href = `${this.config.authEndpoint}?${params}`;
    },

    // Exchange code for token
    async exchangeCodeForToken(code) {
        const verifier = sessionStorage.getItem('pkce_verifier');

        const response = await fetch(this.config.tokenEndpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: JSON.stringify({
                client_id: this.config.clientId,
                code: code,
                redirect_uri: this.config.redirectUri,
                code_verifier: verifier
            })
        });

        if (!response.ok) {
            throw new Error('Token exchange failed');
        }

        const data = await response.json();

        // Store token securely
        await this.storeToken(data.access_token);

        return data.access_token;
    },

    async storeToken(token) {
        // Use encrypted sessionStorage
        const key = await this.getOrCreateEncryptionKey();
        const encrypted = await CryptoUtils.encrypt(token, key);
        sessionStorage.setItem('encrypted_token', JSON.stringify(encrypted));
    },

    async getToken() {
        const encrypted = JSON.parse(sessionStorage.getItem('encrypted_token'));
        if (!encrypted) return null;

        const key = await this.getOrCreateEncryptionKey();
        return await CryptoUtils.decrypt(encrypted, key);
    },

    base64UrlEncode(array) {
        return btoa(String.fromCharCode(...array))
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }
};
        

Token Management

Access tokens are stored encrypted and automatically refreshed before expiration:

// Token Manager
const TokenManager = {
    async refreshToken() {
        const token = await OAuth.getToken();
        if (!token) return null;

        // Check expiration
        const payload = this.parseJwt(token);
        const expiresAt = payload.exp * 1000;
        const now = Date.now();

        // Refresh if expiring within 5 minutes
        if (expiresAt - now < 300000) {
            const newToken = await this.fetchNewToken();
            await OAuth.storeToken(newToken);
            return newToken;
        }

        return token;
    },

    parseJwt(token) {
        const base64Url = token.split('.')[1];
        const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
        const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));

        return JSON.parse(jsonPayload);
    }
};
        

Layer 3: XSS Prevention

Content Security Policy (CSP)

A strict CSP header prevents XSS attacks by controlling which resources can be loaded:

// Content Security Policy
const CSP_POLICY = `
    default-src 'none';
    script-src 'self' https://cdn.jsdelivr.net;
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
    font-src 'self' https://fonts.gstatic.com;
    img-src 'self' data: https: http:;
    connect-src 'self' https://api.github.com;
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    frame-src 'none';
    object-src 'none';
    require-trusted-types-for 'script';
`.trim().replace(/\s+/g, ' ');

// Apply CSP via meta tag (for GitHub Pages)
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = CSP_POLICY;
document.head.appendChild(meta);
        

HTML Sanitization

All user-generated HTML is sanitized using DOMPurify before rendering:

// HTML Sanitization
const Sanitizer = {
    // DOMPurify configuration
    config: {
        ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote'],
        ALLOWED_ATTR: ['href', 'title', 'class'],
        ALLOW_DATA_ATTR: false,
        SAFE_FOR_TEMPLATES: true,
        SANITIZE_DOM: true,
        KEEP_CONTENT: true
    },

    sanitize(html) {
        return DOMPurify.sanitize(html, this.config);
    },

    // Safe HTML insertion
    safeInsert(element, html) {
        const clean = this.sanitize(html);
        element.innerHTML = clean;
    },

    // Safe text content
    safeText(element, text) {
        element.textContent = text;
    }
};
        

Trusted Types API

Modern browsers support Trusted Types, which prevent XSS from DOM sinks:

// Trusted Types Policy
if (window.trustedTypes && window.trustedTypes.createPolicy) {
    const escapePolicy = window.trustedTypes.createPolicy('escapePolicy', {
        createHTML: (string) => {
            return DOMPurify.sanitize(string, { SANITIZE_DOM: true });
        },
        createScriptURL: (string) => {
            const url = new URL(string, window.location.origin);
            if (url.origin !== window.location.origin) {
                throw new Error('External scripts not allowed');
            }
            return string.toString();
        }
    });
}
        

Layer 4: Network Security

HTTPS Enforcement

// Force HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
    location.replace(`https:${location.href.substring(location.protocol.length)}`);
}
        

CORS Configuration

Proper CORS headers prevent cross-origin attacks:

// CORS Configuration
const corsHeaders = {
    'Access-Control-Allow-Origin': window.location.origin,
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400',
    'Access-Control-Allow-Credentials': 'true'
};
        

Security Checklist

All data encrypted at rest (AES-256-GCM)
Never store plaintext sensitive data
OAuth 2.0 with PKCE flow
Prevent authorization code interception
Tokens stored encrypted
Use Web Crypto API for token storage
Strict Content Security Policy
Whitelist all script sources
HTML sanitization with DOMPurify
Clean all user-generated content
HTTPS enforced
No HTTP connections allowed
Input validation on all forms
Validate type, length, and format
No eval() or Function() constructor
Avoid dynamic code execution

Real-World Security Metrics

After implementing this security architecture:

Common Pitfalls to Avoid

1. Storing Tokens in LocalStorage [CRITICAL]

Never store access tokens in localStorage. Any JavaScript can access it, including malicious scripts from XSS vulnerabilities.

// BAD - Token accessible to any script
localStorage.setItem('token', accessToken);

// GOOD - Token encrypted, limited access
await OAuth.storeToken(accessToken);
        

2. Ignoring PKCE [HIGH]

For public clients (SPAs), PKCE is mandatory. Without it, your OAuth flow is vulnerable to authorization code interception.

3. Weak CSP Policies [HIGH]

Avoid unsafe-inline and unsafe-eval in your CSP. These weaken your security posture significantly.

4. Missing Input Validation [MEDIUM]

Always validate input on the client side, even if you don't have a backend. This prevents many common attacks.

Conclusion

Building secure applications without traditional infrastructure is challenging but entirely possible. By leveraging modern browser APIs like Web Crypto, implementing OAuth 2.0 with PKCE, and following defense-in-depth principles, you can create applications that are more secure than many traditional web applications.

The key is to never trust the client—even when you are the client. Validate everything, encrypt everything, and assume that any input could be malicious.

Security is not a feature you add at the end—it's a foundation you build upon.