Files
ubuntu-based-kiosk/menus/addon_authelia.sh
T
Claude 6c68897935 Migrate Authelia addon; fix real config-clobbering bug in save_config; bump to v2.6.0
Second Addon migrated: menus/addon_authelia.sh (encrypted SSO
credentials, AES-256-CBC with a key derived from /etc/machine-id via
scrypt - same algorithm main.js decrypts with - plus the full
Dockerized server-side setup instructions, now viewable again later
without reconfiguring).

Investigating how to wire its three config.json fields (autheliaURL/
autheliaUsername/autheliaEncryptedPassword) into lib/config.sh surfaced
a real, currently-shipping bug that has nothing to do with Authelia
specifically: save_config() did a full `jq -n` rebuild of config.json
from a fixed list of known fields - identical to what the legacy
script's own save_config still does. The legacy configure_authelia()
writes its three fields via a careful `. + {...}` merge that preserves
everything else already in the file, but neither save_config knew those
fields existed - so the next time a user visited Sites, Touch Controls,
Navigation, or Password Protection (all of which call save_config),
their Authelia credentials were silently deleted. This bug already
existed in the shipped single-file installer; it was ported faithfully
into lib/config.sh's first version because no test happened to set an
untracked field before calling save_config.

Fixed in lib/config.sh: save_config now merges its known fields onto
whatever's already in config.json (jq `. + {...}`) instead of rebuilding
the file from nothing, with a `jq empty` validity check falling back to
`{}` if the existing file is missing or corrupt. Any field this tool
doesn't track - Authelia's three today, anything else a future addon
adds tomorrow - now survives automatically. autheliaURL/
autheliaUsername/autheliaEncryptedPassword are also tracked fields in
their own right now (load_existing_config/save_config), consistent with
every other config.json field this tool manages, giving Authelia both a
direct fix and the general safety net.

The equivalent bug still exists, unfixed, in ubuntu-based-kiosk.sh's own
save_config - noted in both that script's changelog and the Readme's
"Modular Management" section as an open question: whether to backport
just that one fix into the legacy script now, independent of the wider
migration, given it's a real credential-loss bug affecting the
currently-shipping installer today.

Verified:
- New dedicated test (test_save_merge.sh) proving the save_config fix
  itself: seeded config.json with a simulated untracked field via the
  same `. + {...}` merge Authelia's own code uses, called save_config
  from an unrelated context (Sites deleting a tab), and confirmed the
  untracked field survived while the tab deletion still correctly took
  effect (not undone by the merge) - plus corrupt-JSON and
  missing-file edge cases both handled without crashing.
- New scratch-config test for addon_authelia.sh using REAL encryption
  (this sandbox has both Node and /etc/machine-id): configured with a
  real password, then decrypted the stored ciphertext using main.js's
  exact algorithm (independently reproduced in the test) and confirmed
  it recovers the original password exactly - true interoperability,
  not just "some ciphertext was produced." Also covered cancel paths,
  clearing the configuration, the encryption-unavailable failure path,
  and confirmed Authelia's config survives an unrelated Sites save.
- Full regression: re-ran all 9 prior scratch/stub test suites after
  both the lib/config.sh changes - all still clean.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user with a seeded minimal config.json, through Addons ->
  Authelia -> Configure with a real URL/username/password -> confirmed
  the resulting config.json on disk, and independently decrypted the
  stored password for real using main.js's algorithm to confirm it
  matches exactly. Clean exit code 0 throughout.
2026-08-18 21:56:21 +00:00

233 lines
9.6 KiB
Bash

