BRAIDGROUP
RESEARCH & DEV
54. Framework Docs

Security

Junction ships a comprehensive security layer: authentication middleware (API key, Basic auth, Bearer/JWT), CSRF token validation, Helmet-style security headers, configurable CORS, rate limiting, input validation with JSON schema, request sanitization, and SQL injection prevention. Most security features are implemented as middleware that short-circuits the pipeline on failure.

Authentication Middleware

API Key Extraction

extract_api_key(req) checks the following sources in order:

  1. X-API-Key header
  2. Authorization: Bearer <token> header
  3. Authorization: Basic <base64> header (extracts the username portion)
  4. api_key query parameter
fn auth_middleware(app: JunctionApp) -&gt; fn {
    return fn(req: Request) -> Response {
        let api_key = junction.extract_api_key(req);
        if (api_key == "" || api_key == null) {
            return junction.error("AUTH_REQUIRED", "API key required", 401);
        }
        let key_record = app.storage["api_keys"][api_key];
        if (key_record == null || key_record["status"] != "active") {
            return junction.error("AUTH_INVALID", "Invalid API key", 401);
        }
        req.auth_context = {
            "tenant_id": key_record["tenant_id"],
            "key_id": key_record["key_id"],
            "permissions": key_record["permissions"]
        };
        return null;
    };
}

junction.use_middleware(app, auth_middleware(app));

Basic Auth

junction.route(app, "GET", "/protected", fn(req: Request) -&gt; Response {
    let creds = junction.basic_auth(req);
    if (creds["username"] == "admin" && creds["password"] == "secret") {
        return junction.json({"access": "granted"}, 200);
    }
    return junction.error("AUTH_FAILED", "Invalid credentials", 401);
});

Bearer Token / JWT

junction.route(app, "GET", "/api/data", fn(req: Request) -&gt; Response {
    let token = junction.bearer_token(req);
    if (token == "") {
        return junction.error("TOKEN_REQUIRED", "Bearer token required", 401);
    }
    // Validate token (JWT decode, signature check, expiry)
    req.auth_context = {"token": token, "valid": true};
    return junction.json({"data": "protected"}, 200);
});

require_auth Guard

The shorthand require_auth(req) returns true if any API key is present (from any source), false otherwise.

junction.route(app, "POST", "/api/v1/events", fn(req: Request) -&gt; Response {
    if (!junction.require_auth(req)) {
        return junction.error("JUNCTION-401", "Unauthorized", 401);
    }
    return junction.json({"event_id": "evt_123"}, 200);
});

CSRF Protection

csrf_middleware(app) intercepts all state-changing HTTP methods (POST, PUT, PATCH, DELETE) and validates a CSRF token. The token must be sent via the X-CSRF-Token header or _csrf_token form field. It verifies the token against the session store. Use generate_csrf_token(app, req) to mint tokens in your templates.

junction.use_middleware(app, junction.csrf_middleware(app));

// In a form handler, generate a CSRF token:
junction.route(app, "GET", "/form", fn(req: Request) -> Response {
    let token = junction.generate_csrf_token(app, req);
    let form_html = "<form method='POST' action='/submit'>" +
        "<input type='hidden' name='_csrf_token' value='" + token + "'/>" +
        "<button>Submit</button></form>";
    return junction.html(form_html, 200);
});

Helmet Security Headers

Every response passes through apply_security_headers() which sets these defaults if not already present:

HeaderDefault Value
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection1; mode=block
Strict-Transport-Securitymax-age=31536000; includeSubDomains
Content-Security-Policydefault-src 'self'
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
X-DNS-Prefetch-Controloff
X-Download-Optionsnoopen
X-Powered-ByJunction

Override any header in your handler before returning:

fn handler(req: Request) -&gt; Response {
    let res = junction.json({"ok": true}, 200);
    res.headers["Content-Security-Policy"] = "default-src 'self' https://cdn.example.com";
    return res;
}

The security_headers_middleware(config) variant allows passing a JSON config to customize defaults at the middleware level.

CORS Configuration

The CORS module handles preflight (OPTIONS) and simple cross-origin requests. Configure via cors.config_from_object():

