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:
X-API-KeyheaderAuthorization: Bearer <token>headerAuthorization: Basic <base64>header (extracts the username portion)api_keyquery parameter
fn auth_middleware(app: JunctionApp) -> 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) -> 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) -> 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) -> 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:
| Header | Default Value |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
X-XSS-Protection | 1; mode=block |
Strict-Transport-Security | max-age=31536000; includeSubDomains |
Content-Security-Policy | default-src 'self' |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
X-DNS-Prefetch-Control | off |
X-Download-Options | noopen |
X-Powered-By | Junction |
Override any header in your handler before returning:
fn handler(req: Request) -> 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
"*"forallowed_originsmirrors 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, andAccess-Control-Max-Age - Simple requests get
Access-Control-Allow-OriginandVary: Originheaders - Credentials header is set only when
allow_credentialsistrue
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) -> 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 listRequest Sanitization
// XSS sanitization — escapes HTML special characters
let clean = junction.sanitize_input(user_input);
// & -> & < -> < > -> > " -> " ' -> ' / -> /
// 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, onloadRequest 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 MBParameter 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