#!/bin/bash
################################################################################
# menus/addon_authelia.sh - "Authelia Auto-Login" addon.
#
# Stores encrypted Authelia SSO credentials so the kiosk authenticates
# automatically on every startup. The password is AES-256-CBC encrypted
# with a key derived from this machine's /etc/machine-id via scrypt -
# the encrypted blob is useless on any other machine - and is NEVER
# stored in plain text, matching the legacy addon exactly (same
# algorithm, same salt, same node crypto calls).
#
# autheliaURL/autheliaUsername/autheliaEncryptedPassword are tracked
# fields in lib/config.sh now (load_existing_config/save_config), same
# as every other config.json field this tool manages - this is also
# what motivated fixing save_config to merge onto the existing file
# instead of rebuilding it from scratch (see lib/config.sh): the legacy
# save_config had no idea these three fields existed, so configuring
# Authelia and then visiting Sites/Touch/Navigation/Password Protection
# in the legacy menu would silently wipe the credentials on the next
# save. Real bug in the shipped script, not unique to this migration.
#
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################
addon_authelia_status() {
if [[ -n "$AUTHELIA_URL" ]]; then
echo "Authelia: configured"
echo " URL: $AUTHELIA_URL"
echo " Username: $AUTHELIA_USERNAME"
else
echo "Authelia: not configured"
fi
}
addon_authelia_menu_builder() {
if [[ -n "$AUTHELIA_URL" ]]; then
MENU_LABELS=("Reconfigure (overwrite)" "Show server-side setup instructions again" "Clear Authelia configuration")
MENU_HANDLERS=(action_configure_authelia action_show_authelia_server_setup action_clear_authelia)
else
MENU_LABELS=("Configure Authelia auto-login")
MENU_HANDLERS=(action_configure_authelia)
fi
}
addon_authelia_menu() {
load_existing_config
if ! sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
log_error "config.json not found at $CONFIG_PATH - run a full install first"
pause
return
fi
run_menu "AUTHELIA AUTO-LOGIN" addon_authelia_menu_builder addon_authelia_status
}
################################################################################
# Encryption
################################################################################
# Same algorithm as the legacy addon: AES-256-CBC, key derived from
# /etc/machine-id via scrypt with a fixed salt, random IV prepended to
# the ciphertext, everything base64-encoded. main.js decrypts with the
# same derivation - do not change this without updating main.js too.
encrypt_authelia_password() {
local password="$1"
command -v node &>/dev/null || return 1
node -e "
const crypto=require('crypto'),fs=require('fs');
const id=fs.readFileSync('/etc/machine-id','utf8').trim();
const key=crypto.scryptSync(id,'kiosk-authelia-v1',32);
const iv=crypto.randomBytes(16);
const c=crypto.createCipheriv('aes-256-cbc',key,iv);
const enc=Buffer.concat([c.update(process.argv[1],'utf8'),c.final()]);
process.stdout.write(Buffer.concat([iv,enc]).toString('base64'));
" "$password" 2>/dev/null
}
################################################################################
# Actions
################################################################################
action_configure_authelia() {
echo
echo "Stores encrypted Authelia credentials so the kiosk"
echo "authenticates automatically on every startup."
echo "Password is encrypted with this machine's unique ID -"
echo "the encrypted blob is useless on any other machine."
echo
if [[ -n "$AUTHELIA_URL" ]]; then
echo "Current config:"
echo " URL: $AUTHELIA_URL"
echo " Username: $AUTHELIA_USERNAME"
echo
ask_yes_no "Overwrite existing Authelia config?" "n" || { echo "Cancelled"; return; }
echo
fi
local url user pass
read -r -p "Authelia URL (e.g. https://auth.yourdomain.com): " url
[[ -z "$url" ]] && { echo "Cancelled"; return; }
read -r -p "Authelia username: " user
[[ -z "$user" ]] && { echo "Cancelled"; return; }
read -r -s -p "Authelia password: " pass
echo
[[ -z "$pass" ]] && { echo "Cancelled"; return; }
echo "Encrypting with machine ID..."
local encrypted
encrypted=$(encrypt_authelia_password "$pass")
if [[ -z "$encrypted" ]]; then
log_error "Encryption failed - is Node.js installed?"
return 1
fi
AUTHELIA_URL="$url"
AUTHELIA_USERNAME="$user"
AUTHELIA_ENCRYPTED_PASSWORD="$encrypted"
save_config
log_success "Authelia config saved (password encrypted, NOT stored in plain text)"
action_show_authelia_server_setup
echo
if is_service_active lightdm && ask_yes_no "Restart kiosk display now?" "n"; then
sudo systemctl restart lightdm
fi
}
action_clear_authelia() {
echo
ask_yes_no "Clear Authelia configuration?" "n" || { echo "Cancelled"; return; }
AUTHELIA_URL=""
AUTHELIA_USERNAME=""
AUTHELIA_ENCRYPTED_PASSWORD=""
save_config
log_success "Authelia configuration cleared"
}
action_show_authelia_server_setup() {
echo
echo "════════════════════════════════════════════════════════════"
echo " AUTHELIA SERVER-SIDE SETUP (Dockerized)"
echo "════════════════════════════════════════════════════════════"
echo
echo "1. Generate the argon2 password hash on your Docker host:"
echo
echo " docker run --rm authelia/authelia:latest \\"
echo " authelia crypto hash generate argon2 \\"
echo " --password 'yourpassword'"
echo
echo " Copy the \$argon2id\$... output — that is your hash."
echo
echo "2. ADD a kiosk user to ~/docker/authelia/config/users.yml"
echo " (append — do not replace existing users):"
echo
echo " kiosk:"
echo " displayname: \"Kiosk Display\""
echo " password: '\$argon2id\$v=19\$m=65536,t=3,p=4\$<paste hash here>'"
echo " email: kiosk@local.com"
echo " groups:"
echo " - kiosk"
echo
echo "3. MERGE into ~/docker/authelia/config/configuration.yml:"
echo
echo " ── access_control ─────────────────────────────────────"
echo " Find your EXISTING access_control block and add the"
echo " kiosk rule as the FIRST rule inside it."
echo
echo " !! DO NOT create a second access_control: block !!"
echo " YAML silently ignores duplicate keys — the kiosk rule"
echo " will be invisible to Authelia and you will get a white"
echo " screen on the kiosk."
echo
echo " Authelia reads rules top-down, first match wins."
echo " The kiosk rule MUST be above any two_factor rule or"
echo " the two_factor wildcard will match first."
echo
echo " ── EXAMPLE — before (your existing config): ──────────"
echo " access_control:"
echo " default_policy: deny"
echo " rules:"
echo " - domain: '*.yourdomain.com'"
echo " policy: two_factor"
echo
echo " ── EXAMPLE — after (add kiosk rule above two_factor): ─"
echo " access_control:"
echo " default_policy: deny"
echo " rules:"
echo " - domain: '*.yourdomain.com' # <-- kiosk first"
echo " subject: 'group:kiosk'"
echo " policy: one_factor"
echo " - domain: '*.yourdomain.com' # <-- existing"
echo " policy: two_factor"
echo
echo " Why one_factor? The kiosk authenticates via the API"
echo " (/api/firstfactor — password only). TOTP and WebAuthn"
echo " require a second interactive step that is impossible"
echo " from a script, so the kiosk group must use one_factor."
echo
echo " ── session ─────────────────────────────────────────────"
echo " Keep your existing session block — no changes needed."
echo " The kiosk re-authenticates via API on every startup so"
echo " session expiry barely matters for it."
echo
echo " If you do NOT yet have a session block, add:"
echo
echo " session:"
echo " expiration: 8h"
echo " inactivity: 1h"
echo " remember_me: 7d"
echo " cookies:"
echo " - domain: yourdomain.com"
echo " authelia_url: https://auth.yourdomain.com"
echo
echo "4. Restart Authelia on your Docker host:"
echo " docker compose restart authelia"
echo
echo "────────────────────────────────────────────────────────────"
echo " NOTE: HTTP Basic Auth (per-site username/password) still"
echo " works alongside Authelia for sites that use browser-popup"
echo " authentication rather than Authelia SSO."
echo "────────────────────────────────────────────────────────────"
echo
echo " To clear Authelia config later, use this menu's"
echo " 'Clear Authelia configuration' option."
echo
pause
}