let cors_cfg = cors.config_from_object({
    "allowed_origins": [
        "https://app.example.com",
        "https://*.mycompany.com",
        "http://localhost:3000"
    ],
    "allowed_methods": ["GET", "POST", "PUT", "PATCH", "DELETE"],
    "allowed_headers": [
        "Content-Type",
        "Authorization",
        "X-API-Key",
        "X-Request-Id",
        "X-CSRF-Token"
    ],
    "exposed_headers": ["Content-Length", "X-Request-Id", "X-Error-Code"],
    "allow_credentials": true,
    "max_age": 86400
});

junction.use_middleware(app, cors.cors_middleware(cors_cfg));

Key behaviors:

  • Wildcard "*" for allowed_origins mirrors any origin
  • Wildcard subdomain pattern "*.example.com" matches any subdomain via suffix matching
  • Preflight responses return 204 with Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age
  • Simple requests get Access-Control-Allow-Origin and Vary: Origin headers
  • Credentials header is set only when allow_credentials is true

Rate Limiting

rate_limit_middleware(app, config) implements a per-IP sliding window counter. Configuration accepts limit (max requests) and window_ms (time window in milliseconds). Exceeded limits return 429 with a Retry-After header.

// Global rate limit: 100 requests per minute
junction.use_middleware(app, junction.rate_limit_middleware(app, {
    "limit": 100,
    "window_ms": 60000
}));

// Per-route rate limit via custom auth middleware
fn check_rate_limit(app: JunctionApp, req: Request, key: string, limit: int) -> bool {
    let rl_key = "rl_" + key;
    if (app.storage["rate_limits"][rl_key] == null) {
        app.storage["rate_limits"][rl_key] = 1;
        return true;
    }
    app.storage["rate_limits"][rl_key] = app.storage["rate_limits"][rl_key] + 1;
    return app.storage["rate_limits"][rl_key] <= limit;
}

Input Validation with JSON Schema

Register named schemas with register_schema(app, name, schema) and validate requests with validate_request(app, schema_name, req). Schema fields support required, type, min, max, and pattern constraints.

let user_schema = {
    "name": {"required": true, "type": "string"},
    "email": {"required": true, "type": "string", "pattern": "^[^@]+@[^@]+$"},
    "age": {"type": "int", "min": 0, "max": 150},
    "role": {"type": "string"}
};
junction.register_schema(app, "create_user", user_schema);

junction.route(app, "POST", "/users", fn(req: Request) -&gt; Response {
    let err = junction.validate_request(app, "create_user", req);
    if (err != null) { return err; }
    return junction.json({"created": true}, 201);
});

Standalone validation helpers:

junction.validate_email("user@example.com");  // true
junction.validate_url("https://example.com");     // true
junction.validate_uuid("550e8400-e29b-41d4-a716-446655440000");  // true
junction.validate_date("2024-01-15T10:30:00Z");   // true
junction.validate_numeric_range(value, 0, 100);    // true if in range
junction.validate_string_length(name, 2, 64);      // true if in bounds
junction.validate_in_list(role, ["admin", "user"]); // true if in list

Request Sanitization

// XSS sanitization — escapes HTML special characters
let clean = junction.sanitize_input(user_input);
// & -> &amp;  < -> &lt;  > -> &gt;  " -> &quot;  ' -> &#x27;  / -> &#x2F;

// SQL injection prevention — escapes single quotes and comment tokens
let safe = junction.prevent_sql_injection(user_input);

// URL parameter encoding
let encoded = junction.sanitize_url_param(user_input);

// Injection pattern detection
let safe = junction.validate_param_no_injection(user_input);
// Checks for <script>, javascript:, onerror, onclick, onload

Request Size Enforcement

The default request size limit is 10 MB (10485760 bytes). Override via app.request_size_limit. Returns 413 when exceeded.

app.request_size_limit = 52428800; // 50 MB

Parameter Pollution Protection

let allowed = ["name", "email", "page"];
let clean_query = junction.clean_query_params(req.query, allowed);
// Drops any query param not in the allowlist

let first_val = junction.first_query_value(req.query, "page");
// Returns the first value if repeated, or empty string