Authentication and Access Control¶
Verified status as of March 28, 2026. Runtime note: FastFN auto-installs function-local dependencies from
requirements.txt/package.json; host runtimes are required infastfn dev --native, whilefastfn devdepends on a running Docker daemon.
Quick View¶
- Complexity: Intermediate
- Typical time: 10-15 minutes
- Use this when: you need to separate platform admin access from function-level auth
- Outcome: admin surface and business auth rules are clearly scoped
This guide separates two concerns that are usually mixed:
- Platform administration access (
/console,/_fn/*) - Business authentication inside each function route (
/<route>)
1) Platform admin surface¶
Main flags:
FN_UI_ENABLED(default0)FN_CONSOLE_API_ENABLED(default1)FN_CONSOLE_WRITE_ENABLED(default0)FN_CONSOLE_LOCAL_ONLY(default1)FN_ADMIN_TOKEN(optional)
Recommended baseline:
- keep
FN_CONSOLE_LOCAL_ONLY=1 - keep
FN_CONSOLE_WRITE_ENABLED=0unless needed - use
FN_ADMIN_TOKENfor remote/admin automation
Example:
2) Function-level auth patterns¶
Gateway forwards headers/query/body/cookies into event, and per-function env into event.env.
Useful fields:
event.headers.authorizationevent.headers.cookieevent.headers.x-api-keyevent.queryevent.bodyevent.env
Pattern A: API key (Python, Node, PHP, Lua)¶
Python¶
import json
def handler(event):
headers = event.get("headers") or {}
env = event.get("env") or {}
provided = headers.get("x-api-key")
expected = env.get("API_KEY")
if not expected or provided != expected:
return {
"status": 401,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "unauthorized"}),
}
return {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"ok": True}),
}
Node¶
exports.handler = async (event) => {
const headers = event.headers || {};
const env = event.env || {};
const provided = headers['x-api-key'];
const expected = env.API_KEY;
if (!expected || provided !== expected) {
return {
status: 401,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'unauthorized' }),
};
}
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ok: true }),
};
};
PHP¶
<?php
function handler($event) {
$headers = $event['headers'] ?? [];
$env = $event['env'] ?? [];
$provided = $headers['x-api-key'] ?? null;
$expected = $env['API_KEY'] ?? null;
if (!$expected || $provided !== $expected) {
return [
'status' => 401,
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['error' => 'unauthorized']),
];
}
return [
'status' => 200,
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['ok' => true]),
];
}
Lua¶
local cjson = require("cjson.safe")
function handler(event)
local headers = event.headers or {}
local env = event.env or {}
local provided = headers["x-api-key"]
local expected = env.API_KEY
if not expected or provided ~= expected then
return {
status = 401,
headers = { ["Content-Type"] = "application/json" },
body = cjson.encode({ error = "unauthorized" }),
}
end
return {
status = 200,
headers = { ["Content-Type"] = "application/json" },
body = cjson.encode({ ok = true }),
}
end
Store secret in function env:
curl -sS 'http://127.0.0.1:8080/_fn/function-env?runtime=python&name=hello' \
-X PUT -H 'Content-Type: application/json' \
--data '{"API_KEY":{"value":"hello-secret","is_secret":true}}'
Pattern B: Session cookie (Python, Node, PHP, Lua)¶
Python¶
import json
def handler(event):
headers = event.get("headers") or {}
cookie = headers.get("cookie") or headers.get("Cookie") or ""
if "session_id=" not in cookie:
return {
"status": 401,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "missing session"}),
}
return {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"ok": True}),
}
Node¶
exports.handler = async (event) => {
const headers = event.headers || {};
const cookie = headers.cookie || headers.Cookie || '';
if (!cookie.includes('session_id=')) {
return {
status: 401,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'missing session' }),
};
}
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ok: true }),
};
};
PHP¶
<?php
function handler($event) {
$headers = $event['headers'] ?? [];
$cookie = $headers['cookie'] ?? ($headers['Cookie'] ?? '');
if (strpos($cookie, 'session_id=') === false) {
return [
'status' => 401,
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['error' => 'missing session']),
];
}
return [
'status' => 200,
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['ok' => true]),
];
}
Lua¶
local cjson = require("cjson.safe")
function handler(event)
local headers = event.headers or {}
local cookie = headers.cookie or headers.Cookie or ""
if not string.find(cookie, "session_id=", 1, true) then
return {
status = 401,
headers = { ["Content-Type"] = "application/json" },
body = cjson.encode({ error = "missing session" }),
}
end
return {
status = 200,
headers = { ["Content-Type"] = "application/json" },
body = cjson.encode({ ok = true }),
}
end
Secret visibility and masking¶
Use fn.env.json with is_secret:
Common mistakes¶
- mixing admin security with function business auth
- exposing
/_fn/*publicly without controls - hardcoding secrets in source instead of
event.env
Flow Diagram¶
flowchart LR
A["Client request"] --> B["Route discovery"]
B --> C["Policy and method validation"]
C --> D["Runtime handler execution"]
D --> E["HTTP response + OpenAPI parity"]
Objective¶
Clear scope, expected outcome, and who should use this page.
Prerequisites¶
- FastFN CLI available
- Runtime dependencies by mode verified (Docker for
fastfn dev, OpenResty+runtimes forfastfn dev --native)
Validation Checklist¶
- Command examples execute with expected status codes
- Routes appear in OpenAPI where applicable
- References at the end are reachable
Troubleshooting¶
- If runtime is down, verify host dependencies and health endpoint
- If routes are missing, re-run discovery and check folder layout
See also¶
Simple token gate (minimum viable auth)¶
A minimal gate should reject missing/invalid credentials early:
curl -i 'http://127.0.0.1:8080/private'
curl -i 'http://127.0.0.1:8080/private' -H 'x-api-key: demo'
Expected:
- missing/invalid token ->
401 - valid token ->
200
OAuth2-like boundaries in FastFN¶
FastFN does not mandate a single OAuth2 framework. Recommended split:
- token issuance in dedicated auth provider
- token verification in function helpers
- claims to permissions mapping in app domain logic
Scope and permission mapping¶
Use stable scopes and deny by default:
reports:readreports:writeadmin:config
Return 403 when authenticated identity lacks required scope.
Auth-specific validation and troubleshooting¶
Validation quick checks:
- missing token ->
401 - valid token without scope ->
403 - valid token with required scope ->
200
If scope checks fail unexpectedly, verify claim parsing format (scope space-separated vs comma-separated).