Files
lua-scripts-w-api/contrib/darktable-api/auth.lua
stephen schuresko bd40b4f850 Add darktable REST API implementation
- Introduced a new Lua-based REST API for darktable, allowing external applications to interact with the darktable library.
- Implemented core functionalities including image listing, retrieval, and preview serving.
- Added support for OAuth 2.0 authentication with token generation.
- Created a simple HTTP router to handle API requests.
- Included JSON encoding/decoding utilities for API responses.
- Developed a script to launch the API server alongside darktable, ensuring seamless integration.
- Added necessary configuration and setup for the API server, including CORS support and error handling.
2026-06-10 15:02:19 -04:00

97 lines
3.5 KiB
Lua

-- OAuth 2.0 Client Credentials grant + HS256 JWT
-- Requires: LuaJIT FFI with libcrypto (OpenSSL)
local ffi = require "ffi"
local base64 = require "base64"
local json = require "json"
local config = require "config"
-- ── OpenSSL HMAC-SHA256 via FFI ───────────────────────────────────────────────
ffi.cdef[[
void *EVP_sha256(void);
unsigned char *HMAC(const void *evp_md, const void *key, int key_len,
const unsigned char *data, size_t data_len,
unsigned char *md, unsigned int *md_len);
]]
-- libcrypto ships as part of OpenSSL; path varies by distro but the soname is standard
local crypto = ffi.load("crypto")
local function hmac_sha256(key, data)
local digest = ffi.new("unsigned char[32]")
local dlen = ffi.new("unsigned int[1]")
crypto.HMAC(crypto.EVP_sha256(), key, #key, data, #data, digest, dlen)
return ffi.string(digest, 32)
end
-- ── JWT helpers ───────────────────────────────────────────────────────────────
local HEADER_B64 = base64.urlencode(json.encode({ alg = "HS256", typ = "JWT" }))
local function jwt_sign(payload_tbl)
local payload = base64.urlencode(json.encode(payload_tbl))
local signing = HEADER_B64 .. "." .. payload
local sig = base64.urlencode(hmac_sha256(config.jwt_secret, signing))
return signing .. "." .. sig
end
local function jwt_verify(token)
local h, p, s = token:match("^([^.]+)%.([^.]+)%.([^.]+)$")
if not h then return nil, "malformed token" end
local expected = base64.urlencode(hmac_sha256(config.jwt_secret, h .. "." .. p))
if s ~= expected then return nil, "invalid signature" end
local payload, err = json.decode(base64.urldecode(p))
if not payload then return nil, "invalid payload: " .. (err or "?") end
if os.time() > (payload.exp or 0) then return nil, "token expired" end
return payload
end
-- ── Public API ────────────────────────────────────────────────────────────────
local M = {}
-- Validate client_id / client_secret pair; returns true + scopes or false + error.
function M.validate_client(client_id, client_secret)
local client = config.clients[client_id]
if not client then return false, "unknown client" end
if client.secret ~= client_secret then return false, "invalid client secret" end
return true, client.scopes
end
-- Issue an access token for a validated client.
function M.issue_token(client_id, scopes)
local now = os.time()
return jwt_sign({
iss = "darktable-api",
sub = client_id,
scope = table.concat(scopes, " "),
iat = now,
exp = now + config.token_ttl,
})
end
-- Extract and verify a Bearer token from a request.
-- Returns payload table on success, or nil + error string on failure.
function M.authenticate(req)
local auth = (req.headers or {})["authorization"] or ""
local token = auth:match("^[Bb]earer%s+(.+)$")
if not token then return nil, "missing Bearer token" end
return jwt_verify(token)
end
-- Check that the authenticated payload contains a required scope.
function M.require_scope(payload, scope)
if not payload then return false end
local scopes = payload.scope or ""
for s in scopes:gmatch("%S+") do
if s == scope then return true end
end
return false
end
return M